apex-ruby 1.0.6 → 1.0.7

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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/ext/apex_ext/apex_ext.c +6 -0
  3. data/ext/apex_ext/apex_src/AGENTS.md +41 -0
  4. data/ext/apex_ext/apex_src/CHANGELOG.md +412 -2
  5. data/ext/apex_ext/apex_src/CMakeLists.txt +41 -29
  6. data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
  7. data/ext/apex_ext/apex_src/Package.swift +9 -0
  8. data/ext/apex_ext/apex_src/README.md +31 -9
  9. data/ext/apex_ext/apex_src/ROADMAP.md +5 -0
  10. data/ext/apex_ext/apex_src/VERSION +1 -1
  11. data/ext/apex_ext/apex_src/cli/main.c +1125 -13
  12. data/ext/apex_ext/apex_src/docs/index.md +459 -0
  13. data/ext/apex_ext/apex_src/include/apex/apex.h +67 -5
  14. data/ext/apex_ext/apex_src/include/apex/ast_man.h +20 -0
  15. data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +39 -0
  16. data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +40 -0
  17. data/ext/apex_ext/apex_src/include/apex/module.modulemap +1 -1
  18. data/ext/apex_ext/apex_src/man/apex-config.5 +333 -258
  19. data/ext/apex_ext/apex_src/man/apex-config.5.md +3 -1
  20. data/ext/apex_ext/apex_src/man/apex-plugins.7 +401 -316
  21. data/ext/apex_ext/apex_src/man/apex.1 +663 -620
  22. data/ext/apex_ext/apex_src/man/apex.1.html +703 -0
  23. data/ext/apex_ext/apex_src/man/apex.1.md +160 -90
  24. data/ext/apex_ext/apex_src/objc/Apex.swift +6 -0
  25. data/ext/apex_ext/apex_src/objc/NSString+Apex.h +12 -0
  26. data/ext/apex_ext/apex_src/objc/NSString+Apex.m +9 -0
  27. data/ext/apex_ext/apex_src/pages/index.md +459 -0
  28. data/ext/apex_ext/apex_src/src/_README.md +4 -4
  29. data/ext/apex_ext/apex_src/src/apex.c +702 -44
  30. data/ext/apex_ext/apex_src/src/ast_json.c +1130 -0
  31. data/ext/apex_ext/apex_src/src/ast_json.h +46 -0
  32. data/ext/apex_ext/apex_src/src/ast_man.c +948 -0
  33. data/ext/apex_ext/apex_src/src/ast_markdown.c +409 -0
  34. data/ext/apex_ext/apex_src/src/ast_terminal.c +2516 -0
  35. data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +8 -5
  36. data/ext/apex_ext/apex_src/src/extensions/definition_list.c +491 -1514
  37. data/ext/apex_ext/apex_src/src/extensions/definition_list.h +8 -15
  38. data/ext/apex_ext/apex_src/src/extensions/emoji.c +207 -0
  39. data/ext/apex_ext/apex_src/src/extensions/emoji.h +14 -0
  40. data/ext/apex_ext/apex_src/src/extensions/header_ids.c +178 -71
  41. data/ext/apex_ext/apex_src/src/extensions/highlight.c +37 -5
  42. data/ext/apex_ext/apex_src/src/extensions/ial.c +416 -47
  43. data/ext/apex_ext/apex_src/src/extensions/includes.c +241 -10
  44. data/ext/apex_ext/apex_src/src/extensions/includes.h +1 -0
  45. data/ext/apex_ext/apex_src/src/extensions/metadata.c +166 -3
  46. data/ext/apex_ext/apex_src/src/extensions/metadata.h +7 -0
  47. data/ext/apex_ext/apex_src/src/extensions/sup_sub.c +34 -3
  48. data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +55 -10
  49. data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.h +7 -4
  50. data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +84 -52
  51. data/ext/apex_ext/apex_src/src/extensions/toc.c +133 -19
  52. data/ext/apex_ext/apex_src/src/filters_ast.c +194 -0
  53. data/ext/apex_ext/apex_src/src/filters_ast.h +36 -0
  54. data/ext/apex_ext/apex_src/src/html_renderer.c +1265 -35
  55. data/ext/apex_ext/apex_src/src/html_renderer.h +21 -0
  56. data/ext/apex_ext/apex_src/src/plugins_remote.c +40 -14
  57. data/ext/apex_ext/apex_src/tests/CMakeLists.txt +1 -0
  58. data/ext/apex_ext/apex_src/tests/README.md +11 -5
  59. data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +13 -2
  60. data/ext/apex_ext/apex_src/tests/fixtures/filters/filter_output_with_rawblock.json +1 -0
  61. data/ext/apex_ext/apex_src/tests/fixtures/filters/unwrap.md +7 -0
  62. data/ext/apex_ext/apex_src/tests/fixtures/images/auto-wildcard.md +8 -0
  63. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.avif +0 -0
  64. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.jpg +0 -0
  65. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.webp +0 -0
  66. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.avif +0 -0
  67. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.jpg +0 -0
  68. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.webp +0 -0
  69. data/ext/apex_ext/apex_src/tests/fixtures/images/media_formats_test.md +63 -0
  70. data/ext/apex_ext/apex_src/tests/fixtures/includes/data-semi.csv +3 -0
  71. data/ext/apex_ext/apex_src/tests/fixtures/includes/with space.txt +1 -0
  72. data/ext/apex_ext/apex_src/tests/fixtures/tables/inline_tables_test.md +4 -1
  73. data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +64 -0
  74. data/ext/apex_ext/apex_src/tests/terminal_width_test.sh +29 -0
  75. data/ext/apex_ext/apex_src/tests/test-swift-package.sh +14 -0
  76. data/ext/apex_ext/apex_src/tests/test_cmark_callback.c +189 -0
  77. data/ext/apex_ext/apex_src/tests/test_extensions.c +374 -0
  78. data/ext/apex_ext/apex_src/tests/test_metadata.c +68 -0
  79. data/ext/apex_ext/apex_src/tests/test_output.c +291 -2
  80. data/ext/apex_ext/apex_src/tests/test_runner.c +10 -0
  81. data/ext/apex_ext/apex_src/tests/test_syntax_highlight.c +1 -1
  82. data/ext/apex_ext/apex_src/tests/test_tables.c +17 -1
  83. data/lib/apex/version.rb +1 -1
  84. metadata +32 -2
  85. 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
