apex-ruby 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/apex_ext/apex_ext.c +6 -0
- data/ext/apex_ext/apex_src/AGENTS.md +41 -0
- data/ext/apex_ext/apex_src/CHANGELOG.md +412 -2
- data/ext/apex_ext/apex_src/CMakeLists.txt +41 -29
- data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
- data/ext/apex_ext/apex_src/Package.swift +9 -0
- data/ext/apex_ext/apex_src/README.md +31 -9
- data/ext/apex_ext/apex_src/ROADMAP.md +5 -0
- data/ext/apex_ext/apex_src/VERSION +1 -1
- data/ext/apex_ext/apex_src/cli/main.c +1125 -13
- data/ext/apex_ext/apex_src/docs/index.md +459 -0
- data/ext/apex_ext/apex_src/include/apex/apex.h +67 -5
- data/ext/apex_ext/apex_src/include/apex/ast_man.h +20 -0
- data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +39 -0
- data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +40 -0
- data/ext/apex_ext/apex_src/include/apex/module.modulemap +1 -1
- data/ext/apex_ext/apex_src/man/apex-config.5 +333 -258
- data/ext/apex_ext/apex_src/man/apex-config.5.md +3 -1
- data/ext/apex_ext/apex_src/man/apex-plugins.7 +401 -316
- data/ext/apex_ext/apex_src/man/apex.1 +663 -620
- data/ext/apex_ext/apex_src/man/apex.1.html +703 -0
- data/ext/apex_ext/apex_src/man/apex.1.md +160 -90
- data/ext/apex_ext/apex_src/objc/Apex.swift +6 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.h +12 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.m +9 -0
- data/ext/apex_ext/apex_src/pages/index.md +459 -0
- data/ext/apex_ext/apex_src/src/_README.md +4 -4
- data/ext/apex_ext/apex_src/src/apex.c +702 -44
- data/ext/apex_ext/apex_src/src/ast_json.c +1130 -0
- data/ext/apex_ext/apex_src/src/ast_json.h +46 -0
- data/ext/apex_ext/apex_src/src/ast_man.c +948 -0
- data/ext/apex_ext/apex_src/src/ast_markdown.c +409 -0
- data/ext/apex_ext/apex_src/src/ast_terminal.c +2516 -0
- data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +8 -5
- data/ext/apex_ext/apex_src/src/extensions/definition_list.c +491 -1514
- data/ext/apex_ext/apex_src/src/extensions/definition_list.h +8 -15
- data/ext/apex_ext/apex_src/src/extensions/emoji.c +207 -0
- data/ext/apex_ext/apex_src/src/extensions/emoji.h +14 -0
- data/ext/apex_ext/apex_src/src/extensions/header_ids.c +178 -71
- data/ext/apex_ext/apex_src/src/extensions/highlight.c +37 -5
- data/ext/apex_ext/apex_src/src/extensions/ial.c +416 -47
- data/ext/apex_ext/apex_src/src/extensions/includes.c +241 -10
- data/ext/apex_ext/apex_src/src/extensions/includes.h +1 -0
- data/ext/apex_ext/apex_src/src/extensions/metadata.c +166 -3
- data/ext/apex_ext/apex_src/src/extensions/metadata.h +7 -0
- data/ext/apex_ext/apex_src/src/extensions/sup_sub.c +34 -3
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +55 -10
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.h +7 -4
- data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +84 -52
- data/ext/apex_ext/apex_src/src/extensions/toc.c +133 -19
- data/ext/apex_ext/apex_src/src/filters_ast.c +194 -0
- data/ext/apex_ext/apex_src/src/filters_ast.h +36 -0
- data/ext/apex_ext/apex_src/src/html_renderer.c +1265 -35
- data/ext/apex_ext/apex_src/src/html_renderer.h +21 -0
- data/ext/apex_ext/apex_src/src/plugins_remote.c +40 -14
- data/ext/apex_ext/apex_src/tests/CMakeLists.txt +1 -0
- data/ext/apex_ext/apex_src/tests/README.md +11 -5
- data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +13 -2
- data/ext/apex_ext/apex_src/tests/fixtures/filters/filter_output_with_rawblock.json +1 -0
- data/ext/apex_ext/apex_src/tests/fixtures/filters/unwrap.md +7 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/auto-wildcard.md +8 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.avif +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.jpg +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.webp +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.avif +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.jpg +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.webp +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/media_formats_test.md +63 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/data-semi.csv +3 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/with space.txt +1 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/inline_tables_test.md +4 -1
- data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +64 -0
- data/ext/apex_ext/apex_src/tests/terminal_width_test.sh +29 -0
- data/ext/apex_ext/apex_src/tests/test-swift-package.sh +14 -0
- data/ext/apex_ext/apex_src/tests/test_cmark_callback.c +189 -0
- data/ext/apex_ext/apex_src/tests/test_extensions.c +374 -0
- data/ext/apex_ext/apex_src/tests/test_metadata.c +68 -0
- data/ext/apex_ext/apex_src/tests/test_output.c +291 -2
- data/ext/apex_ext/apex_src/tests/test_runner.c +10 -0
- data/ext/apex_ext/apex_src/tests/test_syntax_highlight.c +1 -1
- data/ext/apex_ext/apex_src/tests/test_tables.c +17 -1
- data/lib/apex/version.rb +1 -1
- metadata +32 -2
- data/ext/apex_ext/apex_src/docs/FUTURE_FEATURES.md +0 -456
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include "table.h" /* For CMARK_NODE_TABLE */
|
|
7
7
|
#include "apex/apex.h" /* For apex_mode_t */
|
|
8
8
|
#include <string.h>
|
|
9
|
+
#include <strings.h> /* For strcasecmp */
|
|
9
10
|
#include <stdlib.h>
|
|
10
11
|
#include <ctype.h>
|
|
11
12
|
#include <stdio.h>
|
|
@@ -214,6 +215,52 @@ apex_attributes *parse_ial_content(const char *content, int len) {
|
|
|
214
215
|
continue;
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
/* Check for bare webp/avif (picture srcset format markers) */
|
|
219
|
+
if (p > key_start && (size_t)(p - key_start) == 4 &&
|
|
220
|
+
key_start[0] == 'w' && key_start[1] == 'e' && key_start[2] == 'b' && key_start[3] == 'p') {
|
|
221
|
+
add_attribute(attrs, "data-srcset-webp", "1");
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (p > key_start && (size_t)(p - key_start) == 4 &&
|
|
225
|
+
key_start[0] == 'a' && key_start[1] == 'v' && key_start[2] == 'i' && key_start[3] == 'f') {
|
|
226
|
+
add_attribute(attrs, "data-srcset-avif", "1");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Check for bare video format markers (webm, ogg, mp4, mov, m4v) */
|
|
231
|
+
if (p > key_start && (size_t)(p - key_start) == 4 &&
|
|
232
|
+
key_start[0] == 'w' && key_start[1] == 'e' && key_start[2] == 'b' && key_start[3] == 'm') {
|
|
233
|
+
add_attribute(attrs, "data-video-webm", "1");
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
237
|
+
key_start[0] == 'o' && key_start[1] == 'g' && key_start[2] == 'g') {
|
|
238
|
+
add_attribute(attrs, "data-video-ogg", "1");
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
242
|
+
key_start[0] == 'm' && key_start[1] == 'p' && key_start[2] == '4') {
|
|
243
|
+
add_attribute(attrs, "data-video-mp4", "1");
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
247
|
+
key_start[0] == 'm' && key_start[1] == 'o' && key_start[2] == 'v') {
|
|
248
|
+
add_attribute(attrs, "data-video-mov", "1");
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
252
|
+
key_start[0] == 'm' && key_start[1] == '4' && key_start[2] == 'v') {
|
|
253
|
+
add_attribute(attrs, "data-video-m4v", "1");
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Check for bare auto (discover formats from filesystem) */
|
|
258
|
+
if (p > key_start && (size_t)(p - key_start) == 4 &&
|
|
259
|
+
key_start[0] == 'a' && key_start[1] == 'u' && key_start[2] == 't' && key_start[3] == 'o') {
|
|
260
|
+
add_attribute(attrs, "data-apex-auto", "1");
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
217
264
|
/* Unknown token, skip */
|
|
218
265
|
p++;
|
|
219
266
|
}
|
|
@@ -713,6 +760,17 @@ char *attributes_to_html(apex_attributes *attrs) {
|
|
|
713
760
|
strcmp(key, "data-srcset-3x") == 0) {
|
|
714
761
|
continue;
|
|
715
762
|
}
|
|
763
|
+
/* Skip picture/video format markers - used for picture/video element generation */
|
|
764
|
+
if (strcmp(key, "data-srcset-webp") == 0 ||
|
|
765
|
+
strcmp(key, "data-srcset-avif") == 0 ||
|
|
766
|
+
strcmp(key, "data-video-webm") == 0 ||
|
|
767
|
+
strcmp(key, "data-video-ogg") == 0 ||
|
|
768
|
+
strcmp(key, "data-video-mp4") == 0 ||
|
|
769
|
+
strcmp(key, "data-video-mov") == 0 ||
|
|
770
|
+
strcmp(key, "data-video-m4v") == 0 ||
|
|
771
|
+
strcmp(key, "data-apex-auto") == 0) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
716
774
|
|
|
717
775
|
char attr_str[1024];
|
|
718
776
|
if (first_attr) {
|
|
@@ -1183,32 +1241,45 @@ static bool process_span_ial(cmark_node *para, ald_entry *alds) {
|
|
|
1183
1241
|
|
|
1184
1242
|
/**
|
|
1185
1243
|
* Extract IAL from heading text (inline syntax: ## Heading {: #id})
|
|
1244
|
+
* Headings may have multiple inline children (e.g. when "&" creates HTML_INLINE),
|
|
1245
|
+
* so we must check all children, not just the first.
|
|
1186
1246
|
*/
|
|
1187
1247
|
static bool extract_ial_from_heading(cmark_node *heading, apex_attributes **attrs_out, ald_entry *alds) {
|
|
1188
1248
|
if (cmark_node_get_type(heading) != CMARK_NODE_HEADING) return false;
|
|
1189
1249
|
|
|
1190
|
-
/*
|
|
1191
|
-
|
|
1192
|
-
|
|
1250
|
+
/* Find the text node that contains the IAL - walk all children since "&" etc.
|
|
1251
|
+
can split content across multiple nodes (e.g. TEXT + HTML_INLINE + TEXT) */
|
|
1252
|
+
cmark_node *ial_node = NULL;
|
|
1253
|
+
const char *ial_start = NULL;
|
|
1193
1254
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1255
|
+
for (cmark_node *child = cmark_node_first_child(heading); child; child = cmark_node_next(child)) {
|
|
1256
|
+
if (cmark_node_get_type(child) != CMARK_NODE_TEXT) continue;
|
|
1196
1257
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
if (!ial_start) return false;
|
|
1200
|
-
char second_char = ial_start[1];
|
|
1201
|
-
if (second_char != ':' && second_char != '#' && second_char != '.') return false;
|
|
1258
|
+
const char *text = cmark_node_get_literal(child);
|
|
1259
|
+
if (!text) continue;
|
|
1202
1260
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
if (!close) return false;
|
|
1261
|
+
const char *brace = strrchr(text, '{');
|
|
1262
|
+
if (!brace) continue;
|
|
1206
1263
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1264
|
+
char second_char = brace[1];
|
|
1265
|
+
if (second_char != ':' && second_char != '#' && second_char != '.') continue;
|
|
1266
|
+
|
|
1267
|
+
const char *close = strchr(brace, '}');
|
|
1268
|
+
if (!close) continue;
|
|
1269
|
+
|
|
1270
|
+
const char *after = close + 1;
|
|
1271
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
1272
|
+
if (*after) continue;
|
|
1273
|
+
|
|
1274
|
+
/* Found valid IAL - prefer the rightmost (last) one */
|
|
1275
|
+
ial_node = child;
|
|
1276
|
+
ial_start = brace;
|
|
1277
|
+
}
|
|
1211
1278
|
|
|
1279
|
+
if (!ial_node || !ial_start) return false;
|
|
1280
|
+
|
|
1281
|
+
const char *text = cmark_node_get_literal(ial_node);
|
|
1282
|
+
if (!text) return false;
|
|
1212
1283
|
|
|
1213
1284
|
/* Extract attributes */
|
|
1214
1285
|
if (!extract_ial_from_text(ial_start, attrs_out, alds)) {
|
|
@@ -1229,12 +1300,11 @@ static bool extract_ial_from_heading(cmark_node *heading, apex_attributes **attr
|
|
|
1229
1300
|
char *end = new_text + prefix_len - 1;
|
|
1230
1301
|
while (end >= new_text && isspace((unsigned char)*end)) *end-- = '\0';
|
|
1231
1302
|
} else {
|
|
1232
|
-
/*
|
|
1303
|
+
/* This node was only IAL - leave empty string */
|
|
1233
1304
|
new_text[0] = '\0';
|
|
1234
1305
|
}
|
|
1235
1306
|
|
|
1236
|
-
|
|
1237
|
-
cmark_node_set_literal(text_node, new_text);
|
|
1307
|
+
cmark_node_set_literal(ial_node, new_text);
|
|
1238
1308
|
free(new_text);
|
|
1239
1309
|
return true;
|
|
1240
1310
|
}
|
|
@@ -1783,6 +1853,52 @@ static apex_attributes *parse_image_attributes(const char *attr_str, int len) {
|
|
|
1783
1853
|
continue;
|
|
1784
1854
|
}
|
|
1785
1855
|
|
|
1856
|
+
/* Check for bare webp/avif (picture srcset format markers) */
|
|
1857
|
+
if (p > key_start && (size_t)(p - key_start) == 4 &&
|
|
1858
|
+
key_start[0] == 'w' && key_start[1] == 'e' && key_start[2] == 'b' && key_start[3] == 'p') {
|
|
1859
|
+
add_attribute(attrs, "data-srcset-webp", "1");
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
if (p > key_start && (size_t)(p - key_start) == 4 &&
|
|
1863
|
+
key_start[0] == 'a' && key_start[1] == 'v' && key_start[2] == 'i' && key_start[3] == 'f') {
|
|
1864
|
+
add_attribute(attrs, "data-srcset-avif", "1");
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/* Check for bare video format markers (webm, ogg, mp4, mov, m4v) */
|
|
1869
|
+
if (p > key_start && (size_t)(p - key_start) == 4 &&
|
|
1870
|
+
key_start[0] == 'w' && key_start[1] == 'e' && key_start[2] == 'b' && key_start[3] == 'm') {
|
|
1871
|
+
add_attribute(attrs, "data-video-webm", "1");
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
1875
|
+
key_start[0] == 'o' && key_start[1] == 'g' && key_start[2] == 'g') {
|
|
1876
|
+
add_attribute(attrs, "data-video-ogg", "1");
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
1880
|
+
key_start[0] == 'm' && key_start[1] == 'p' && key_start[2] == '4') {
|
|
1881
|
+
add_attribute(attrs, "data-video-mp4", "1");
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
1885
|
+
key_start[0] == 'm' && key_start[1] == 'o' && key_start[2] == 'v') {
|
|
1886
|
+
add_attribute(attrs, "data-video-mov", "1");
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
1890
|
+
key_start[0] == 'm' && key_start[1] == '4' && key_start[2] == 'v') {
|
|
1891
|
+
add_attribute(attrs, "data-video-m4v", "1");
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/* Check for bare auto (discover formats from filesystem) */
|
|
1896
|
+
if (p > key_start && (size_t)(p - key_start) == 4 &&
|
|
1897
|
+
key_start[0] == 'a' && key_start[1] == 'u' && key_start[2] == 't' && key_start[3] == 'o') {
|
|
1898
|
+
add_attribute(attrs, "data-apex-auto", "1");
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1786
1902
|
/* Unknown token, skip */
|
|
1787
1903
|
p++;
|
|
1788
1904
|
}
|
|
@@ -1861,6 +1977,73 @@ static char *url_with_2x_suffix(const char *url) {
|
|
|
1861
1977
|
return out;
|
|
1862
1978
|
}
|
|
1863
1979
|
|
|
1980
|
+
/**
|
|
1981
|
+
* Replace the file extension in a URL with a new extension.
|
|
1982
|
+
* Uses same path logic as url_with_2x_suffix. Caller must free.
|
|
1983
|
+
* e.g. url_with_extension("img/icon.png?x=1", "webp") -> "img/icon.webp?x=1"
|
|
1984
|
+
*/
|
|
1985
|
+
static char *url_with_extension(const char *url, const char *new_ext) {
|
|
1986
|
+
if (!url || !*url || !new_ext) return NULL;
|
|
1987
|
+
|
|
1988
|
+
const char *p = strstr(url, "://");
|
|
1989
|
+
if (p) p += 3;
|
|
1990
|
+
else p = url;
|
|
1991
|
+
|
|
1992
|
+
const char *first_slash = strchr(p, '/');
|
|
1993
|
+
const char *path_start = first_slash ? first_slash : url;
|
|
1994
|
+
const char *qmark = strchr(path_start, '?');
|
|
1995
|
+
const char *hash = strchr(path_start, '#');
|
|
1996
|
+
const char *path_end = (qmark && hash) ? ((qmark < hash) ? qmark : hash) :
|
|
1997
|
+
qmark ? qmark : hash ? hash : url + strlen(url);
|
|
1998
|
+
|
|
1999
|
+
const char *last_dot = NULL;
|
|
2000
|
+
for (const char *c = path_start; c < path_end; c++) {
|
|
2001
|
+
if (*c == '.') last_dot = c;
|
|
2002
|
+
}
|
|
2003
|
+
if (!last_dot) return NULL;
|
|
2004
|
+
|
|
2005
|
+
size_t prefix_len = (size_t)(last_dot - url);
|
|
2006
|
+
size_t ext_len = strlen(new_ext);
|
|
2007
|
+
size_t tail_len = strlen(path_end); /* from ? or # to end, or 0 */
|
|
2008
|
+
char *out = malloc(prefix_len + 1 + ext_len + tail_len + 1);
|
|
2009
|
+
if (!out) return NULL;
|
|
2010
|
+
|
|
2011
|
+
memcpy(out, url, prefix_len);
|
|
2012
|
+
out[prefix_len] = '.';
|
|
2013
|
+
memcpy(out + prefix_len + 1, new_ext, ext_len + 1);
|
|
2014
|
+
if (tail_len > 0) {
|
|
2015
|
+
memcpy(out + prefix_len + 1 + ext_len, path_end, tail_len + 1);
|
|
2016
|
+
}
|
|
2017
|
+
return out;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Check if URL has a video extension (whitelist: mp4, mov, webm, ogg, ogv, m4v)
|
|
2022
|
+
*/
|
|
2023
|
+
static bool is_video_url(const char *url) {
|
|
2024
|
+
if (!url || !*url) return false;
|
|
2025
|
+
const char *path_end = strchr(url, '?');
|
|
2026
|
+
if (!path_end) path_end = strchr(url, '#');
|
|
2027
|
+
if (!path_end) path_end = url + strlen(url);
|
|
2028
|
+
|
|
2029
|
+
const char *last_dot = NULL;
|
|
2030
|
+
for (const char *c = url; c < path_end; c++) {
|
|
2031
|
+
if (*c == '.') last_dot = c;
|
|
2032
|
+
}
|
|
2033
|
+
if (!last_dot || last_dot <= url) return false;
|
|
2034
|
+
const char *ext = last_dot + 1;
|
|
2035
|
+
size_t ext_len = (size_t)(path_end - ext);
|
|
2036
|
+
if (ext_len == 0) return false;
|
|
2037
|
+
|
|
2038
|
+
if (ext_len == 3 && strncasecmp(ext, "mp4", 3) == 0) return true;
|
|
2039
|
+
if (ext_len == 3 && strncasecmp(ext, "mov", 3) == 0) return true;
|
|
2040
|
+
if (ext_len == 4 && strncasecmp(ext, "webm", 4) == 0) return true;
|
|
2041
|
+
if (ext_len == 3 && strncasecmp(ext, "ogg", 3) == 0) return true;
|
|
2042
|
+
if (ext_len == 3 && strncasecmp(ext, "ogv", 3) == 0) return true;
|
|
2043
|
+
if (ext_len == 3 && strncasecmp(ext, "m4v", 3) == 0) return true;
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1864
2047
|
/**
|
|
1865
2048
|
* Check if attributes contain the @2x/@3x srcset markers
|
|
1866
2049
|
*/
|
|
@@ -1874,6 +2057,23 @@ static bool attrs_have_srcset_3x(apex_attributes *attrs) {
|
|
|
1874
2057
|
return find_attribute_index(attrs, "data-srcset-3x") >= 0;
|
|
1875
2058
|
}
|
|
1876
2059
|
|
|
2060
|
+
static bool attrs_have_srcset_webp(apex_attributes *attrs) {
|
|
2061
|
+
if (!attrs) return false;
|
|
2062
|
+
return find_attribute_index(attrs, "data-srcset-webp") >= 0;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
static bool attrs_have_srcset_avif(apex_attributes *attrs) {
|
|
2066
|
+
if (!attrs) return false;
|
|
2067
|
+
return find_attribute_index(attrs, "data-srcset-avif") >= 0;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
static bool attrs_have_video_format(apex_attributes *attrs, const char *fmt) {
|
|
2071
|
+
if (!attrs) return false;
|
|
2072
|
+
char key[32];
|
|
2073
|
+
snprintf(key, sizeof(key), "data-video-%s", fmt);
|
|
2074
|
+
return find_attribute_index(attrs, key) >= 0;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1877
2077
|
/**
|
|
1878
2078
|
* Build the @3x version of a URL.
|
|
1879
2079
|
* Uses the same domain-safe rules as url_with_2x_suffix.
|
|
@@ -1934,22 +2134,147 @@ static char *url_with_3x_suffix(const char *url) {
|
|
|
1934
2134
|
return out;
|
|
1935
2135
|
}
|
|
1936
2136
|
|
|
2137
|
+
/**
|
|
2138
|
+
* Build picture srcset string for a format (e.g. webp: "base.webp 1x, base@2x.webp 2x").
|
|
2139
|
+
* Uses base url and optional @2x/@3x. Caller must free.
|
|
2140
|
+
*/
|
|
2141
|
+
static char *build_picture_srcset(const char *url, const char *ext, bool want_2x, bool want_3x) {
|
|
2142
|
+
if (!url) return NULL;
|
|
2143
|
+
char *base = url_with_extension(url, ext);
|
|
2144
|
+
if (!base) return NULL;
|
|
2145
|
+
|
|
2146
|
+
char *url_2x = NULL, *url_3x = NULL;
|
|
2147
|
+
if (want_2x) {
|
|
2148
|
+
char *base_2x = url_with_2x_suffix(url);
|
|
2149
|
+
if (base_2x) {
|
|
2150
|
+
url_2x = url_with_extension(base_2x, ext);
|
|
2151
|
+
free(base_2x);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (want_3x) {
|
|
2155
|
+
char *base_3x = url_with_3x_suffix(url);
|
|
2156
|
+
if (base_3x) {
|
|
2157
|
+
url_3x = url_with_extension(base_3x, ext);
|
|
2158
|
+
free(base_3x);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
size_t len = strlen(base) + 32;
|
|
2163
|
+
if (url_2x) len += strlen(url_2x) + 16;
|
|
2164
|
+
if (url_3x) len += strlen(url_3x) + 16;
|
|
2165
|
+
|
|
2166
|
+
char *out = malloc(len);
|
|
2167
|
+
if (!out) {
|
|
2168
|
+
free(base);
|
|
2169
|
+
free(url_2x);
|
|
2170
|
+
free(url_3x);
|
|
2171
|
+
return NULL;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
char *p = out;
|
|
2175
|
+
p += snprintf(p, len, "%s 1x", base);
|
|
2176
|
+
if (url_2x) p += snprintf(p, len - (size_t)(p - out), ", %s 2x", url_2x);
|
|
2177
|
+
if (url_3x) p += snprintf(p, len - (size_t)(p - out), ", %s 3x", url_3x);
|
|
2178
|
+
|
|
2179
|
+
free(base);
|
|
2180
|
+
free(url_2x);
|
|
2181
|
+
free(url_3x);
|
|
2182
|
+
return out;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
1937
2185
|
/**
|
|
1938
2186
|
* Convert image attributes to HTML string, including srcset when @2x/@3x is present.
|
|
1939
2187
|
* When data-srcset-2x/data-srcset-3x are in attrs, emits srcset="url 1x, url@2x 2x[, url@3x 3x]"
|
|
1940
2188
|
* and omits the internal markers from the output attributes.
|
|
2189
|
+
* For video URLs, emits data-apex-replace-video with format markers for renderer replacement.
|
|
2190
|
+
* For webp/avif, emits data-apex-picture-* for renderer to wrap in <picture>.
|
|
1941
2191
|
* Caller must free the returned string.
|
|
1942
2192
|
*/
|
|
1943
2193
|
static char *attributes_to_html_for_image(const char *url, apex_attributes *attrs) {
|
|
1944
2194
|
if (!attrs) return strdup("");
|
|
1945
2195
|
|
|
2196
|
+
bool have_auto = (find_attribute_index(attrs, "data-apex-auto") >= 0);
|
|
1946
2197
|
bool have_2x = attrs_have_srcset_2x(attrs);
|
|
1947
2198
|
bool have_3x = attrs_have_srcset_3x(attrs);
|
|
1948
|
-
|
|
1949
|
-
/* @3x implies we should also emit a 2x entry, even if @2x was not explicitly set. */
|
|
1950
2199
|
bool want_2x = have_2x || have_3x;
|
|
1951
2200
|
bool want_3x = have_3x;
|
|
1952
2201
|
|
|
2202
|
+
/* Auto: emit marker for html_renderer to discover formats from filesystem */
|
|
2203
|
+
if (have_auto && url) {
|
|
2204
|
+
char *base = attributes_to_html(attrs);
|
|
2205
|
+
size_t base_len = base && *base ? strlen(base) : 0;
|
|
2206
|
+
size_t need = base_len + 64;
|
|
2207
|
+
char *result = malloc(need);
|
|
2208
|
+
if (result) {
|
|
2209
|
+
char *p = result;
|
|
2210
|
+
if (base_len > 0) {
|
|
2211
|
+
memcpy(p, base, base_len + 1);
|
|
2212
|
+
p += base_len;
|
|
2213
|
+
}
|
|
2214
|
+
p += sprintf(p, " data-apex-replace-auto=1");
|
|
2215
|
+
free(base);
|
|
2216
|
+
return result;
|
|
2217
|
+
}
|
|
2218
|
+
free(base);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/* Video URL: emit replacement markers for renderer to output <video> */
|
|
2222
|
+
if (url && is_video_url(url)) {
|
|
2223
|
+
char *base = attributes_to_html(attrs);
|
|
2224
|
+
size_t base_len = base && *base ? strlen(base) : 0;
|
|
2225
|
+
size_t need = base_len + 128;
|
|
2226
|
+
char *result = malloc(need);
|
|
2227
|
+
if (!result) {
|
|
2228
|
+
free(base);
|
|
2229
|
+
return strdup("");
|
|
2230
|
+
}
|
|
2231
|
+
char *p = result;
|
|
2232
|
+
if (base_len > 0) {
|
|
2233
|
+
memcpy(p, base, base_len + 1);
|
|
2234
|
+
p += base_len;
|
|
2235
|
+
}
|
|
2236
|
+
p += sprintf(p, " data-apex-replace-video=1");
|
|
2237
|
+
if (attrs_have_video_format(attrs, "webm")) p += sprintf(p, " data-apex-video-webm=1");
|
|
2238
|
+
if (attrs_have_video_format(attrs, "ogg")) p += sprintf(p, " data-apex-video-ogg=1");
|
|
2239
|
+
if (attrs_have_video_format(attrs, "mp4")) p += sprintf(p, " data-apex-video-mp4=1");
|
|
2240
|
+
if (attrs_have_video_format(attrs, "mov")) p += sprintf(p, " data-apex-video-mov=1");
|
|
2241
|
+
if (attrs_have_video_format(attrs, "m4v")) p += sprintf(p, " data-apex-video-m4v=1");
|
|
2242
|
+
free(base);
|
|
2243
|
+
return result;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
/* Picture (webp/avif): emit data-apex-picture-* for renderer to wrap in <picture> */
|
|
2247
|
+
bool have_webp = attrs_have_srcset_webp(attrs);
|
|
2248
|
+
bool have_avif = attrs_have_srcset_avif(attrs);
|
|
2249
|
+
if ((have_webp || have_avif) && url) {
|
|
2250
|
+
char *webp_srcset = have_webp ? build_picture_srcset(url, "webp", want_2x, want_3x) : NULL;
|
|
2251
|
+
char *avif_srcset = have_avif ? build_picture_srcset(url, "avif", want_2x, want_3x) : NULL;
|
|
2252
|
+
|
|
2253
|
+
char *base = attributes_to_html(attrs);
|
|
2254
|
+
size_t need = (base ? strlen(base) : 0) + 64;
|
|
2255
|
+
if (webp_srcset) need += strlen(webp_srcset) + 32;
|
|
2256
|
+
if (avif_srcset) need += strlen(avif_srcset) + 32;
|
|
2257
|
+
|
|
2258
|
+
char *result = malloc(need);
|
|
2259
|
+
if (result) {
|
|
2260
|
+
char *p = result;
|
|
2261
|
+
if (base && *base) p += sprintf(p, "%s", base);
|
|
2262
|
+
if (webp_srcset) {
|
|
2263
|
+
p += sprintf(p, " data-apex-replace-picture=1 data-apex-picture-webp=\"%s\"", webp_srcset);
|
|
2264
|
+
}
|
|
2265
|
+
if (avif_srcset) {
|
|
2266
|
+
p += sprintf(p, " data-apex-picture-avif=\"%s\"", avif_srcset);
|
|
2267
|
+
}
|
|
2268
|
+
if (!webp_srcset && avif_srcset) {
|
|
2269
|
+
p += sprintf(p, " data-apex-replace-picture=1");
|
|
2270
|
+
}
|
|
2271
|
+
free(webp_srcset);
|
|
2272
|
+
free(avif_srcset);
|
|
2273
|
+
}
|
|
2274
|
+
free(base);
|
|
2275
|
+
if (result) return result;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
1953
2278
|
char *url_2x = (want_2x && url) ? url_with_2x_suffix(url) : NULL;
|
|
1954
2279
|
char *url_3x = (want_3x && url) ? url_with_3x_suffix(url) : NULL;
|
|
1955
2280
|
|
|
@@ -2361,12 +2686,7 @@ char *apex_preprocess_image_attributes(const char *text, image_attr_entry **img_
|
|
|
2361
2686
|
while (after_space < paren_end && (*after_space == ' ' || *after_space == '\t')) after_space++;
|
|
2362
2687
|
|
|
2363
2688
|
if (after_space < paren_end) {
|
|
2364
|
-
/*
|
|
2365
|
-
if (looks_like_attr_key_equals(after_space, paren_end)) {
|
|
2366
|
-
attr_start = after_space;
|
|
2367
|
-
url_end = p;
|
|
2368
|
-
break;
|
|
2369
|
-
}
|
|
2689
|
+
/* @2x/@3x: always split so we don't encode into URL */
|
|
2370
2690
|
if ((size_t)(paren_end - after_space) >= 3 &&
|
|
2371
2691
|
after_space[0] == '@' &&
|
|
2372
2692
|
((after_space[1] == '2' && after_space[2] == 'x') ||
|
|
@@ -2391,6 +2711,25 @@ char *apex_preprocess_image_attributes(const char *text, image_attr_entry **img_
|
|
|
2391
2711
|
break;
|
|
2392
2712
|
}
|
|
2393
2713
|
|
|
2714
|
+
/* For quoted titles only (no other attributes),
|
|
2715
|
+
* let cmark handle the title so it appears on the
|
|
2716
|
+
* img tag for caption logic and tooltips.
|
|
2717
|
+
* Must check BEFORE looks_like_attr_key_equals,
|
|
2718
|
+
* which returns true for '"' and would treat
|
|
2719
|
+
* the title as attributes.
|
|
2720
|
+
*/
|
|
2721
|
+
if (*after_space == '"' || *after_space == '\'') {
|
|
2722
|
+
char qc = *after_space;
|
|
2723
|
+
const char *tail = after_space + 1;
|
|
2724
|
+
while (tail < paren_end && *tail != qc) tail++;
|
|
2725
|
+
if (tail < paren_end) tail++; /* skip closing quote */
|
|
2726
|
+
while (tail < paren_end && (*tail == ' ' || *tail == '\t')) tail++;
|
|
2727
|
+
if (tail == paren_end) {
|
|
2728
|
+
url_end = p; /* Let cmark handle the title */
|
|
2729
|
+
break;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2394
2733
|
/* For everything else, treat the tail as an
|
|
2395
2734
|
* attribute string (including quoted title).
|
|
2396
2735
|
*/
|
|
@@ -2511,19 +2850,26 @@ char *apex_preprocess_image_attributes(const char *text, image_attr_entry **img_
|
|
|
2511
2850
|
}
|
|
2512
2851
|
}
|
|
2513
2852
|
|
|
2853
|
+
/* URL ending in .* means auto-discover formats (same as auto attribute) */
|
|
2854
|
+
bool url_is_wildcard = (url_len >= 2 && url[url_len - 2] == '.' && url[url_len - 1] == '*');
|
|
2855
|
+
if (url_is_wildcard && do_image_attrs) {
|
|
2856
|
+
if (!attrs) attrs = create_attributes();
|
|
2857
|
+
if (attrs) add_attribute(attrs, "data-apex-auto", "1");
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2514
2860
|
/* URL encode the URL only when enabled and URL has no known protocol (http/https/file/x-marked) */
|
|
2515
2861
|
bool skip_encode = has_protocol(url);
|
|
2516
2862
|
char *encoded_url = (do_url_encoding && !skip_encode) ? url_encode(url) : strdup(url);
|
|
2517
2863
|
if (encoded_url) {
|
|
2518
|
-
/* Store attributes with URL - create entry
|
|
2864
|
+
/* Store attributes with URL - create entry when we have attrs, or when URL is a video (needs replacement), or when URL is wildcard (.*) */
|
|
2519
2865
|
image_attr_entry *entry = NULL;
|
|
2520
|
-
if (attrs) {
|
|
2866
|
+
if (attrs || is_video_url(url) || url_is_wildcard) {
|
|
2521
2867
|
/* Use the running image_index so attributes are
|
|
2522
2868
|
* bound to the correct inline image position,
|
|
2523
2869
|
* even when some images have no attributes.
|
|
2524
2870
|
*/
|
|
2525
2871
|
entry = create_image_attr_entry(&local_img_attrs, encoded_url, image_index);
|
|
2526
|
-
if (entry) {
|
|
2872
|
+
if (entry && attrs) {
|
|
2527
2873
|
/* Copy attributes (don't merge) */
|
|
2528
2874
|
for (int i = 0; i < attrs->attr_count; i++) {
|
|
2529
2875
|
add_attribute(entry->attrs, attrs->keys[i], attrs->values[i]);
|
|
@@ -3681,31 +4027,28 @@ void apex_apply_image_attributes(cmark_node *document, image_attr_entry *img_att
|
|
|
3681
4027
|
cmark_iter *iter = cmark_iter_new(document);
|
|
3682
4028
|
cmark_event_type event;
|
|
3683
4029
|
|
|
3684
|
-
/*
|
|
3685
|
-
*
|
|
3686
|
-
*
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
* images are expanded/rewrapped, and cleanly distinguishes inline vs ref
|
|
3690
|
-
* attributes without letting one overwrite the other.
|
|
3691
|
-
*/
|
|
4030
|
+
/* Preprocessing assigns index 0, 1, 2... to inline images only (ref-style get -1).
|
|
4031
|
+
* Use inline_image_position to match so that same-URL images (e.g. webp vs avif)
|
|
4032
|
+
* get correct attrs, while ref-style images are matched by URL. */
|
|
4033
|
+
int inline_image_position = 0;
|
|
4034
|
+
|
|
3692
4035
|
while ((event = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
|
|
3693
4036
|
cmark_node *node = cmark_iter_get_node(iter);
|
|
3694
4037
|
if (event == CMARK_EVENT_ENTER && cmark_node_get_type(node) == CMARK_NODE_IMAGE) {
|
|
3695
4038
|
const char *url = cmark_node_get_url(node);
|
|
3696
4039
|
image_attr_entry *matching = NULL;
|
|
3697
4040
|
|
|
3698
|
-
/* First, try
|
|
4041
|
+
/* First, try inline entry with index == inline_image_position and URL match. */
|
|
3699
4042
|
for (image_attr_entry *e = img_attrs; e; e = e->next) {
|
|
3700
|
-
if (e->index
|
|
4043
|
+
if (e->index == inline_image_position && e->url && url && strcmp(e->url, url) == 0 && e->attrs) {
|
|
3701
4044
|
matching = e;
|
|
3702
|
-
|
|
3703
|
-
|
|
4045
|
+
e->index = -2; /* mark as used */
|
|
4046
|
+
inline_image_position++;
|
|
3704
4047
|
break;
|
|
3705
4048
|
}
|
|
3706
4049
|
}
|
|
3707
4050
|
|
|
3708
|
-
/* If no inline
|
|
4051
|
+
/* If no inline match, try reference-style entries (index == -1) by URL. */
|
|
3709
4052
|
if (!matching && url) {
|
|
3710
4053
|
for (image_attr_entry *e = img_attrs; e; e = e->next) {
|
|
3711
4054
|
if (e->index == -1 && e->url && strcmp(e->url, url) == 0 && e->attrs) {
|
|
@@ -3986,7 +4329,23 @@ char *apex_preprocess_bracketed_spans(const char *text) {
|
|
|
3986
4329
|
/* Build span tag with attributes */
|
|
3987
4330
|
char *attr_str = attributes_to_html(attrs);
|
|
3988
4331
|
if (attr_str) {
|
|
3989
|
-
/*
|
|
4332
|
+
/* Decide whether this span actually needs markdown=\"span\"
|
|
4333
|
+
* Only enable markdown-in-HTML for bracketed spans whose
|
|
4334
|
+
* inner text contains inline markdown syntax (emphasis,
|
|
4335
|
+
* links, code, etc.). This prevents simple spans like
|
|
4336
|
+
* [-]{.taskmarker} from being reparsed as block lists. */
|
|
4337
|
+
bool needs_markdown_span = false;
|
|
4338
|
+
for (size_t i = 0; i < text_len; i++) {
|
|
4339
|
+
char ch = bracket_text[i];
|
|
4340
|
+
if (ch == '*' || ch == '_' || ch == '`' ||
|
|
4341
|
+
ch == '[' || ch == '!' || ch == '#') {
|
|
4342
|
+
needs_markdown_span = true;
|
|
4343
|
+
break;
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
/* Calculate space needed.
|
|
4348
|
+
* Worst case assumes we include markdown=\"span\" plus attributes. */
|
|
3990
4349
|
size_t span_open_len = 20 + strlen(attr_str) + strlen(bracket_text) + 10; /* <span markdown="span" ...>text</span> */
|
|
3991
4350
|
if (remaining < span_open_len) {
|
|
3992
4351
|
size_t written = write - output;
|
|
@@ -4003,8 +4362,18 @@ char *apex_preprocess_bracketed_spans(const char *text) {
|
|
|
4003
4362
|
remaining = output_capacity - written;
|
|
4004
4363
|
}
|
|
4005
4364
|
|
|
4006
|
-
|
|
4007
|
-
|
|
4365
|
+
int written;
|
|
4366
|
+
if (needs_markdown_span) {
|
|
4367
|
+
/* Write <span markdown="span" ...> for spans that
|
|
4368
|
+
* genuinely need inline markdown processing. */
|
|
4369
|
+
written = snprintf(write, remaining, "<span markdown=\"span\"%s>", attr_str);
|
|
4370
|
+
} else {
|
|
4371
|
+
/* For simple text-only spans, omit markdown=\"span\"
|
|
4372
|
+
* so that content like a lone '-' is not reparsed
|
|
4373
|
+
* as a list item by the markdown-in-HTML pipeline. */
|
|
4374
|
+
written = snprintf(write, remaining, "<span%s>", attr_str);
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4008
4377
|
if (written > 0 && (size_t)written < remaining) {
|
|
4009
4378
|
write += written;
|
|
4010
4379
|
remaining -= written;
|