apex-ruby 1.0.17 → 1.0.20

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 (207) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -3
  3. data/ext/apex_ext/apex_ext.c +260 -91
  4. data/ext/apex_ext/apex_src/CHANGELOG.md +440 -0
  5. data/ext/apex_ext/apex_src/CMakeLists.txt +15 -1
  6. data/ext/apex_ext/apex_src/Formula/apex.rb +7 -2
  7. data/ext/apex_ext/apex_src/Package.swift +15 -4
  8. data/ext/apex_ext/apex_src/README.md +94 -35
  9. data/ext/apex_ext/apex_src/VERSION +1 -1
  10. data/ext/apex_ext/apex_src/apex-debug.log +4 -0
  11. data/ext/apex_ext/apex_src/apex.code-workspace +38 -0
  12. data/ext/apex_ext/apex_src/apex.taskpaper +12 -0
  13. data/ext/apex_ext/apex_src/apex_deflist_debug.log +25 -0
  14. data/ext/apex_ext/apex_src/buildnotes.md +1435 -0
  15. data/ext/apex_ext/apex_src/cli/main.c +262 -33
  16. data/ext/apex_ext/apex_src/debug.log +7 -0
  17. data/ext/apex_ext/apex_src/docs/CITATIONS_EXAMPLES.md +274 -0
  18. data/ext/apex_ext/apex_src/docs/CITATIONS_IMPLEMENTATION_PLAN.md +749 -0
  19. data/ext/apex_ext/apex_src/docs/CITATIONS_PLAN.md +744 -0
  20. data/ext/apex_ext/apex_src/docs/INDEX_IMPLEMENTATION_ANALYSIS.md +382 -0
  21. data/ext/apex_ext/apex_src/docs/plans/2025-02-27-man-page-output-implementation.md +351 -0
  22. data/ext/apex_ext/apex_src/docs/superpowers/plans/2026-07-12-toc-output-format.md +745 -0
  23. data/ext/apex_ext/apex_src/docs/superpowers/plans/2026-07-18-bear-image-attributes.md +829 -0
  24. data/ext/apex_ext/apex_src/docs/superpowers/plans/2026-07-19-apex-node-npm.md +1148 -0
  25. data/ext/apex_ext/apex_src/docs/superpowers/specs/2026-07-12-toc-output-format-design.md +161 -0
  26. data/ext/apex_ext/apex_src/docs/superpowers/specs/2026-07-18-bear-image-attributes-design.md +184 -0
  27. data/ext/apex_ext/apex_src/docs/superpowers/specs/2026-07-19-apex-node-npm-design.md +211 -0
  28. data/ext/apex_ext/apex_src/documentation/app-settings-table.md +67 -0
  29. data/ext/apex_ext/apex_src/documentation/app-transformed/Citations.md +340 -0
  30. data/ext/apex_ext/apex_src/documentation/app-transformed/Credits.md +11 -0
  31. data/ext/apex_ext/apex_src/documentation/app-transformed/Header-IDs.md +234 -0
  32. data/ext/apex_ext/apex_src/documentation/app-transformed/Inline-Attribute-Lists.md +516 -0
  33. data/ext/apex_ext/apex_src/documentation/app-transformed/Metadata-Transforms.md +461 -0
  34. data/ext/apex_ext/apex_src/documentation/app-transformed/Modes.md +193 -0
  35. data/ext/apex_ext/apex_src/documentation/app-transformed/Multi-File-Documents.md +201 -0
  36. data/ext/apex_ext/apex_src/documentation/app-transformed/Plugins.md +678 -0
  37. data/ext/apex_ext/apex_src/documentation/app-transformed/Syntax.md +1367 -0
  38. data/ext/apex_ext/apex_src/documentation/docsets/Apex Command Line Options.cheatsheet.txt +6 -0
  39. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/C-API.html +774 -300
  40. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Callouts.html +1194 -0
  41. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Citations.html +416 -216
  42. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Command-Line-Options.html +3544 -1094
  43. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Configuration.html +371 -97
  44. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Credits.html +153 -23
  45. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Examples.html +358 -124
  46. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Filters.html +1656 -0
  47. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Generating-Man-Pages.html +1226 -0
  48. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Getting-Started.html +201 -56
  49. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Header-IDs.html +279 -109
  50. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Home.html +200 -53
  51. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Indices.html +2327 -0
  52. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Inline-Attribute-Lists.html +690 -271
  53. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Installation.html +201 -55
  54. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Limitations-and-Roadmap.html +238 -68
  55. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Metadata-Transforms.html +421 -221
  56. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Modes.html +340 -78
  57. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Multi-File-Documents.html +251 -86
  58. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Multi-Format-Images.html +1362 -0
  59. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Pandoc-Integration.html +277 -90
  60. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Plugins.html +673 -346
  61. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Quarto-Mode.html +1405 -0
  62. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Rendering-Markdown-In-Terminal.html +1902 -0
  63. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Syntax.html +1939 -1698
  64. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Tables.html +1781 -0
  65. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Troubleshooting.html +315 -140
  66. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Usage.html +307 -113
  67. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Using-Apex-with-Jekyll.html +1132 -0
  68. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Writing-Tests.html +1754 -0
  69. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Xcode-Integration.html +872 -376
  70. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/docSet.dsidx +0 -0
  71. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/optimizedIndex.dsidx +0 -0
  72. data/ext/apex_ext/apex_src/documentation/html/apex-app-docs.html +8746 -0
  73. data/ext/apex_ext/apex_src/documentation/shared_styles.css +20 -15
  74. data/ext/apex_ext/apex_src/documentation/transform_for_app.md +55 -0
  75. data/ext/apex_ext/apex_src/escaping-repro.md +103 -0
  76. data/ext/apex_ext/apex_src/examples/quick-actions/Markdown to RTF.workflow/Contents/Info.plist +8 -0
  77. data/ext/apex_ext/apex_src/examples/quick-actions/Markdown to RTF.workflow/Contents/QuickLook/Preview.png +0 -0
  78. data/ext/apex_ext/apex_src/examples/quick-actions/Markdown to RTF.workflow/Contents/document.wflow +547 -0
  79. data/ext/apex_ext/apex_src/examples/quick-actions/Markdown-to-RTF.workflow.zip +0 -0
  80. data/ext/apex_ext/apex_src/examples/quick-actions/README.md +33 -0
  81. data/ext/apex_ext/apex_src/include/apex/apex.h +92 -6
  82. data/ext/apex_ext/apex_src/include/apex/ast_rtf.h +23 -0
  83. data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +6 -0
  84. data/ext/apex_ext/apex_src/include/apex/module.modulemap +5 -0
  85. data/ext/apex_ext/apex_src/include/apex/plugins.h +105 -0
  86. data/ext/apex_ext/apex_src/include/apexc.h +1 -0
  87. data/ext/apex_ext/apex_src/man/apex.1 +82 -78
  88. data/ext/apex_ext/apex_src/man/apex.1.md +81 -4
  89. data/ext/apex_ext/apex_src/objc/Apex.swift +73 -0
  90. data/ext/apex_ext/apex_src/objc/ApexPlugins.swift +79 -0
  91. data/ext/apex_ext/apex_src/objc/NSString+Apex.h +83 -5
  92. data/ext/apex_ext/apex_src/objc/NSString+Apex.m +244 -1
  93. data/ext/apex_ext/apex_src/objc/NSString+ApexPlugins.h +76 -0
  94. data/ext/apex_ext/apex_src/objc/NSString+ApexPlugins.m +169 -0
  95. data/ext/apex_ext/apex_src/output.html +87 -0
  96. data/ext/apex_ext/apex_src/plans/quarto-mode.md +442 -0
  97. data/ext/apex_ext/apex_src/src/_README.md +85 -32
  98. data/ext/apex_ext/apex_src/src/apex.c +770 -200
  99. data/ext/apex_ext/apex_src/src/ast_rtf.c +848 -0
  100. data/ext/apex_ext/apex_src/src/ast_terminal.c +184 -7
  101. data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +10 -3
  102. data/ext/apex_ext/apex_src/src/extensions/advanced_tables.c +7 -11
  103. data/ext/apex_ext/apex_src/src/extensions/bear_image_attrs.c +467 -0
  104. data/ext/apex_ext/apex_src/src/extensions/bear_image_attrs.h +27 -0
  105. data/ext/apex_ext/apex_src/src/extensions/callouts.c +400 -4
  106. data/ext/apex_ext/apex_src/src/extensions/callouts.h +15 -5
  107. data/ext/apex_ext/apex_src/src/extensions/citations.c +26 -5
  108. data/ext/apex_ext/apex_src/src/extensions/code_fence_attrs.c +632 -0
  109. data/ext/apex_ext/apex_src/src/extensions/code_fence_attrs.h +22 -0
  110. data/ext/apex_ext/apex_src/src/extensions/concordance.c +632 -0
  111. data/ext/apex_ext/apex_src/src/extensions/definition_list.c +129 -15
  112. data/ext/apex_ext/apex_src/src/extensions/grid_tables.c +1050 -121
  113. data/ext/apex_ext/apex_src/src/extensions/html_markdown.c +569 -16
  114. data/ext/apex_ext/apex_src/src/extensions/ial.c +502 -52
  115. data/ext/apex_ext/apex_src/src/extensions/ial.h +2 -1
  116. data/ext/apex_ext/apex_src/src/extensions/includes.c +532 -33
  117. data/ext/apex_ext/apex_src/src/extensions/includes.h +7 -1
  118. data/ext/apex_ext/apex_src/src/extensions/index.c +1427 -140
  119. data/ext/apex_ext/apex_src/src/extensions/index.h +28 -0
  120. data/ext/apex_ext/apex_src/src/extensions/metadata.c +200 -161
  121. data/ext/apex_ext/apex_src/src/extensions/quarto_diagrams.c +251 -0
  122. data/ext/apex_ext/apex_src/src/extensions/quarto_diagrams.h +22 -0
  123. data/ext/apex_ext/apex_src/src/extensions/quarto_lists.c +624 -0
  124. data/ext/apex_ext/apex_src/src/extensions/quarto_lists.h +35 -0
  125. data/ext/apex_ext/apex_src/src/extensions/quarto_polish.c +264 -0
  126. data/ext/apex_ext/apex_src/src/extensions/quarto_polish.h +23 -0
  127. data/ext/apex_ext/apex_src/src/extensions/quarto_shortcodes.c +307 -0
  128. data/ext/apex_ext/apex_src/src/extensions/quarto_shortcodes.h +21 -0
  129. data/ext/apex_ext/apex_src/src/extensions/raw_content.c +351 -0
  130. data/ext/apex_ext/apex_src/src/extensions/raw_content.h +20 -0
  131. data/ext/apex_ext/apex_src/src/extensions/relaxed_tables.c +117 -3
  132. data/ext/apex_ext/apex_src/src/extensions/special_markers.c +53 -0
  133. data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +23 -1
  134. data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +83 -92
  135. data/ext/apex_ext/apex_src/src/extensions/toc.c +446 -25
  136. data/ext/apex_ext/apex_src/src/extensions/toc.h +26 -1
  137. data/ext/apex_ext/apex_src/src/extensions/wiki_links.c +28 -0
  138. data/ext/apex_ext/apex_src/src/html_renderer.c +387 -17
  139. data/ext/apex_ext/apex_src/src/plugin_catalog.c +517 -0
  140. data/ext/apex_ext/apex_src/src/plugins_remote.c +88 -14
  141. data/ext/apex_ext/apex_src/src/plugins_remote.h +34 -0
  142. data/ext/apex_ext/apex_src/src/pretty_html.c +6 -1
  143. data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +21 -0
  144. data/ext/apex_ext/apex_src/tests/fixtures/images/bear_image_attributes.md +10 -0
  145. data/ext/apex_ext/apex_src/tests/fixtures/includes/sections.md +19 -0
  146. data/ext/apex_ext/apex_src/tests/fixtures/includes/sections.txt +11 -0
  147. data/ext/apex_ext/apex_src/tests/fixtures/minimal_pb.md +2 -0
  148. data/ext/apex_ext/apex_src/tests/fixtures/obsidian-callouts.md +46 -0
  149. data/ext/apex_ext/apex_src/tests/fixtures/pagebreak_html_repro.md +5 -0
  150. data/ext/apex_ext/apex_src/tests/fixtures/python-callouts.md +47 -0
  151. data/ext/apex_ext/apex_src/tests/fixtures/quarto/callouts.md +3 -0
  152. data/ext/apex_ext/apex_src/tests/fixtures/quarto/code-attrs.md +3 -0
  153. data/ext/apex_ext/apex_src/tests/fixtures/quarto/diagrams.md +8 -0
  154. data/ext/apex_ext/apex_src/tests/fixtures/quarto/lists.md +19 -0
  155. data/ext/apex_ext/apex_src/tests/fixtures/quarto/raw-content.md +5 -0
  156. data/ext/apex_ext/apex_src/tests/fixtures/quarto/shortcodes.md +7 -0
  157. data/ext/apex_ext/apex_src/tests/fixtures/quarto/smoke.md +81 -0
  158. data/ext/apex_ext/apex_src/tests/fixtures/quarto/spans.md +5 -0
  159. data/ext/apex_ext/apex_src/tests/fixtures/quarto-callouts.md +47 -0
  160. data/ext/apex_ext/apex_src/tests/fixtures/speed.md +192 -0
  161. data/ext/apex_ext/apex_src/tests/fixtures/tables/grid-tables.md +37 -0
  162. data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +16 -0
  163. data/ext/apex_ext/apex_src/tests/test_bear_image_attrs.c +301 -0
  164. data/ext/apex_ext/apex_src/tests/test_concordance.tsv +4 -0
  165. data/ext/apex_ext/apex_src/tests/test_escaping_repro.c +82 -0
  166. data/ext/apex_ext/apex_src/tests/test_extensions.c +821 -1
  167. data/ext/apex_ext/apex_src/tests/test_ial.c +7 -0
  168. data/ext/apex_ext/apex_src/tests/test_marked_integration.c +25 -0
  169. data/ext/apex_ext/apex_src/tests/test_metadata.c +129 -0
  170. data/ext/apex_ext/apex_src/tests/test_output.c +568 -2
  171. data/ext/apex_ext/apex_src/tests/test_runner.c +11 -0
  172. data/ext/apex_ext/apex_src/tests/test_tables.c +420 -0
  173. data/ext/apex_ext/apex_src/vendor/cmark-gfm/CMakeLists.txt +1 -1
  174. data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/autolink.c +77 -1
  175. data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/table.c +135 -5
  176. data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/blocks.c +56 -1
  177. data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/config.h +38 -0
  178. data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/main.c +1 -1
  179. data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/__pycache__/cmark.cpython-314.pyc +0 -0
  180. data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/__pycache__/normalize.cpython-314.pyc +0 -0
  181. data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/extensions.txt +69 -0
  182. data/ext/apex_ext/apex_src/xcode_build/CMakeScripts/ReRunCMake.make +74 -0
  183. data/ext/apex_ext/apex_src/xcode_build/CTestTestfile.cmake +22 -0
  184. data/ext/apex_ext/apex_src/xcode_build/Debug/Apex.framework/Versions/A/Apex +0 -0
  185. data/ext/apex_ext/apex_src/xcode_build/Debug/Apex.framework/Versions/A/Resources/Info.plist +44 -0
  186. data/ext/apex_ext/apex_src/xcode_build/Debug/Apex.framework/Versions/A/_CodeSignature/CodeResources +128 -0
  187. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/project.pbxproj +5118 -0
  188. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  189. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/project.xcworkspace/xcuserdata/ttscoff.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  190. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/project.xcworkspace/xcuserdata/ttscoff.xcuserdatad/WorkspaceSettings.xcsettings +8 -0
  191. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/xcuserdata/ttscoff.xcuserdatad/xcschemes/xcschememanagement.plist +99 -0
  192. data/ext/apex_ext/apex_src/xcode_build/cmake_install.cmake +452 -0
  193. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/CMakeScripts/ReRunCMake.make +52 -0
  194. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/cmake_install.cmake +55 -0
  195. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/cmark-gfm.xcodeproj/project.pbxproj +2455 -0
  196. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/cmark-gfm.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  197. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/extensions/cmake_install.cmake +171 -0
  198. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/man/cmake_install.cmake +55 -0
  199. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/cmake_install.cmake +220 -0
  200. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/cmark-gfm_export.h +43 -0
  201. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/cmark-gfm_version.h +7 -0
  202. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/config.h +76 -0
  203. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/libcmark-gfm.pc +10 -0
  204. data/ext/apex_ext/extconf.rb +35 -0
  205. data/lib/apex/version.rb +2 -2
  206. metadata +117 -2
  207. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/tempOptimizedIndex.dsidx +0 -0