- /* Get the text node inside the heading */
1191
- cmark_node *text_node = cmark_node_first_child(heading);
1192
- if (!text_node || cmark_node_get_type(text_node) != CMARK_NODE_TEXT) return false;
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
- const char *text = cmark_node_get_literal(text_node);
1195
- if (!text) return false;
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
- /* Look for { at the end - support both {: and {# or {. formats */
1198
- const char *ial_start = strrchr(text, '{');
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
- /* Find closing } */
1204
- const char *close = strchr(ial_start, '}');
1205
- if (!close) return false;
1261
+ const char *brace = strrchr(text, '{');
1262
+ if (!brace) continue;
1206
1263
 
1207
- /* Check nothing after } except whitespace */
1208
- const char *after = close + 1;
1209
- while (*after && isspace((unsigned char)*after)) after++;
1210
- if (*after) return false;
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
- /* Heading was only IAL - leave empty string */
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
- /* Space + key= or bare @2x/@3x: always split so we don't encode into URL */
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 whenever we have attrs (from do_image_attrs or known-attribute split) */
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
- /* For each image node, we:
3685
- * 1. Prefer a matching inline entry (index >= 0), using each at most once.
3686
- * 2. If none, fall back to a reference-style entry (index == -1) matching by URL.
3687
- *
3688
- * This avoids relying on a separate image_index counter that can drift when
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 to find an unused inline entry for this URL (index >= 0). */
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 >= 0 && e->url && url && strcmp(e->url, url) == 0 && e->attrs) {
4043
+ if (e->index == inline_image_position && e->url && url && strcmp(e->url, url) == 0 && e->attrs) {
3701
4044
  matching = e;
3702
- /* Consume this inline entry so it is only applied once. */
3703
- e->index = -2; /* mark as used inline */
4045
+ e->index = -2; /* mark as used */
4046
+ inline_image_position++;
3704
4047
  break;
3705
4048
  }
3706
4049
  }
3707
4050
 
3708
- /* If no inline entry found, try reference-style entries (index == -1) by URL. */
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
- /* Calculate space needed */
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
- /* Write <span markdown="span" ...> */
4007
- int written = snprintf(write, remaining, "<span markdown=\"span\"%s>", attr_str);
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;