@@ -275,6 +275,67 @@ static char *extract_data_apex_picture_srcset(const char *attrs, const char *for
275
275
  return out;
276
276
  }
277
277
 
278
+ /**
279
+ * Return attrs with internal data-apex-* and core img attrs removed.
280
+ * Keeps user-specified attrs (e.g. width/height/loading/class/style) for img fallback.
281
+ * Caller must free.
282
+ */
283
+ static char *filter_img_fallback_attrs(const char *attrs) {
284
+ if (!attrs) return NULL;
285
+
286
+ size_t cap = strlen(attrs) + 1;
287
+ char *out = malloc(cap);
288
+ if (!out) return NULL;
289
+
290
+ char *w = out;
291
+ const char *p = attrs;
292
+
293
+ while (*p) {
294
+ while (*p && isspace((unsigned char)*p)) p++;
295
+ if (!*p) break;
296
+
297
+ const char *token_start = p;
298
+ const char *name_start = p;
299
+ while (*p && !isspace((unsigned char)*p) && *p != '=') p++;
300
+ const char *name_end = p;
301
+
302
+ if (*p == '=') {
303
+ p++; /* consume '=' */
304
+ if (*p == '"' || *p == '\'') {
305
+ char q = *p++;
306
+ while (*p && *p != q) p++;
307
+ if (*p == q) p++;
308
+ } else {
309
+ while (*p && !isspace((unsigned char)*p)) p++;
310
+ }
311
+ }
312
+
313
+ const char *token_end = p;
314
+ size_t name_len = (size_t)(name_end - name_start);
315
+ bool skip_attr = false;
316
+
317
+ if (name_len == 0) continue;
318
+
319
+ if (name_len >= 10 && strncasecmp(name_start, "data-apex-", 10) == 0) {
320
+ skip_attr = true;
321
+ } else if ((name_len == 3 && strncasecmp(name_start, "src", 3) == 0) ||
322
+ (name_len == 3 && strncasecmp(name_start, "alt", 3) == 0) ||
323
+ (name_len == 5 && strncasecmp(name_start, "title", 5) == 0)) {
324
+ skip_attr = true;
325
+ }
326
+
327
+ if (!skip_attr) {
328
+ if (w != out) *w++ = ' ';
329
+ size_t token_len = (size_t)(token_end - token_start);
330
+ memcpy(w, token_start, token_len);
331
+ w += token_len;
332
+ }
333
+ }
334
+
335
+ *w = '\0';
336
+ return out;
337
+ }
338
+
278
339
  /* Counters for element indexing */
279
340
  typedef struct {
280
341
  int para_count;
@@ -335,16 +396,36 @@ static char *get_node_text_fingerprint(cmark_node *node) {
335
396
  }
336
397
  }
337
398
 
338
- /* For links, use the URL */
399
+ /* For links, use URL + link text (first text child) to disambiguate
400
+ * same-href links and avoid matching raw HTML <a> tags. */
339
401
  if (type == CMARK_NODE_LINK) {
340
402
  const char *url = cmark_node_get_url(node);
341
403
  if (url) {
342
- size_t len = strlen(url);
343
- if (len > 50) len = 50;
344
- char *fingerprint = malloc(len + 1);
404
+ size_t url_len = strlen(url);
405
+ if (url_len > 50) url_len = 50;
406
+ const char *text = NULL;
407
+ for (cmark_node *child = cmark_node_first_child(node); child;
408
+ child = cmark_node_next(child)) {
409
+ if (cmark_node_get_type(child) == CMARK_NODE_TEXT) {
410
+ text = cmark_node_get_literal(child);
411
+ if (text && *text) break;
412
+ text = NULL;
413
+ }
414
+ }
415
+ size_t text_len = text ? strlen(text) : 0;
416
+ if (text_len > 20) text_len = 20;
417
+ size_t total = url_len + (text_len ? 1 + text_len : 0);
418
+ if (total > 50) total = 50;
419
+ char *fingerprint = malloc(total + 1);
345
420
  if (fingerprint) {
346
- memcpy(fingerprint, url, len);
347
- fingerprint[len] = '\0';
421
+ memcpy(fingerprint, url, url_len);
422
+ size_t pos = url_len;
423
+ if (text_len && pos + 1 + text_len <= 50) {
424
+ fingerprint[pos++] = '|';
425
+ memcpy(fingerprint + pos, text, text_len);
426
+ pos += text_len;
427
+ }
428
+ fingerprint[pos] = '\0';
348
429
  return fingerprint;
349
430
  }
350
431
  }
@@ -661,6 +742,23 @@ char *apex_render_html_with_attributes(cmark_node *document, int options) {
661
742
  }
662
743
  }
663
744
  }
745
+ } else if (elem_type == CMARK_NODE_LINK && fp_idx < 49) {
746
+ /* Append visible text before any nested tag (skip image-only links). */
747
+ const char *content_start = tag_end;
748
+ if (*content_start == '>') content_start++;
749
+ if (*content_start && *content_start != '<') {
750
+ size_t text_len = 0;
751
+ while (content_start[text_len] &&
752
+ content_start[text_len] != '<' &&
753
+ text_len < 20) {
754
+ text_len++;
755
+ }
756
+ if (text_len > 0 && fp_idx + 1 + text_len <= 50) {
757
+ html_fingerprint[fp_idx++] = '|';
758
+ memcpy(html_fingerprint + fp_idx, content_start, text_len);
759
+ fp_idx += text_len;
760
+ }
761
+ }
664
762
  }
665
763
  html_fingerprint[fp_idx] = '\0';
666
764
  }
@@ -714,6 +812,35 @@ char *apex_render_html_with_attributes(cmark_node *document, int options) {
714
812
  break;
715
813
  }
716
814
  }
815
+ } else if (elem_type == CMARK_NODE_LINK) {
816
+ /* Links: match by href(+text) fingerprint among unused attrs.
817
+ * Raw HTML <a> tags inflate html link_count, so element_index is
818
+ * not reliable. Consume matching fingerprints in document order. */
819
+ if (fp_idx > 0) {
820
+ for (attr_node *a = attr_list; a; a = a->next, idx++) {
821
+ if (used[idx]) continue;
822
+ if (a->node_type != CMARK_NODE_LINK) continue;
823
+ if (!a->text_fingerprint) continue;
824
+ if (strncmp(a->text_fingerprint, html_fingerprint, 50) == 0) {
825
+ matching = a;
826
+ used[idx] = true;
827
+ break;
828
+ }
829
+ }
830
+ }
831
+ /* Fall back to index only when no fingerprint is available. */
832
+ if (!matching) {
833
+ idx = 0;
834
+ for (attr_node *a = attr_list; a; a = a->next, idx++) {
835
+ if (used[idx]) continue;
836
+ if (a->node_type != CMARK_NODE_LINK) continue;
837
+ if (!a->text_fingerprint && a->element_index == elem_idx) {
838
+ matching = a;
839
+ used[idx] = true;
840
+ break;
841
+ }
842
+ }
843
+ }
717
844
  } else {
718
845
  /* For other elements, use the existing matching logic */
719
846
  for (attr_node *a = attr_list; a; a = a->next, idx++) {
@@ -728,10 +855,8 @@ char *apex_render_html_with_attributes(cmark_node *document, int options) {
728
855
  /* Try fingerprint match first (works for both block and inline) */
729
856
  if (a->text_fingerprint && fp_idx > 0 &&
730
857
  strncmp(a->text_fingerprint, html_fingerprint, 50) == 0) {
731
- /* For inline elements, also check element_index to handle duplicates.
732
- * (Images with same src use sequential matching in the branch above.) */
733
- if (elem_type == CMARK_NODE_LINK || elem_type == CMARK_NODE_IMAGE ||
734
- elem_type == CMARK_NODE_STRONG || elem_type == CMARK_NODE_EMPH ||
858
+ /* For inline elements, also check element_index to handle duplicates. */
859
+ if (elem_type == CMARK_NODE_STRONG || elem_type == CMARK_NODE_EMPH ||
735
860
  elem_type == CMARK_NODE_CODE) {
736
861
  if (a->element_index == elem_idx) {
737
862
  matching = a;
@@ -896,9 +1021,12 @@ char *apex_render_html_with_attributes(cmark_node *document, int options) {
896
1021
  /* Build <picture> with <source> elements and <img> fallback */
897
1022
  char *webp_srcset = extract_data_apex_picture_srcset(matching->attrs, "webp");
898
1023
  char *avif_srcset = extract_data_apex_picture_srcset(matching->attrs, "avif");
1024
+ char *img_fallback_attrs = filter_img_fallback_attrs(matching->attrs);
899
1025
 
900
- /* Strip data-apex-* from attrs for the img */
901
- size_t cap = 512 + (src ? strlen(src) * 2 : 0) + (webp_srcset ? strlen(webp_srcset) : 0) + (avif_srcset ? strlen(avif_srcset) : 0);
1026
+ size_t cap = 512 + (src ? strlen(src) * 2 : 0) +
1027
+ (webp_srcset ? strlen(webp_srcset) : 0) +
1028
+ (avif_srcset ? strlen(avif_srcset) : 0) +
1029
+ (img_fallback_attrs ? strlen(img_fallback_attrs) : 0);
902
1030
  replacement = malloc(cap);
903
1031
  if (replacement) {
904
1032
  char *w = replacement;
@@ -906,15 +1034,25 @@ char *apex_render_html_with_attributes(cmark_node *document, int options) {
906
1034
  if (avif_srcset) w += snprintf(w, cap - (size_t)(w - replacement), "<source type=\"image/avif\" srcset=\"%s\">", avif_srcset);
907
1035
  if (webp_srcset) w += snprintf(w, cap - (size_t)(w - replacement), "<source type=\"image/webp\" srcset=\"%s\">", webp_srcset);
908
1036
  /* Preserve title on img for caption logic (apex_convert_image_captions) */
1037
+ const char *img_attrs = (img_fallback_attrs && *img_fallback_attrs) ? img_fallback_attrs : NULL;
909
1038
  if (title && *title) {
910
- w += snprintf(w, cap - (size_t)(w - replacement), "<img src=\"%s\" alt=\"%s\" title=\"%s\"></picture>", src, alt, title);
1039
+ if (img_attrs) {
1040
+ w += snprintf(w, cap - (size_t)(w - replacement), "<img src=\"%s\" alt=\"%s\" title=\"%s\" %s></picture>", src, alt, title, img_attrs);
1041
+ } else {
1042
+ w += snprintf(w, cap - (size_t)(w - replacement), "<img src=\"%s\" alt=\"%s\" title=\"%s\"></picture>", src, alt, title);
1043
+ }
911
1044
  } else {
912
- w += snprintf(w, cap - (size_t)(w - replacement), "<img src=\"%s\" alt=\"%s\"></picture>", src, alt);
1045
+ if (img_attrs) {
1046
+ w += snprintf(w, cap - (size_t)(w - replacement), "<img src=\"%s\" alt=\"%s\" %s></picture>", src, alt, img_attrs);
1047
+ } else {
1048
+ w += snprintf(w, cap - (size_t)(w - replacement), "<img src=\"%s\" alt=\"%s\"></picture>", src, alt);
1049
+ }
913
1050
  }
914
1051
  repl_len = (size_t)(w - replacement);
915
1052
  }
916
1053
  free(webp_srcset);
917
1054
  free(avif_srcset);
1055
+ free(img_fallback_attrs);
918
1056
  }
919
1057
 
920
1058
  if (replacement && repl_len > 0 && repl_len <= remaining) {
@@ -2764,6 +2902,164 @@ static void apex_trim_attr_value(const char *s, size_t len,
2764
2902
  *out_len = (size_t)(end - start);
2765
2903
  }
2766
2904
 
2905
+ /**
2906
+ * Rewrite img alt text and optionally strip fig-alt attribute.
2907
+ * alt_val/alt_len and new_alt/new_alt_len are the inner text (no quotes).
2908
+ * Caller must free.
2909
+ */
2910
+ static char *apex_img_rewrite_alt_and_strip(const char *tag_start, const char *tag_end,
2911
+ const char *alt_val, size_t alt_len,
2912
+ const char *new_alt, size_t new_alt_len,
2913
+ const char *strip_start, const char *strip_end) {
2914
+ if (!tag_start || !tag_end || tag_end < tag_start || !alt_val || alt_len == 0 ||
2915
+ !new_alt || new_alt_len == 0) {
2916
+ return NULL;
2917
+ }
2918
+
2919
+ const char *alt_value_end = alt_val + alt_len;
2920
+ if (alt_value_end > tag_end + 1) {
2921
+ return NULL;
2922
+ }
2923
+
2924
+ size_t head_len = (size_t)(alt_val - tag_start);
2925
+ const char *suffix_start;
2926
+ size_t suffix_len;
2927
+
2928
+ if (strip_start && strip_end && strip_end > strip_start &&
2929
+ strip_start >= alt_value_end && strip_end <= tag_end + 1) {
2930
+ /* fig-alt attribute removed: emit closing quote then skip to after it */
2931
+ suffix_start = strip_end;
2932
+ suffix_len = (size_t)(tag_end + 1 - suffix_start);
2933
+ size_t out_len = head_len + new_alt_len + 1 + suffix_len;
2934
+
2935
+ char *out = malloc(out_len + 1);
2936
+ if (!out) {
2937
+ return NULL;
2938
+ }
2939
+
2940
+ char *w = out;
2941
+ memcpy(w, tag_start, head_len);
2942
+ w += head_len;
2943
+ memcpy(w, new_alt, new_alt_len);
2944
+ w += new_alt_len;
2945
+ *w++ = '"';
2946
+ memcpy(w, suffix_start, suffix_len);
2947
+ w[suffix_len] = '\0';
2948
+ return out;
2949
+ }
2950
+
2951
+ suffix_start = alt_value_end;
2952
+ suffix_len = (size_t)(tag_end + 1 - suffix_start);
2953
+ size_t out_len = head_len + new_alt_len + suffix_len;
2954
+
2955
+ char *out = malloc(out_len + 1);
2956
+ if (!out) {
2957
+ return NULL;
2958
+ }
2959
+
2960
+ char *w = out;
2961
+ memcpy(w, tag_start, head_len);
2962
+ w += head_len;
2963
+ memcpy(w, new_alt, new_alt_len);
2964
+ w += new_alt_len;
2965
+ memcpy(w, suffix_start, suffix_len);
2966
+ w[suffix_len] = '\0';
2967
+ return out;
2968
+ }
2969
+
2970
+ /**
2971
+ * Copy an <img> tag, remove fig-alt attribute, and set alt to the fig-alt value.
2972
+ * Caller must free the returned string.
2973
+ */
2974
+ static char *apex_img_tag_with_fig_alt(const char *tag_start, const char *tag_end,
2975
+ const char *fig_alt_val, size_t fig_alt_len,
2976
+ const char *strip_start, const char *strip_end,
2977
+ const char *alt_val, size_t alt_len) {
2978
+ if (!tag_start || !tag_end || tag_end < tag_start || !fig_alt_val || fig_alt_len == 0) {
2979
+ return NULL;
2980
+ }
2981
+
2982
+ if (alt_val && alt_len > 0) {
2983
+ return apex_img_rewrite_alt_and_strip(tag_start, tag_end, alt_val, alt_len,
2984
+ fig_alt_val, fig_alt_len,
2985
+ strip_start, strip_end);
2986
+ }
2987
+
2988
+ /* No existing alt: strip fig-alt and insert alt="..." before closing > */
2989
+ size_t tag_len = (size_t)(tag_end - tag_start + 1);
2990
+ size_t strip_len = 0;
2991
+ if (strip_start && strip_end && strip_end > strip_start &&
2992
+ strip_start >= tag_start && strip_end <= tag_end + 1) {
2993
+ strip_len = (size_t)(strip_end - strip_start);
2994
+ }
2995
+
2996
+ size_t cap = tag_len + fig_alt_len + 64;
2997
+ if (strip_len > 0 && strip_len < tag_len) {
2998
+ cap = tag_len - strip_len + fig_alt_len + 64;
2999
+ }
3000
+ char *buf = malloc(cap);
3001
+ if (!buf) {
3002
+ return NULL;
3003
+ }
3004
+
3005
+ char *p = buf;
3006
+ if (strip_len > 0) {
3007
+ size_t head = (size_t)(strip_start - tag_start);
3008
+ memcpy(p, tag_start, head);
3009
+ p += head;
3010
+ size_t tail = (size_t)(tag_end + 1 - strip_end);
3011
+ memcpy(p, strip_end, tail);
3012
+ p += tail;
3013
+ *p = '\0';
3014
+ } else {
3015
+ memcpy(buf, tag_start, tag_len);
3016
+ buf[tag_len] = '\0';
3017
+ p = buf + tag_len;
3018
+ }
3019
+
3020
+ char *gt = strrchr(buf, '>');
3021
+ if (!gt) {
3022
+ free(buf);
3023
+ return NULL;
3024
+ }
3025
+
3026
+ char fig_alt_buf[512];
3027
+ size_t use_len = fig_alt_len;
3028
+ if (use_len >= sizeof(fig_alt_buf)) {
3029
+ use_len = sizeof(fig_alt_buf) - 1;
3030
+ }
3031
+ memcpy(fig_alt_buf, fig_alt_val, use_len);
3032
+ fig_alt_buf[use_len] = '\0';
3033
+
3034
+ char insert[600];
3035
+ int n = snprintf(insert, sizeof(insert), " alt=\"%s\"", fig_alt_buf);
3036
+ if (n < 0 || (size_t)n >= sizeof(insert)) {
3037
+ free(buf);
3038
+ return NULL;
3039
+ }
3040
+
3041
+ size_t tail_len = strlen(gt);
3042
+ size_t new_len = (size_t)(gt - buf) + (size_t)n + tail_len;
3043
+ if (new_len + 1 > cap) {
3044
+ char *grown = realloc(buf, new_len + 32);
3045
+ if (!grown) {
3046
+ free(buf);
3047
+ return NULL;
3048
+ }
3049
+ buf = grown;
3050
+ gt = strrchr(buf, '>');
3051
+ if (!gt) {
3052
+ free(buf);
3053
+ return NULL;
3054
+ }
3055
+ tail_len = strlen(gt);
3056
+ }
3057
+
3058
+ memmove(gt + n, gt, tail_len + 1);
3059
+ memcpy(gt, insert, (size_t)n);
3060
+ return buf;
3061
+ }
3062
+
2767
3063
  /**
2768
3064
  * Convert <img> tags to <figure> with <figcaption> when alt/title/caption are present.
2769
3065
  * If caption="TEXT" is present, always wrap. Otherwise when enable_image_captions,
@@ -2948,6 +3244,10 @@ char *apex_convert_image_captions(const char *html, bool enable_image_captions,
2948
3244
  size_t caption_len = 0;
2949
3245
  const char *caption_attr_start = NULL; /* start of caption attr (for stripping) */
2950
3246
  const char *caption_attr_end = NULL;
3247
+ const char *fig_alt_val = NULL;
3248
+ size_t fig_alt_len = 0;
3249
+ const char *fig_alt_attr_start = NULL;
3250
+ const char *fig_alt_attr_end = NULL;
2951
3251
 
2952
3252
  const char *q = attr_start;
2953
3253
  while (q < attr_end) {
@@ -3001,6 +3301,12 @@ char *apex_convert_image_captions(const char *html, bool enable_image_captions,
3001
3301
  (strncasecmp(name_start, "title", 5) == 0)) {
3002
3302
  title_val = value_start;
3003
3303
  title_len = (size_t)(value_end - value_start);
3304
+ } else if (name_len == 7 &&
3305
+ (strncasecmp(name_start, "fig-alt", 7) == 0)) {
3306
+ fig_alt_val = value_start;
3307
+ fig_alt_len = (size_t)(value_end - value_start);
3308
+ fig_alt_attr_start = (name_start > attr_start && isspace((unsigned char)name_start[-1])) ? name_start - 1 : name_start;
3309
+ fig_alt_attr_end = q;
3004
3310
  } else if (name_len == 3 &&
3005
3311
  (strncasecmp(name_start, "alt", 3) == 0)) {
3006
3312
  alt_val = value_start;
@@ -3043,8 +3349,42 @@ char *apex_convert_image_captions(const char *html, bool enable_image_captions,
3043
3349
  }
3044
3350
  }
3045
3351
 
3352
+ /* Quarto/Pandoc: fig-alt is accessibility alt; markdown alt text is the caption */
3353
+ if (fig_alt_val && fig_alt_len > 0 && alt_val && alt_len > 0 && !use_caption_attr) {
3354
+ apex_trim_attr_value(alt_val, alt_len, &caption, &caption_text_len);
3355
+ }
3356
+
3357
+ char *fig_alt_tag = NULL;
3358
+ if (fig_alt_val && fig_alt_len > 0) {
3359
+ fig_alt_tag = apex_img_tag_with_fig_alt(tag_start, tag_end, fig_alt_val, fig_alt_len,
3360
+ fig_alt_attr_start, fig_alt_attr_end,
3361
+ alt_val, alt_len);
3362
+ }
3363
+
3046
3364
  if (!caption || caption_text_len == 0) {
3047
- /* No caption - copy tag as-is */
3365
+ /* No caption - copy tag as-is (apply fig-alt alt rewrite when present) */
3366
+ if (fig_alt_tag) {
3367
+ size_t tag_len = strlen(fig_alt_tag);
3368
+ if (tag_len >= remaining) {
3369
+ size_t used = write - output;
3370
+ size_t new_cap = (used + tag_len + 1) * 2;
3371
+ char *new_out = realloc(output, new_cap);
3372
+ if (!new_out) {
3373
+ free(fig_alt_tag);
3374
+ free(output);
3375
+ return NULL;
3376
+ }
3377
+ output = new_out;
3378
+ write = output + used;
3379
+ remaining = new_cap - used;
3380
+ }
3381
+ memcpy(write, fig_alt_tag, tag_len);
3382
+ write += tag_len;
3383
+ remaining -= tag_len;
3384
+ free(fig_alt_tag);
3385
+ read = tag_end + 1;
3386
+ continue;
3387
+ }
3048
3388
  size_t tag_len = (size_t)(tag_end - tag_start + 1);
3049
3389
  if (tag_len >= remaining) {
3050
3390
  size_t used = write - output;
@@ -3086,6 +3426,28 @@ char *apex_convert_image_captions(const char *html, bool enable_image_captions,
3086
3426
  }
3087
3427
  if (figure_depth > 0) {
3088
3428
  /* Already inside a figure - copy img tag as-is, no extra wrap */
3429
+ if (fig_alt_tag) {
3430
+ size_t tag_len = strlen(fig_alt_tag);
3431
+ if (tag_len >= remaining) {
3432
+ size_t used = write - output;
3433
+ size_t new_cap = (used + tag_len + 1) * 2;
3434
+ char *new_out = realloc(output, new_cap);
3435
+ if (!new_out) {
3436
+ free(fig_alt_tag);
3437
+ free(output);
3438
+ return NULL;
3439
+ }
3440
+ output = new_out;
3441
+ write = output + used;
3442
+ remaining = new_cap - used;
3443
+ }
3444
+ memcpy(write, fig_alt_tag, tag_len);
3445
+ write += tag_len;
3446
+ remaining -= tag_len;
3447
+ free(fig_alt_tag);
3448
+ read = tag_end + 1;
3449
+ continue;
3450
+ }
3089
3451
  size_t tag_len = (size_t)(tag_end - tag_start + 1);
3090
3452
  if (tag_len >= remaining) {
3091
3453
  size_t used = write - output;
@@ -3115,7 +3477,9 @@ char *apex_convert_image_captions(const char *html, bool enable_image_captions,
3115
3477
 
3116
3478
  /* When we have caption= attribute, output img tag without it (strip caption attr) */
3117
3479
  size_t img_tag_output_len;
3118
- if (caption_attr_start != NULL && caption_attr_end != NULL) {
3480
+ if (fig_alt_tag) {
3481
+ img_tag_output_len = strlen(fig_alt_tag);
3482
+ } else if (caption_attr_start != NULL && caption_attr_end != NULL) {
3119
3483
  img_tag_output_len = (size_t)(caption_attr_start - tag_start) +
3120
3484
  (size_t)(tag_end + 1 - caption_attr_end);
3121
3485
  } else {
@@ -3145,7 +3509,13 @@ char *apex_convert_image_captions(const char *html, bool enable_image_captions,
3145
3509
  remaining -= strlen(figure_open);
3146
3510
 
3147
3511
  /* Write <img ...> tag (omitting caption attribute if present) */
3148
- if (caption_attr_start != NULL && caption_attr_end != NULL) {
3512
+ if (fig_alt_tag) {
3513
+ memcpy(write, fig_alt_tag, img_tag_output_len);
3514
+ write += img_tag_output_len;
3515
+ remaining -= img_tag_output_len;
3516
+ free(fig_alt_tag);
3517
+ fig_alt_tag = NULL;
3518
+ } else if (caption_attr_start != NULL && caption_attr_end != NULL) {
3149
3519
  size_t part1 = (size_t)(caption_attr_start - tag_start);
3150
3520
  memcpy(write, tag_start, part1);
3151
3521
  write += part1;