apex-ruby 1.0.18 → 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
@@ -19,6 +19,12 @@
19
19
  #include "extensions/math.h"
20
20
  #include "extensions/critic.h"
21
21
  #include "extensions/callouts.h"
22
+ #include "extensions/raw_content.h"
23
+ #include "extensions/code_fence_attrs.h"
24
+ #include "extensions/quarto_diagrams.h"
25
+ #include "extensions/quarto_shortcodes.h"
26
+ #include "extensions/quarto_polish.h"
27
+ #include "extensions/quarto_lists.h"
22
28
  #include "extensions/includes.h"
23
29
  #include "extensions/toc.h"
24
30
  #include "extensions/abbreviations.h"
@@ -36,6 +42,7 @@
36
42
  #include "extensions/sup_sub.h"
37
43
  #include "extensions/header_ids.h"
38
44
  #include "extensions/relaxed_tables.h"
45
+ #include "extensions/grid_tables.h"
39
46
  #include "extensions/citations.h"
40
47
  #include "extensions/index.h"
41
48
  #include "extensions/fenced_divs.h"
@@ -45,6 +52,7 @@
45
52
  #include "apex/ast_markdown.h"
46
53
  #include "apex/ast_terminal.h"
47
54
  #include "apex/ast_man.h"
55
+ #include "apex/ast_rtf.h"
48
56
  #include "filters_ast.h"
49
57
 
50
58
  /* Custom renderer */
@@ -663,6 +671,49 @@ static char *apex_embed_images(const char *html, const apex_options *options, co
663
671
  return output;
664
672
  }
665
673
 
674
+ /**
675
+ * Track HTML tag / quoted-attribute state while scanning source text.
676
+ * State reflects characters already consumed, matching the previous
677
+ * "rescan from document start" semantics without the O(n^2) cost.
678
+ */
679
+ static void apex_autolink_html_consume(char c, bool *in_html_tag,
680
+ bool *html_quote_active,
681
+ char *html_quote_char) {
682
+ if (!*in_html_tag) {
683
+ if (c == '<') {
684
+ *in_html_tag = true;
685
+ *html_quote_active = false;
686
+ *html_quote_char = '\0';
687
+ }
688
+ return;
689
+ }
690
+
691
+ if (*html_quote_active) {
692
+ if (c == *html_quote_char) {
693
+ *html_quote_active = false;
694
+ *html_quote_char = '\0';
695
+ }
696
+ return;
697
+ }
698
+
699
+ if (c == '"' || c == '\'') {
700
+ *html_quote_active = true;
701
+ *html_quote_char = c;
702
+ } else if (c == '>') {
703
+ *in_html_tag = false;
704
+ }
705
+ }
706
+
707
+ static void apex_autolink_html_consume_range(const char *s, size_t n,
708
+ bool *in_html_tag,
709
+ bool *html_quote_active,
710
+ char *html_quote_char) {
711
+ for (size_t i = 0; i < n; i++) {
712
+ apex_autolink_html_consume(s[i], in_html_tag, html_quote_active,
713
+ html_quote_char);
714
+ }
715
+ }
716
+
666
717
  /**
667
718
  * Preprocess angle-bracket autolinks (<http://...>) into explicit links
668
719
  * and convert bare URLs/emails to explicit links so they survive
@@ -687,6 +738,9 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
687
738
  bool in_markdown_link_url = false; /* Track if we're inside [text](url) URL part */
688
739
  int bracket_count = 0; /* Count unmatched [ for markdown links */
689
740
  int paren_count = 0; /* Count unmatched ( inside markdown link URLs */
741
+ bool in_html_tag = false;
742
+ bool html_quote_active = false;
743
+ char html_quote_char = '\0';
690
744
 
691
745
  while (*r) {
692
746
  const char *loop_start = r;
@@ -694,16 +748,26 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
694
748
  /* Handle Liquid tags: copy {% ... %} without processing */
695
749
  if (!in_liquid && *r == '{' && r[1] == '%') {
696
750
  in_liquid = true;
751
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
752
+ &html_quote_char);
697
753
  *w++ = *r++;
754
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
755
+ &html_quote_char);
698
756
  *w++ = *r++;
699
757
  continue;
700
758
  }
701
759
  if (in_liquid) {
702
760
  if (*r == '%' && r[1] == '}') {
761
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
762
+ &html_quote_char);
703
763
  *w++ = *r++;
764
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
765
+ &html_quote_char);
704
766
  *w++ = *r++;
705
767
  in_liquid = false;
706
768
  } else {
769
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
770
+ &html_quote_char);
707
771
  *w++ = *r++;
708
772
  }
709
773
  continue;
@@ -733,6 +797,9 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
733
797
  }
734
798
  memcpy(w, r, line_len);
735
799
  w += line_len;
800
+ apex_autolink_html_consume_range(r, line_len, &in_html_tag,
801
+ &html_quote_active,
802
+ &html_quote_char);
736
803
  r = line_end;
737
804
  continue;
738
805
  }
@@ -762,6 +829,9 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
762
829
  }
763
830
  memcpy(w, r, line_len);
764
831
  w += line_len;
832
+ apex_autolink_html_consume_range(r, line_len, &in_html_tag,
833
+ &html_quote_active,
834
+ &html_quote_char);
765
835
  r = line_end;
766
836
  continue;
767
837
  }
@@ -788,17 +858,23 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
788
858
  }
789
859
  /* Copy the backticks */
790
860
  for (int i = 0; i < backtick_count; i++) {
861
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
862
+ &html_quote_char);
791
863
  *w++ = *r++;
792
864
  }
793
865
  continue;
794
866
  } else if (backtick_count == 1) {
795
867
  /* Inline code span - toggle state and copy the backtick */
796
868
  in_inline_code = !in_inline_code;
869
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
870
+ &html_quote_char);
797
871
  *w++ = *r++;
798
872
  continue;
799
873
  } else {
800
874
  /* Multiple backticks but less than 3 - copy them as-is (shouldn't happen in valid markdown) */
801
875
  for (int i = 0; i < backtick_count; i++) {
876
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
877
+ &html_quote_char);
802
878
  *w++ = *r++;
803
879
  }
804
880
  continue;
@@ -807,6 +883,17 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
807
883
 
808
884
  /* Skip processing inside code blocks or inline code */
809
885
  if (in_code_block || in_inline_code) {
886
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
887
+ &html_quote_char);
888
+ *w++ = *r++;
889
+ continue;
890
+ }
891
+
892
+ /* Skip any autolink processing while inside an HTML tag (<...>),
893
+ * including quoted attributes. */
894
+ if (in_html_tag) {
895
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
896
+ &html_quote_char);
810
897
  *w++ = *r++;
811
898
  continue;
812
899
  }
@@ -820,7 +907,11 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
820
907
  in_markdown_link_url = true;
821
908
  paren_count = 1; /* Count the opening '(' */
822
909
  /* Copy ']' and '(' */
910
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
911
+ &html_quote_char);
823
912
  *w++ = *r++; /* ']' */
913
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
914
+ &html_quote_char);
824
915
  *w++ = *r++; /* '(' */
825
916
  continue;
826
917
  }
@@ -837,95 +928,18 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
837
928
  }
838
929
  }
839
930
 
840
- /* Skip autolinking if we're inside a markdown link URL */
841
- if (in_markdown_link_url) {
931
+ /* Skip autolinking while inside markdown link text [ ... ] */
932
+ if (bracket_count > 0 && !in_markdown_link_url) {
933
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
934
+ &html_quote_char);
842
935
  *w++ = *r++;
843
936
  continue;
844
937
  }
845
938
 
846
- /* Check if we're inside an HTML tag attribute - if so, skip autolinking */
847
- /* Look backwards for < to see if we're inside a tag */
848
- bool in_html_attribute = false;
849
- if (*r == 'h' || *r == 'm') { /* Quick check: URLs start with http or mailto */
850
- const char *p = r - 1;
851
- const char *tag_start = NULL;
852
- const char *tag_end = NULL;
853
-
854
- /* Find nearest < or > before r */
855
- while (p >= text) {
856
- if (*p == '>') {
857
- tag_end = p;
858
- break;
859
- } else if (*p == '<') {
860
- tag_start = p;
861
- break;
862
- }
863
- p--;
864
- }
865
-
866
- /* If we're inside a tag (between < and >), check if we're in an attribute */
867
- if (tag_start && (!tag_end || tag_start > tag_end)) {
868
- /* Look backwards from r to find the nearest = sign within this tag */
869
- p = r - 1;
870
- const char *equals_pos = NULL;
871
- while (p > tag_start) {
872
- if (*p == '=') {
873
- equals_pos = p;
874
- break;
875
- } else if (*p == '>') {
876
- break;
877
- }
878
- p--;
879
- }
880
-
881
- if (equals_pos) {
882
- /* Check what comes after the = */
883
- const char *after_equals = equals_pos + 1;
884
- /* Skip whitespace */
885
- while (after_equals < r && isspace((unsigned char)*after_equals)) {
886
- after_equals++;
887
- }
888
-
889
- if (after_equals < r) {
890
- /* Check if it's a quoted attribute */
891
- if (*after_equals == '"' || *after_equals == '\'') {
892
- char quote = *after_equals;
893
- const char *value_start = after_equals + 1;
894
-
895
- /* If r is after the opening quote, check if we're inside */
896
- if (r >= value_start) {
897
- /* Look for the closing quote - scan forward from value_start */
898
- const char *quote_end = value_start;
899
- while (quote_end < r && *quote_end != quote && *quote_end != '\0') {
900
- quote_end++;
901
- }
902
-
903
- /* If we haven't found the closing quote by the time we reach r, we're inside */
904
- if (quote_end >= r || *quote_end != quote) {
905
- in_html_attribute = true;
906
- }
907
- }
908
- } else {
909
- /* Unquoted attribute - value is between = and next space or > */
910
- const char *value_start = after_equals;
911
- if (r > value_start) {
912
- const char *value_end = value_start;
913
- while (value_end < r && !isspace((unsigned char)*value_end) && *value_end != '>') {
914
- value_end++;
915
- }
916
- /* If r is in this unquoted value, we're inside an attribute */
917
- if (r <= value_end) {
918
- in_html_attribute = true;
919
- }
920
- }
921
- }
922
- }
923
- }
924
- }
925
- }
926
-
927
- if (in_html_attribute) {
928
- /* Inside HTML attribute, copy as-is */
939
+ /* Skip autolinking if we're inside a markdown link URL */
940
+ if (in_markdown_link_url) {
941
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
942
+ &html_quote_char);
929
943
  *w++ = *r++;
930
944
  continue;
931
945
  }
@@ -953,6 +967,7 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
953
967
  *w++ = ']'; *w++ = '(';
954
968
  memcpy(w, start, url_len); w += url_len;
955
969
  *w++ = ')';
970
+ /* Rewritten away from HTML; net tag state is unchanged. */
956
971
  r = end + 1;
957
972
  continue;
958
973
  }
@@ -975,6 +990,9 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
975
990
  }
976
991
  memcpy(w, r, comment_len);
977
992
  w += comment_len;
993
+ apex_autolink_html_consume_range(r, comment_len, &in_html_tag,
994
+ &html_quote_active,
995
+ &html_quote_char);
978
996
  r = comment_end + 3;
979
997
  continue;
980
998
  }
@@ -984,87 +1002,7 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
984
1002
  bool is_url_start = false;
985
1003
  bool is_email_start = false;
986
1004
 
987
- /* Check again if we're inside an HTML attribute before processing URLs */
988
- bool in_html_attribute_url = false;
989
- if (!isspace((unsigned char)*r) && (*r == 'h' || *r == 'm' || *r == '@')) {
990
- const char *p = r - 1;
991
- const char *tag_start = NULL;
992
- const char *tag_end = NULL;
993
-
994
- /* Find nearest < or > before r */
995
- while (p >= text) {
996
- if (*p == '>') {
997
- tag_end = p;
998
- break;
999
- } else if (*p == '<') {
1000
- tag_start = p;
1001
- break;
1002
- }
1003
- p--;
1004
- }
1005
-
1006
- /* If we're inside a tag (between < and >), check if we're in an attribute */
1007
- if (tag_start && (!tag_end || tag_start > tag_end)) {
1008
- /* Look backwards from r to find the nearest = sign within this tag */
1009
- p = r - 1;
1010
- const char *equals_pos = NULL;
1011
- while (p > tag_start) {
1012
- if (*p == '=') {
1013
- equals_pos = p;
1014
- break;
1015
- } else if (*p == '>') {
1016
- break;
1017
- }
1018
- p--;
1019
- }
1020
-
1021
- if (equals_pos) {
1022
- /* Check what comes after the = */
1023
- const char *after_equals = equals_pos + 1;
1024
- /* Skip whitespace */
1025
- while (after_equals < r && isspace((unsigned char)*after_equals)) {
1026
- after_equals++;
1027
- }
1028
-
1029
- if (after_equals < r) {
1030
- /* Check if it's a quoted attribute */
1031
- if (*after_equals == '"' || *after_equals == '\'') {
1032
- char quote = *after_equals;
1033
- const char *value_start = after_equals + 1;
1034
-
1035
- /* If r is after the opening quote, check if we're inside */
1036
- if (r >= value_start) {
1037
- /* Look for the closing quote - scan forward from value_start */
1038
- const char *quote_end = value_start;
1039
- while (quote_end < r && *quote_end != quote && *quote_end != '\0') {
1040
- quote_end++;
1041
- }
1042
-
1043
- /* If we haven't found the closing quote by the time we reach r, we're inside */
1044
- if (quote_end >= r || *quote_end != quote) {
1045
- in_html_attribute_url = true;
1046
- }
1047
- }
1048
- } else {
1049
- /* Unquoted attribute - value is between = and next space or > */
1050
- const char *value_start = after_equals;
1051
- if (r > value_start) {
1052
- const char *value_end = value_start;
1053
- while (value_end < r && !isspace((unsigned char)*value_end) && *value_end != '>') {
1054
- value_end++;
1055
- }
1056
- /* If r is in this unquoted value, we're inside an attribute */
1057
- if (r <= value_end) {
1058
- in_html_attribute_url = true;
1059
- }
1060
- }
1061
- }
1062
- }
1063
- }
1064
- }
1065
- }
1066
-
1067
- if (!isspace((unsigned char)*r) && !in_html_attribute_url) {
1005
+ if (!isspace((unsigned char)*r)) {
1068
1006
  /* Check for URL protocols */
1069
1007
  if (strncmp(r, "http://", 7) == 0 || strncmp(r, "https://", 8) == 0 || strncmp(r, "mailto:", 7) == 0) {
1070
1008
  is_url_start = true;
@@ -1090,6 +1028,30 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
1090
1028
  /* Require that the character immediately before @ is alphanumeric.
1091
1029
  * This prevents matching @ in URLs like https://example.com/@user */
1092
1030
  if (at_pos > r && isalnum((unsigned char)at_pos[-1])) {
1031
+ /* Ignore image-density suffixes like @2x.jpg, @3x.avif, etc. */
1032
+ const char *suffix = at_pos + 1;
1033
+ if (suffix < token_end && isdigit((unsigned char)*suffix)) {
1034
+ while (suffix < token_end && isdigit((unsigned char)*suffix)) {
1035
+ suffix++;
1036
+ }
1037
+ if (suffix < token_end && *suffix == 'x') {
1038
+ suffix++;
1039
+ if (suffix < token_end && *suffix == '.') {
1040
+ suffix++;
1041
+ if (suffix < token_end && isalnum((unsigned char)*suffix)) {
1042
+ while (suffix < token_end && isalnum((unsigned char)*suffix)) {
1043
+ suffix++;
1044
+ }
1045
+ if (suffix == token_end) {
1046
+ /* This token is an image filename density suffix,
1047
+ * not an email address. */
1048
+ goto skip_email_candidate;
1049
+ }
1050
+ }
1051
+ }
1052
+ }
1053
+ }
1054
+
1093
1055
  /* Validate that email has a TLD (at least one dot followed by alphanumeric) */
1094
1056
  const char *after_at = at_pos + 1;
1095
1057
  bool has_tld = false;
@@ -1113,6 +1075,7 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
1113
1075
  }
1114
1076
  }
1115
1077
  }
1078
+ skip_email_candidate:
1116
1079
 
1117
1080
  if (is_url_start || is_email_start) {
1118
1081
  const char *start = r;
@@ -1170,6 +1133,9 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
1170
1133
  *w++ = ']'; *w++ = '(';
1171
1134
  memcpy(w, href_text, href_len); w += href_len;
1172
1135
  *w++ = ')';
1136
+ apex_autolink_html_consume_range(start, (size_t)(end - start),
1137
+ &in_html_tag, &html_quote_active,
1138
+ &html_quote_char);
1173
1139
  r = end;
1174
1140
  free(mailto_buf);
1175
1141
  continue;
@@ -1177,6 +1143,8 @@ static char *apex_preprocess_autolinks(const char *text, const apex_options *opt
1177
1143
  free(mailto_buf);
1178
1144
  }
1179
1145
 
1146
+ apex_autolink_html_consume(*r, &in_html_tag, &html_quote_active,
1147
+ &html_quote_char);
1180
1148
  *w++ = *r++;
1181
1149
 
1182
1150
  /* Safety: ensure we always advance */
@@ -2207,9 +2175,13 @@ static char *apex_preprocess_table_colspans(const char *text) {
2207
2175
 
2208
2176
  /**
2209
2177
  * Preprocess alpha list markers (a., b., c. and A., B., C.)
2210
- * Converts them to numbered markers (1., 2., 3.) and adds markers for post-processing
2178
+ * Converts them to numbered markers (1., 2., 3.) and inserts an HTML comment (indented like
2179
+ * the list items) so the HTML pass can apply list-style-type; bracket tokens are not used
2180
+ * because the Markdown parser strips them before HTML is produced.
2211
2181
  */
2212
- static char *apex_preprocess_alpha_lists(const char *text) {
2182
+ static char *apex_preprocess_alpha_lists(const char *text,
2183
+ bool *inserted_synthetic_nested_alpha_break,
2184
+ bool *saw_explicit_nested_alpha_break) {
2213
2185
  if (!text) return NULL;
2214
2186
 
2215
2187
  size_t text_len = strlen(text);
@@ -2227,6 +2199,8 @@ static char *apex_preprocess_alpha_lists(const char *text) {
2227
2199
  bool is_upper = false;
2228
2200
  int item_number = 1;
2229
2201
  int blank_lines_since_alpha = 0; /* Track blank lines to detect list breaks */
2202
+ if (inserted_synthetic_nested_alpha_break) *inserted_synthetic_nested_alpha_break = false;
2203
+ if (saw_explicit_nested_alpha_break) *saw_explicit_nested_alpha_break = false;
2230
2204
 
2231
2205
  while (*read) {
2232
2206
  const char *line_start = read;
@@ -2261,6 +2235,14 @@ static char *apex_preprocess_alpha_lists(const char *text) {
2261
2235
  }
2262
2236
 
2263
2237
  if (is_alpha_marker) {
2238
+ bool nested_alpha_after_item = false;
2239
+ if (in_alpha_list && alpha_is_upper == is_upper && current_indent > alpha_list_indent) {
2240
+ if (alpha_is_upper) {
2241
+ nested_alpha_after_item = (alpha_char == expected_upper);
2242
+ } else {
2243
+ nested_alpha_after_item = (alpha_char == expected_lower);
2244
+ }
2245
+ }
2264
2246
  /* Check if this continues an existing alpha list */
2265
2247
  bool continues_list = false;
2266
2248
  if (in_alpha_list) {
@@ -2278,6 +2260,13 @@ static char *apex_preprocess_alpha_lists(const char *text) {
2278
2260
  }
2279
2261
 
2280
2262
  if (!continues_list) {
2263
+ if (nested_alpha_after_item) {
2264
+ if (blank_lines_since_alpha > 0) {
2265
+ if (saw_explicit_nested_alpha_break) *saw_explicit_nested_alpha_break = true;
2266
+ } else {
2267
+ if (inserted_synthetic_nested_alpha_break) *inserted_synthetic_nested_alpha_break = true;
2268
+ }
2269
+ }
2281
2270
  /* Start new alpha list */
2282
2271
  in_alpha_list = true;
2283
2272
  is_upper = alpha_is_upper;
@@ -2289,8 +2278,10 @@ static char *apex_preprocess_alpha_lists(const char *text) {
2289
2278
  } else {
2290
2279
  expected_lower = alpha_char;
2291
2280
  }
2292
- /* Add marker paragraph before the list (will be rendered as <p data-apex-alpha-list="lower|upper"></p>) */
2293
- int needed = snprintf(write, remaining, "[apex-alpha-list:%s]\n\n", alpha_is_upper ? "upper" : "lower");
2281
+ /* HTML comment + indent: bracket markers are stripped by the parser; comments survive in unsafe HTML
2282
+ * and must be indented like the following list items to stay inside a parent <li>. */
2283
+ int needed = snprintf(write, remaining, "%.*s<!-- apex-alpha-list-%s -->\n\n",
2284
+ (int)(p - line_start), line_start, alpha_is_upper ? "upper" : "lower");
2294
2285
  if (needed > 0 && needed < (int)remaining) {
2295
2286
  write += needed;
2296
2287
  remaining -= needed;
@@ -2407,7 +2398,9 @@ static char *apex_preprocess_alpha_lists(const char *text) {
2407
2398
  * Insert a blank line before indented ordered sublists that directly follow
2408
2399
  * a parent list item line, so they parse as nested <ol> blocks.
2409
2400
  */
2410
- static char *apex_preprocess_nested_ordered_sublists(const char *text) {
2401
+ static char *apex_preprocess_nested_ordered_sublists(const char *text,
2402
+ bool *inserted_synthetic_break,
2403
+ bool *saw_explicit_break) {
2411
2404
  if (!text) return NULL;
2412
2405
 
2413
2406
  size_t text_len = strlen(text);
@@ -2423,6 +2416,9 @@ static char *apex_preprocess_nested_ordered_sublists(const char *text) {
2423
2416
  bool prev_line_was_list_item = false;
2424
2417
  size_t prev_line_indent = 0;
2425
2418
 
2419
+ if (inserted_synthetic_break) *inserted_synthetic_break = false;
2420
+ if (saw_explicit_break) *saw_explicit_break = false;
2421
+
2426
2422
  while (*read) {
2427
2423
  const char *line_start = read;
2428
2424
  const char *line_end = strchr(read, '\n');
@@ -2454,8 +2450,14 @@ static char *apex_preprocess_nested_ordered_sublists(const char *text) {
2454
2450
  }
2455
2451
  }
2456
2452
 
2457
- if (!prev_line_was_blank && prev_line_was_list_item &&
2458
- current_line_ordered_marker && current_indent > prev_line_indent) {
2453
+ bool nested_ordered_after_item = (prev_line_was_list_item &&
2454
+ current_line_ordered_marker &&
2455
+ current_indent > prev_line_indent);
2456
+ if (nested_ordered_after_item && prev_line_was_blank) {
2457
+ if (saw_explicit_break) *saw_explicit_break = true;
2458
+ }
2459
+
2460
+ if (!prev_line_was_blank && nested_ordered_after_item) {
2459
2461
  if (remaining < 1) {
2460
2462
  size_t used = (size_t)(write - output);
2461
2463
  size_t new_capacity = output_capacity * 2 + 64;
@@ -2471,6 +2473,7 @@ static char *apex_preprocess_nested_ordered_sublists(const char *text) {
2471
2473
  }
2472
2474
  *write++ = '\n';
2473
2475
  remaining--;
2476
+ if (inserted_synthetic_break) *inserted_synthetic_break = true;
2474
2477
  prev_line_was_blank = true;
2475
2478
  }
2476
2479
 
@@ -2507,6 +2510,35 @@ static char *apex_preprocess_nested_ordered_sublists(const char *text) {
2507
2510
  return output;
2508
2511
  }
2509
2512
 
2513
+ /* Force tight rendering for lists synthesized from nested ordered sublist fixes. */
2514
+ static void apex_tighten_nested_ordered_list_items(cmark_node *node) {
2515
+ if (!node) return;
2516
+
2517
+ for (cmark_node *child = cmark_node_first_child(node); child; ) {
2518
+ cmark_node *next = cmark_node_next(child);
2519
+ apex_tighten_nested_ordered_list_items(child);
2520
+ child = next;
2521
+ }
2522
+
2523
+ if (cmark_node_get_type(node) != CMARK_NODE_LIST) return;
2524
+
2525
+ bool has_para_then_sublist_item = false;
2526
+ for (cmark_node *item = cmark_node_first_child(node); item; item = cmark_node_next(item)) {
2527
+ if (cmark_node_get_type(item) != CMARK_NODE_ITEM) continue;
2528
+ cmark_node *first = cmark_node_first_child(item);
2529
+ cmark_node *second = first ? cmark_node_next(first) : NULL;
2530
+ if (first && second &&
2531
+ cmark_node_get_type(first) == CMARK_NODE_PARAGRAPH &&
2532
+ cmark_node_get_type(second) == CMARK_NODE_LIST) {
2533
+ has_para_then_sublist_item = true;
2534
+ break;
2535
+ }
2536
+ }
2537
+ if (has_para_then_sublist_item) {
2538
+ cmark_node_set_list_tight(node, 1);
2539
+ }
2540
+ }
2541
+
2510
2542
  /**
2511
2543
  * Post-process HTML to add style attributes to alpha lists
2512
2544
  * Finds HTML comments like <!-- apex-alpha-list-lower --> or <!-- apex-alpha-list-upper -->
@@ -2516,9 +2548,10 @@ static char *apex_preprocess_nested_ordered_sublists(const char *text) {
2516
2548
  static char *apex_postprocess_alpha_lists_html(const char *html) {
2517
2549
  if (!html) return NULL;
2518
2550
 
2519
- /* Early exit: check if alpha list markers exist before processing */
2520
- if (strstr(html, "[apex-alpha-list:") == NULL) {
2521
- return NULL; /* No alpha list markers, skip processing */
2551
+ /* Early exit: bracket markers (legacy) or HTML comments from preprocessor */
2552
+ if (strstr(html, "[apex-alpha-list:") == NULL &&
2553
+ strstr(html, "<!-- apex-alpha-list-") == NULL) {
2554
+ return NULL;
2522
2555
  }
2523
2556
 
2524
2557
  size_t html_len = strlen(html);
@@ -2546,9 +2579,78 @@ static char *apex_postprocess_alpha_lists_html(const char *html) {
2546
2579
  } \
2547
2580
  } while(0)
2548
2581
 
2549
- /* Single-pass optimization: look for markers as we go */
2550
- const char *read_start = read; /* Track where we started reading from */
2582
+ /* Single-pass: HTML comments (current), <p>[apex-alpha-list:…]</p> (legacy), or stray brackets */
2583
+ const char *read_start = read;
2584
+ static const char alpha_comment_lower[] = "<!-- apex-alpha-list-lower -->";
2585
+ static const char alpha_comment_upper[] = "<!-- apex-alpha-list-upper -->";
2586
+ const size_t alpha_comment_lower_len = sizeof(alpha_comment_lower) - 1;
2587
+ const size_t alpha_comment_upper_len = sizeof(alpha_comment_upper) - 1;
2588
+
2551
2589
  while (*read) {
2590
+ /* Strip HTML comment markers and style the following <ol> (not copied to output). */
2591
+ bool comment_upper = false;
2592
+ size_t comment_len = 0;
2593
+ if ((size_t)(html + html_len - read) >= alpha_comment_lower_len &&
2594
+ strncmp(read, alpha_comment_lower, alpha_comment_lower_len) == 0) {
2595
+ comment_len = alpha_comment_lower_len;
2596
+ } else if ((size_t)(html + html_len - read) >= alpha_comment_upper_len &&
2597
+ strncmp(read, alpha_comment_upper, alpha_comment_upper_len) == 0) {
2598
+ comment_upper = true;
2599
+ comment_len = alpha_comment_upper_len;
2600
+ }
2601
+ if (comment_len > 0) {
2602
+ size_t copy_len = (size_t)(read - read_start);
2603
+ ENSURE_SPACE(copy_len);
2604
+ if (copy_len > 0) {
2605
+ memcpy(write, read_start, copy_len);
2606
+ write += copy_len;
2607
+ remaining -= copy_len;
2608
+ }
2609
+ read += comment_len;
2610
+ read_start = read;
2611
+ while (*read == ' ' || *read == '\t' || *read == '\n' || *read == '\r') {
2612
+ read++;
2613
+ }
2614
+ read_start = read;
2615
+ if (read[0] == '<' && read[1] == 'o' && read[2] == 'l' &&
2616
+ (read[3] == '>' || read[3] == ' ' || read[3] == '\t' || read[3] == '\n')) {
2617
+ const char *ol_start = read;
2618
+ const char *tag_end = strchr(ol_start, '>');
2619
+
2620
+ if (tag_end) {
2621
+ bool has_style = false;
2622
+ for (const char *p = ol_start; p < tag_end; p++) {
2623
+ if (strncmp(p, "style=", 6) == 0) {
2624
+ has_style = true;
2625
+ break;
2626
+ }
2627
+ }
2628
+ size_t tag_len = (size_t)(tag_end - ol_start); /* before '>' */
2629
+ ENSURE_SPACE(tag_len + 50);
2630
+ memcpy(write, ol_start, tag_len);
2631
+ write += tag_len;
2632
+ remaining -= tag_len;
2633
+ if (!has_style) {
2634
+ const char *style = comment_upper
2635
+ ? " style=\"list-style-type: upper-alpha\">"
2636
+ : " style=\"list-style-type: lower-alpha\">";
2637
+ size_t style_len = strlen(style);
2638
+ memcpy(write, style, style_len);
2639
+ write += style_len;
2640
+ remaining -= style_len;
2641
+ } else {
2642
+ ENSURE_SPACE(1);
2643
+ *write++ = '>';
2644
+ remaining--;
2645
+ }
2646
+ read = tag_end + 1;
2647
+ read_start = read;
2648
+ continue;
2649
+ }
2650
+ }
2651
+ continue;
2652
+ }
2653
+
2552
2654
  /* Drop any stray raw markers that did not form a standalone paragraph. */
2553
2655
  if (strncmp(read, "[apex-alpha-list:lower]", 23) == 0 ||
2554
2656
  strncmp(read, "[apex-alpha-list:upper]", 23) == 0) {
@@ -2823,9 +2925,22 @@ apex_options apex_options_default(void) {
2823
2925
  opts.enable_task_lists = true;
2824
2926
  opts.enable_attributes = true;
2825
2927
  opts.enable_callouts = true;
2928
+ opts.enable_py_callouts = false;
2929
+ opts.enable_quarto_callouts = false;
2930
+ opts.enable_quarto_extensions = false;
2931
+ opts.enable_quarto_raw = false;
2932
+ opts.enable_quarto_example_lists = false;
2933
+ opts.enable_quarto_line_blocks = false;
2934
+ opts.enable_quarto_roman_lists = false;
2935
+ opts.enable_quarto_code_attrs = false;
2936
+ opts.enable_quarto_diagrams = false;
2937
+ opts.enable_quarto_shortcodes = false;
2938
+ opts.enable_quarto_strict_lists = false;
2939
+ opts.enable_quarto_xrefs = false;
2826
2940
  opts.enable_marked_extensions = true;
2827
2941
  opts.enable_divs = true; /* Enabled by default in unified mode */
2828
2942
  opts.enable_spans = true; /* Enabled by default in unified mode */
2943
+ opts.enable_grid_tables = false; /* Disabled by default - enable with --grid-tables */
2829
2944
 
2830
2945
  /* Critic markup mode (0=accept, 1=reject, 2=markup) */
2831
2946
  opts.critic_mode = 2; /* Default: show markup */
@@ -2861,6 +2976,10 @@ apex_options apex_options_default(void) {
2861
2976
  opts.generate_header_ids = true;
2862
2977
  opts.header_anchors = false; /* Use header IDs by default, not anchor tags */
2863
2978
  opts.id_format = 0; /* GFM format (with dashes) */
2979
+ opts.toc_min = 1;
2980
+ opts.toc_max = 3;
2981
+ opts.toc_entries_out = NULL;
2982
+ opts.toc_entries_count_out = NULL;
2864
2983
 
2865
2984
  /* Table options */
2866
2985
  opts.relaxed_tables = true; /* Default: enabled in unified mode (can be disabled with --no-relaxed-tables) */
@@ -2903,6 +3022,7 @@ apex_options apex_options_default(void) {
2903
3022
  opts.enable_leanpub_index_syntax = false;
2904
3023
  opts.suppress_index = false;
2905
3024
  opts.group_index_by_letter = true;
3025
+ opts.concordance_files = NULL;
2906
3026
 
2907
3027
  /* Wiki link options */
2908
3028
  opts.wikilink_space = 0; /* Default: dash (0=dash, 1=none, 2=underscore, 3=space) */
@@ -2960,6 +3080,7 @@ apex_options apex_options_default(void) {
2960
3080
  opts.theme_name = NULL;
2961
3081
  opts.terminal_width = 0;
2962
3082
  opts.paginate = false;
3083
+ opts.paginate_symbols = false;
2963
3084
  opts.terminal_inline_images = true;
2964
3085
  opts.terminal_image_width = 50;
2965
3086
 
@@ -2986,6 +3107,8 @@ apex_options apex_options_for_mode(apex_mode_t mode) {
2986
3107
  opts.enable_task_lists = false;
2987
3108
  opts.enable_attributes = false;
2988
3109
  opts.enable_callouts = false;
3110
+ opts.enable_py_callouts = false;
3111
+ opts.enable_quarto_callouts = false;
2989
3112
  opts.enable_marked_extensions = false;
2990
3113
  opts.enable_divs = false;
2991
3114
  opts.enable_spans = false;
@@ -3021,6 +3144,8 @@ apex_options apex_options_for_mode(apex_mode_t mode) {
3021
3144
  opts.enable_wiki_links = false;
3022
3145
  opts.enable_attributes = false;
3023
3146
  opts.enable_callouts = false;
3147
+ opts.enable_py_callouts = false;
3148
+ opts.enable_quarto_callouts = false;
3024
3149
  opts.enable_marked_extensions = false;
3025
3150
  opts.enable_divs = false;
3026
3151
  opts.enable_spans = false;
@@ -3058,6 +3183,8 @@ apex_options apex_options_for_mode(apex_mode_t mode) {
3058
3183
  opts.enable_task_lists = false;
3059
3184
  opts.enable_attributes = false;
3060
3185
  opts.enable_callouts = false;
3186
+ opts.enable_py_callouts = false;
3187
+ opts.enable_quarto_callouts = false;
3061
3188
  opts.enable_marked_extensions = false;
3062
3189
  opts.enable_divs = false;
3063
3190
  opts.enable_spans = false;
@@ -3092,6 +3219,8 @@ apex_options apex_options_for_mode(apex_mode_t mode) {
3092
3219
  opts.enable_task_lists = false;
3093
3220
  opts.enable_attributes = true;
3094
3221
  opts.enable_callouts = false;
3222
+ opts.enable_py_callouts = false;
3223
+ opts.enable_quarto_callouts = false;
3095
3224
  /* Enable Marked-style extensions (including TOC) so that
3096
3225
  * Kramdown documents can use <!--TOC--> and {:toc} syntax
3097
3226
  * for table-of-contents generation. */
@@ -3133,16 +3262,142 @@ apex_options apex_options_for_mode(apex_mode_t mode) {
3133
3262
  opts.enable_mmark_index_syntax = true; /* Unified: mmark index syntax */
3134
3263
  opts.enable_textindex_syntax = true; /* Unified: TextIndex syntax enabled */
3135
3264
  opts.enable_leanpub_index_syntax = true; /* Unified: Leanpub index syntax enabled */
3265
+ opts.enable_py_callouts = false; /* Unified default: off unless explicitly enabled */
3266
+ opts.enable_quarto_callouts = false; /* Unified default: off unless explicitly enabled */
3136
3267
  opts.enable_divs = true; /* Unified: Pandoc fenced divs enabled */
3137
3268
  opts.enable_spans = true; /* Unified: bracketed spans enabled */
3138
3269
  opts.enable_emoji_autocorrect = true; /* Unified: emoji autocorrect enabled */
3139
3270
  opts.enable_image_captions = true; /* Unified: image captions enabled by default */
3140
3271
  break;
3272
+
3273
+ case APEX_MODE_QUARTO:
3274
+ /* Pandoc/Quarto markdown: unified-family defaults tuned for Quarto docs */
3275
+ opts = apex_options_for_mode(APEX_MODE_UNIFIED);
3276
+ opts.mode = APEX_MODE_QUARTO;
3277
+ opts.enable_quarto_extensions = true;
3278
+ opts.enable_quarto_raw = true;
3279
+ opts.enable_quarto_example_lists = true;
3280
+ opts.enable_quarto_line_blocks = true;
3281
+ opts.enable_quarto_roman_lists = true;
3282
+ opts.enable_quarto_code_attrs = true;
3283
+ opts.enable_quarto_diagrams = true;
3284
+ opts.enable_quarto_shortcodes = true;
3285
+ opts.enable_quarto_xrefs = true;
3286
+ opts.enable_quarto_strict_lists = false;
3287
+ opts.enable_quarto_callouts = true;
3288
+ opts.enable_wiki_links = false;
3289
+ opts.enable_marked_extensions = false;
3290
+ opts.enable_py_callouts = false;
3291
+ opts.enable_indices = false;
3292
+ opts.enable_mmark_index_syntax = false;
3293
+ opts.enable_textindex_syntax = false;
3294
+ opts.enable_leanpub_index_syntax = false;
3295
+ break;
3141
3296
  }
3142
3297
 
3143
3298
  return opts;
3144
3299
  }
3145
3300
 
3301
+ bool apex_options_apply_meta_file(apex_options *options, const char *path) {
3302
+ if (!options || !path || !path[0]) {
3303
+ return false;
3304
+ }
3305
+
3306
+ apex_options original_options = *options;
3307
+ apex_metadata_item *metadata = apex_load_metadata_from_file(path);
3308
+ if (!metadata) {
3309
+ return false;
3310
+ }
3311
+
3312
+ apex_apply_metadata_to_options(metadata, options);
3313
+
3314
+ bool owns_csl_file = options->csl_file &&
3315
+ options->csl_file != original_options.csl_file;
3316
+ bool owns_document_title = options->document_title &&
3317
+ options->document_title != original_options.document_title;
3318
+ bool owns_base_directory = options->base_directory &&
3319
+ options->base_directory != original_options.base_directory;
3320
+ bool owns_wikilink_extension = options->wikilink_extension &&
3321
+ options->wikilink_extension != original_options.wikilink_extension;
3322
+ bool owns_theme_name = options->theme_name &&
3323
+ options->theme_name != original_options.theme_name;
3324
+ bool owns_code_highlight_theme = options->code_highlight_theme &&
3325
+ options->code_highlight_theme != original_options.code_highlight_theme;
3326
+ bool owns_stylesheet_paths = options->stylesheet_paths &&
3327
+ options->stylesheet_paths != original_options.stylesheet_paths;
3328
+
3329
+ char *csl_file = owns_csl_file ? strdup(options->csl_file) : NULL;
3330
+ char *document_title = owns_document_title ? strdup(options->document_title) : NULL;
3331
+ char *base_directory = owns_base_directory ? strdup(options->base_directory) : NULL;
3332
+ char *wikilink_extension = owns_wikilink_extension ? strdup(options->wikilink_extension) : NULL;
3333
+ char *theme_name = owns_theme_name ? strdup(options->theme_name) : NULL;
3334
+ char *code_highlight_theme = owns_code_highlight_theme ? strdup(options->code_highlight_theme) : NULL;
3335
+ const char **stylesheet_paths = NULL;
3336
+ bool copy_failed = (owns_csl_file && !csl_file) ||
3337
+ (owns_document_title && !document_title) ||
3338
+ (owns_base_directory && !base_directory) ||
3339
+ (owns_wikilink_extension && !wikilink_extension) ||
3340
+ (owns_theme_name && !theme_name) ||
3341
+ (owns_code_highlight_theme && !code_highlight_theme);
3342
+
3343
+ if (owns_stylesheet_paths) {
3344
+ stylesheet_paths = calloc(options->stylesheet_count + 1, sizeof(*stylesheet_paths));
3345
+ if (!stylesheet_paths) {
3346
+ copy_failed = true;
3347
+ } else {
3348
+ for (size_t i = 0; i < options->stylesheet_count; i++) {
3349
+ if (options->stylesheet_paths[i]) {
3350
+ stylesheet_paths[i] = strdup(options->stylesheet_paths[i]);
3351
+ if (!stylesheet_paths[i]) {
3352
+ copy_failed = true;
3353
+ break;
3354
+ }
3355
+ }
3356
+ }
3357
+ }
3358
+ }
3359
+
3360
+ if (copy_failed) {
3361
+ free(csl_file);
3362
+ free(document_title);
3363
+ free(base_directory);
3364
+ free(wikilink_extension);
3365
+ free(theme_name);
3366
+ free(code_highlight_theme);
3367
+ if (stylesheet_paths) {
3368
+ for (size_t i = 0; i < options->stylesheet_count; i++) {
3369
+ free((void *)stylesheet_paths[i]);
3370
+ }
3371
+ free(stylesheet_paths);
3372
+ }
3373
+ if (owns_stylesheet_paths) {
3374
+ free(options->stylesheet_paths);
3375
+ }
3376
+ apex_free_metadata(metadata);
3377
+ *options = original_options;
3378
+ return false;
3379
+ }
3380
+
3381
+ if (owns_stylesheet_paths) {
3382
+ free(options->stylesheet_paths);
3383
+ }
3384
+ apex_free_metadata(metadata);
3385
+
3386
+ if (owns_csl_file) options->csl_file = csl_file;
3387
+ if (owns_document_title) options->document_title = document_title;
3388
+ if (owns_base_directory) options->base_directory = base_directory;
3389
+ if (owns_wikilink_extension) options->wikilink_extension = wikilink_extension;
3390
+ if (owns_theme_name) options->theme_name = theme_name;
3391
+ if (owns_code_highlight_theme) options->code_highlight_theme = code_highlight_theme;
3392
+ if (owns_stylesheet_paths) options->stylesheet_paths = stylesheet_paths;
3393
+ return true;
3394
+ }
3395
+
3396
+ static bool apex_quarto_feature(const apex_options *options, bool feature_enabled) {
3397
+ return feature_enabled &&
3398
+ (options->enable_quarto_extensions || options->mode == APEX_MODE_QUARTO);
3399
+ }
3400
+
3146
3401
  /**
3147
3402
  * Convert cmark-gfm option flags based on Apex options
3148
3403
  */
@@ -3221,7 +3476,7 @@ static void apex_register_extensions(cmark_parser *parser, const apex_options *o
3221
3476
  }
3222
3477
 
3223
3478
  /* GFM autolink - enable for GFM and Unified modes if autolink is enabled */
3224
- if (options->enable_autolink && (options->mode == APEX_MODE_GFM || options->mode == APEX_MODE_UNIFIED)) {
3479
+ if (options->enable_autolink && (options->mode == APEX_MODE_GFM || apex_mode_is_unified_family(options->mode))) {
3225
3480
  cmark_syntax_extension *autolink_ext = cmark_find_syntax_extension("autolink");
3226
3481
  if (autolink_ext) {
3227
3482
  cmark_parser_attach_syntax_extension(parser, autolink_ext);
@@ -4479,6 +4734,30 @@ fail:
4479
4734
  return NULL;
4480
4735
  }
4481
4736
 
4737
+ apex_toc_entry *apex_markdown_to_toc_entries(const char *markdown, size_t len,
4738
+ const apex_options *options,
4739
+ size_t *out_count) {
4740
+ if (out_count) *out_count = 0;
4741
+ if (!out_count) return NULL;
4742
+ if (!markdown || len == 0) return NULL;
4743
+
4744
+ apex_options opts = options ? *options : apex_options_default();
4745
+ opts.output_format = APEX_OUTPUT_TOC;
4746
+ opts.toc_entries_out = NULL; /* set below after locals exist */
4747
+ opts.toc_entries_count_out = NULL;
4748
+
4749
+ apex_toc_entry *entries = NULL;
4750
+ size_t count = 0;
4751
+ opts.toc_entries_out = &entries;
4752
+ opts.toc_entries_count_out = &count;
4753
+
4754
+ char *discard = apex_markdown_to_html(markdown, len, &opts);
4755
+ apex_free_string(discard);
4756
+
4757
+ *out_count = count;
4758
+ return entries;
4759
+ }
4760
+
4482
4761
  char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options *options) {
4483
4762
  if (!markdown || len == 0) {
4484
4763
  char *empty = malloc(1);
@@ -4529,6 +4808,19 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4529
4808
  plugin_manager = apex_plugins_load(options);
4530
4809
  }
4531
4810
 
4811
+ char *quarto_shortcodes_processed = NULL;
4812
+ if (apex_quarto_feature(options, options->enable_quarto_shortcodes)) {
4813
+ bool warn_unknown = getenv("APEX_VERBOSE") != NULL;
4814
+ PROFILE_START(quarto_shortcodes_preprocess);
4815
+ quarto_shortcodes_processed = apex_preprocess_quarto_shortcodes(working_text, warn_unknown, options->unsafe);
4816
+ PROFILE_END(quarto_shortcodes_preprocess);
4817
+ if (quarto_shortcodes_processed) {
4818
+ free(working_text);
4819
+ working_text = quarto_shortcodes_processed;
4820
+ len = strlen(working_text);
4821
+ }
4822
+ }
4823
+
4532
4824
  /* Optional pre-parse plugin hook: run all configured pre_parse plugins
4533
4825
  * over the raw markdown before any Apex-specific preprocessing.
4534
4826
  */
@@ -4552,6 +4844,19 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4552
4844
  char **liquid_tags = NULL;
4553
4845
  size_t liquid_tag_count = 0;
4554
4846
  char *liquid_protected = NULL;
4847
+ char *py_callouts_processed = NULL;
4848
+ /* Process Python-Markdown/markdown-callouts syntax early so metadata parsing
4849
+ * doesn't consume top-of-file TYPE: lines before they can be treated as callouts.
4850
+ */
4851
+ if (options->enable_py_callouts) {
4852
+ PROFILE_START(py_callouts_preprocess);
4853
+ py_callouts_processed = apex_preprocess_py_callouts(text_ptr);
4854
+ PROFILE_END(py_callouts_preprocess);
4855
+ if (py_callouts_processed) {
4856
+ text_ptr = py_callouts_processed;
4857
+ }
4858
+ }
4859
+
4555
4860
 
4556
4861
 
4557
4862
  if (getenv("APEX_DEBUG_PIPELINE")) {
@@ -4565,7 +4870,7 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4565
4870
 
4566
4871
  if (options->mode == APEX_MODE_MULTIMARKDOWN ||
4567
4872
  options->mode == APEX_MODE_KRAMDOWN ||
4568
- options->mode == APEX_MODE_UNIFIED) {
4873
+ apex_mode_is_unified_family(options->mode)) {
4569
4874
  /* Extract metadata FIRST */
4570
4875
  PROFILE_START(metadata);
4571
4876
  metadata = apex_extract_metadata_for_mode(&text_ptr, options->mode);
@@ -4577,7 +4882,7 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4577
4882
  }
4578
4883
 
4579
4884
  /* Extract ALDs for Kramdown */
4580
- if (options->mode == APEX_MODE_KRAMDOWN || options->mode == APEX_MODE_UNIFIED) {
4885
+ if (apex_mode_is_kramdown_or_unified_family(options->mode)) {
4581
4886
  alds = apex_extract_alds(&text_ptr);
4582
4887
  if (getenv("APEX_DEBUG_PIPELINE")) {
4583
4888
  size_t len = strlen(text_ptr);
@@ -4717,13 +5022,27 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4717
5022
 
4718
5023
  /* Process index entries (preprocessing) */
4719
5024
  apex_index_registry index_registry = {0};
5025
+ char *concordance_processed = NULL;
4720
5026
  char *indices_processed = NULL;
5027
+ if (options->enable_indices && options->concordance_files && options->concordance_files[0]) {
5028
+ PROGRESS_REPORT("Applying concordance", -1);
5029
+ PROFILE_START(concordance);
5030
+ concordance_processed = apex_apply_concordance(text_ptr, options);
5031
+ PROFILE_END(concordance);
5032
+ if (concordance_processed) {
5033
+ text_ptr = concordance_processed;
5034
+ }
5035
+ }
4721
5036
  if (options->enable_indices) {
4722
5037
  PROGRESS_REPORT("Processing indices", -1);
4723
5038
  PROFILE_START(indices);
4724
5039
  indices_processed = apex_process_index_entries(text_ptr, &index_registry, options);
4725
5040
  PROFILE_END(indices);
4726
5041
  if (indices_processed) {
5042
+ if (concordance_processed) {
5043
+ free(concordance_processed);
5044
+ concordance_processed = NULL;
5045
+ }
4727
5046
  text_ptr = indices_processed;
4728
5047
  }
4729
5048
  }
@@ -4746,9 +5065,11 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4746
5065
  /* Preprocess image attributes and URL-encode all link URLs */
4747
5066
  image_attr_entry *img_attrs = NULL;
4748
5067
  char *image_attrs_processed = NULL;
4749
- if (options->mode == APEX_MODE_UNIFIED ||
5068
+ if (apex_mode_is_unified_family(options->mode) ||
4750
5069
  options->mode == APEX_MODE_MULTIMARKDOWN ||
4751
- options->mode == APEX_MODE_KRAMDOWN) {
5070
+ options->mode == APEX_MODE_KRAMDOWN ||
5071
+ options->mode == APEX_MODE_GFM ||
5072
+ options->mode == APEX_MODE_COMMONMARK) {
4752
5073
  PROFILE_START(image_attrs_preprocess);
4753
5074
  image_attrs_processed = apex_preprocess_image_attributes(text_ptr, &img_attrs, options->mode);
4754
5075
  PROFILE_END(image_attrs_preprocess);
@@ -4764,7 +5085,8 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4764
5085
 
4765
5086
  /* Preprocess IAL markers (insert blank lines before them so cmark parses correctly) */
4766
5087
  char *ial_preprocessed = NULL;
4767
- if (options->mode == APEX_MODE_KRAMDOWN || options->mode == APEX_MODE_UNIFIED) {
5088
+ char *escaped_toc_protected = NULL;
5089
+ if (options->mode == APEX_MODE_KRAMDOWN || apex_mode_is_unified_family(options->mode)) {
4768
5090
  PROFILE_START(ial_preprocess);
4769
5091
  ial_preprocessed = apex_preprocess_ial(text_ptr);
4770
5092
  PROFILE_END(ial_preprocess);
@@ -4778,9 +5100,42 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4778
5100
  }
4779
5101
  }
4780
5102
 
5103
+ /* Preprocess grid tables BEFORE spans/special_markers.
5104
+ * Grid tables must process === separators before proofreader/highlight converts them.
5105
+ * Converts Pandoc +---+ syntax to pipe tables for the normal table pipeline.
5106
+ */
5107
+ char *grid_tables_processed = NULL;
5108
+ char *normalized_for_grid_tables = NULL;
5109
+ if (options->enable_grid_tables && options->enable_tables) {
5110
+ size_t pre_grid_len = strlen(text_ptr);
5111
+ bool needs_newline_for_grid = (pre_grid_len > 0 &&
5112
+ text_ptr[pre_grid_len - 1] != '\n' &&
5113
+ text_ptr[pre_grid_len - 1] != '\r');
5114
+ if (needs_newline_for_grid) {
5115
+ normalized_for_grid_tables = malloc(pre_grid_len + 2);
5116
+ if (normalized_for_grid_tables) {
5117
+ memcpy(normalized_for_grid_tables, text_ptr, pre_grid_len);
5118
+ normalized_for_grid_tables[pre_grid_len] = '\n';
5119
+ normalized_for_grid_tables[pre_grid_len + 1] = '\0';
5120
+ }
5121
+ }
5122
+
5123
+ PROFILE_START(grid_tables_preprocess);
5124
+ grid_tables_processed = apex_preprocess_grid_tables(normalized_for_grid_tables ? normalized_for_grid_tables : text_ptr);
5125
+ PROFILE_END(grid_tables_preprocess);
5126
+
5127
+ if (normalized_for_grid_tables) {
5128
+ free(normalized_for_grid_tables);
5129
+ }
5130
+
5131
+ if (grid_tables_processed) {
5132
+ text_ptr = grid_tables_processed;
5133
+ }
5134
+ }
5135
+
4781
5136
  /* Preprocess bracketed spans [text]{IAL} */
4782
5137
  char *spans_preprocessed = NULL;
4783
- if (options->enable_spans && (options->mode == APEX_MODE_UNIFIED || options->mode == APEX_MODE_KRAMDOWN)) {
5138
+ if (options->enable_spans && apex_mode_is_kramdown_or_unified_family(options->mode)) {
4784
5139
  PROFILE_START(spans_preprocess);
4785
5140
  spans_preprocessed = apex_preprocess_bracketed_spans(text_ptr);
4786
5141
  PROFILE_END(spans_preprocess);
@@ -4793,7 +5148,13 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4793
5148
  char *includes_processed = NULL;
4794
5149
  if (options->enable_file_includes) {
4795
5150
  PROFILE_START(includes);
4796
- includes_processed = apex_process_includes(text_ptr, options->base_directory, metadata, 0);
5151
+ includes_processed = apex_process_includes(text_ptr,
5152
+ options->base_directory,
5153
+ metadata,
5154
+ 0,
5155
+ options->wikilink_extension,
5156
+ plugin_manager,
5157
+ options);
4797
5158
  PROFILE_END(includes);
4798
5159
  if (includes_processed) {
4799
5160
  text_ptr = includes_processed;
@@ -4821,11 +5182,58 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4821
5182
  text_ptr = inline_tables_processed;
4822
5183
  }
4823
5184
 
5185
+ /* Pandoc/Quarto list extensions before alpha/roman marker normalization */
5186
+ char *example_lists_processed = NULL;
5187
+ char *line_blocks_processed = NULL;
5188
+ char *roman_lists_processed = NULL;
5189
+ char *strict_lists_processed = NULL;
5190
+ if (apex_quarto_feature(options, options->enable_quarto_example_lists)) {
5191
+ PROFILE_START(example_lists_preprocess);
5192
+ example_lists_processed = apex_preprocess_example_lists(text_ptr);
5193
+ PROFILE_END(example_lists_preprocess);
5194
+ if (example_lists_processed) {
5195
+ text_ptr = example_lists_processed;
5196
+ }
5197
+ }
5198
+
5199
+ if (apex_quarto_feature(options, options->enable_quarto_line_blocks)) {
5200
+ PROFILE_START(line_blocks_preprocess);
5201
+ line_blocks_processed = apex_preprocess_line_blocks(text_ptr, options->unsafe);
5202
+ PROFILE_END(line_blocks_preprocess);
5203
+ if (line_blocks_processed) {
5204
+ text_ptr = line_blocks_processed;
5205
+ }
5206
+ }
5207
+
5208
+ if (apex_quarto_feature(options, options->enable_quarto_roman_lists)) {
5209
+ PROFILE_START(roman_lists_preprocess);
5210
+ roman_lists_processed = apex_preprocess_roman_lists(text_ptr);
5211
+ PROFILE_END(roman_lists_preprocess);
5212
+ if (roman_lists_processed) {
5213
+ text_ptr = roman_lists_processed;
5214
+ }
5215
+ }
5216
+
5217
+ if (apex_quarto_feature(options, options->enable_quarto_strict_lists)) {
5218
+ PROFILE_START(quarto_strict_lists_preprocess);
5219
+ strict_lists_processed = apex_preprocess_quarto_strict_lists(text_ptr);
5220
+ PROFILE_END(quarto_strict_lists_preprocess);
5221
+ if (strict_lists_processed) {
5222
+ text_ptr = strict_lists_processed;
5223
+ }
5224
+ }
5225
+
4824
5226
  /* Process alpha lists before parsing (preprocessing) */
4825
5227
  char *alpha_lists_processed = NULL;
5228
+ bool inserted_synthetic_nested_alpha_break = false;
5229
+ bool saw_explicit_nested_alpha_break = false;
4826
5230
  if (options->allow_alpha_lists) {
4827
5231
  PROFILE_START(alpha_lists);
4828
- alpha_lists_processed = apex_preprocess_alpha_lists(text_ptr);
5232
+ alpha_lists_processed = apex_preprocess_alpha_lists(
5233
+ text_ptr,
5234
+ &inserted_synthetic_nested_alpha_break,
5235
+ &saw_explicit_nested_alpha_break
5236
+ );
4829
5237
  PROFILE_END(alpha_lists);
4830
5238
  if (alpha_lists_processed) {
4831
5239
  text_ptr = alpha_lists_processed;
@@ -4833,10 +5241,12 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
4833
5241
  }
4834
5242
 
4835
5243
  char *nested_ordered_sublists_processed = NULL;
5244
+ bool inserted_synthetic_nested_ordered_break = false;
5245
+ bool saw_explicit_nested_ordered_break = false;
4836
5246
 
4837
5247
  /* Process emoji autocorrect before parsing (preprocessing) */
4838
5248
  char *emoji_autocorrect_processed = NULL;
4839
- if (options->enable_emoji_autocorrect && (options->mode == APEX_MODE_UNIFIED || options->mode == APEX_MODE_GFM)) {
5249
+ if (options->enable_emoji_autocorrect && (options->mode == APEX_MODE_GFM || apex_mode_is_unified_family(options->mode))) {
4840
5250
  PROFILE_START(emoji_autocorrect);
4841
5251
  emoji_autocorrect_processed = apex_autocorrect_emoji_names(text_ptr);
4842
5252
  PROFILE_END(emoji_autocorrect);
@@ -5063,10 +5473,52 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5063
5473
  }
5064
5474
  }
5065
5475
 
5476
+ /* Pandoc/Quarto raw content ({=format}) before other fence preprocessors */
5477
+ char *raw_content_processed = NULL;
5478
+ char *code_fence_attrs_processed = NULL;
5479
+ char *quarto_diagrams_processed = NULL;
5480
+ if (apex_quarto_feature(options, options->enable_quarto_raw)) {
5481
+ PROFILE_START(raw_content_preprocess);
5482
+ raw_content_processed = apex_preprocess_raw_content(text_ptr, options->unsafe);
5483
+ PROFILE_END(raw_content_preprocess);
5484
+ if (raw_content_processed) {
5485
+ text_ptr = raw_content_processed;
5486
+ }
5487
+ }
5488
+
5489
+ if (apex_quarto_feature(options, options->enable_quarto_code_attrs)) {
5490
+ PROFILE_START(code_fence_attrs_preprocess);
5491
+ code_fence_attrs_processed = apex_preprocess_code_fence_attrs(text_ptr);
5492
+ PROFILE_END(code_fence_attrs_preprocess);
5493
+ if (code_fence_attrs_processed) {
5494
+ text_ptr = code_fence_attrs_processed;
5495
+ }
5496
+ }
5497
+
5498
+ if (apex_quarto_feature(options, options->enable_quarto_diagrams)) {
5499
+ PROFILE_START(quarto_diagrams_preprocess);
5500
+ quarto_diagrams_processed = apex_preprocess_quarto_diagrams(text_ptr, options->unsafe);
5501
+ PROFILE_END(quarto_diagrams_preprocess);
5502
+ if (quarto_diagrams_processed) {
5503
+ text_ptr = quarto_diagrams_processed;
5504
+ }
5505
+ }
5506
+
5507
+ /* Process Quarto ::: callouts before fenced divs so recognized callouts bypass generic div conversion */
5508
+ char *quarto_callouts_processed = NULL;
5509
+ if (options->enable_quarto_callouts) {
5510
+ PROFILE_START(quarto_callouts_preprocess);
5511
+ quarto_callouts_processed = apex_preprocess_quarto_callouts(text_ptr);
5512
+ PROFILE_END(quarto_callouts_preprocess);
5513
+ if (quarto_callouts_processed) {
5514
+ text_ptr = quarto_callouts_processed;
5515
+ }
5516
+ }
5517
+
5066
5518
  /* Process fenced divs before parsing (preprocessing) */
5067
5519
  /* Only enabled in Unified mode */
5068
5520
  char *fenced_divs_processed = NULL;
5069
- if (options->enable_divs && options->mode == APEX_MODE_UNIFIED) {
5521
+ if (options->enable_divs && apex_mode_is_unified_family(options->mode)) {
5070
5522
  PROFILE_START(fenced_divs);
5071
5523
  fenced_divs_processed = apex_process_fenced_divs(text_ptr);
5072
5524
  PROFILE_END(fenced_divs);
@@ -5473,9 +5925,13 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5473
5925
  }
5474
5926
 
5475
5927
  /* Keep this late so later preprocessors do not collapse inserted separation. */
5476
- if (options->mode == APEX_MODE_UNIFIED) {
5928
+ if (apex_mode_is_unified_family(options->mode)) {
5477
5929
  PROFILE_START(nested_ordered_sublists);
5478
- nested_ordered_sublists_processed = apex_preprocess_nested_ordered_sublists(text_ptr);
5930
+ nested_ordered_sublists_processed = apex_preprocess_nested_ordered_sublists(
5931
+ text_ptr,
5932
+ &inserted_synthetic_nested_ordered_break,
5933
+ &saw_explicit_nested_ordered_break
5934
+ );
5479
5935
  PROFILE_END(nested_ordered_sublists);
5480
5936
  if (nested_ordered_sublists_processed) {
5481
5937
  text_ptr = nested_ordered_sublists_processed;
@@ -5526,6 +5982,15 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5526
5982
  /* Convert options to cmark-gfm format */
5527
5983
  int cmark_opts = apex_to_cmark_options(options);
5528
5984
 
5985
+ /* Protect backslash-escaped {{TOC...}} markers before parsing */
5986
+ if (options->enable_marked_extensions || options->mode == APEX_MODE_MULTIMARKDOWN) {
5987
+ escaped_toc_protected = apex_protect_escaped_toc_markers(text_ptr);
5988
+ if (escaped_toc_protected) {
5989
+ text_ptr = escaped_toc_protected;
5990
+ text_len = strlen(text_ptr);
5991
+ }
5992
+ }
5993
+
5529
5994
  /* Create parser */
5530
5995
  PROFILE_START(parsing);
5531
5996
  cmark_parser *parser = cmark_parser_new(cmark_opts);
@@ -5596,6 +6061,8 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5596
6061
  } else if (options->output_format == APEX_OUTPUT_TERMINAL ||
5597
6062
  options->output_format == APEX_OUTPUT_TERMINAL256) {
5598
6063
  target_format = "terminal";
6064
+ } else if (options->output_format == APEX_OUTPUT_RTF) {
6065
+ target_format = "rtf";
5599
6066
  }
5600
6067
  cmark_node *filtered = apex_run_ast_filters(document, options, target_format);
5601
6068
  if (!filtered && options->ast_filter_strict) {
@@ -5631,14 +6098,14 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5631
6098
 
5632
6099
  /* Postprocess callouts if enabled */
5633
6100
  if (options->enable_callouts) {
5634
- apex_process_callouts_in_tree(document);
6101
+ apex_process_callouts_in_tree(document, options->enable_py_callouts);
5635
6102
  }
5636
6103
 
5637
6104
  /* Process IAL (Inline Attribute Lists) BEFORE manual header IDs.
5638
6105
  IAL handles {: #id}, {#id}, and {.class} - running first ensures these
5639
6106
  are extracted and removed from heading text before manual header ID
5640
6107
  looks for MMD [id] or Kramdown {#id}. Avoids duplicate handling. */
5641
- if (alds || options->mode == APEX_MODE_KRAMDOWN || options->mode == APEX_MODE_UNIFIED) {
6108
+ if (alds || apex_mode_is_kramdown_or_unified_family(options->mode)) {
5642
6109
  /* Fast path: skip AST walk if no IAL markers present */
5643
6110
  /* Check for both Kramdown-style ({:) and Pandoc-style ({# or {.) IALs */
5644
6111
  if (strstr(text_ptr, "{:") != NULL ||
@@ -5676,6 +6143,11 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5676
6143
  if (options->allow_mixed_list_markers) {
5677
6144
  apex_merge_mixed_list_markers(document);
5678
6145
  }
6146
+ if (apex_mode_is_unified_family(options->mode) &&
6147
+ ((inserted_synthetic_nested_ordered_break && !saw_explicit_nested_ordered_break) ||
6148
+ (inserted_synthetic_nested_alpha_break && !saw_explicit_nested_alpha_break))) {
6149
+ apex_tighten_nested_ordered_list_items(document);
6150
+ }
5679
6151
 
5680
6152
  /* Note: Critic Markup is now handled via preprocessing (before parsing) */
5681
6153
 
@@ -5687,6 +6159,18 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5687
6159
  return json;
5688
6160
  }
5689
6161
 
6162
+ if (options->output_format == APEX_OUTPUT_TOC) {
6163
+ if (options->toc_entries_out && options->toc_entries_count_out) {
6164
+ *options->toc_entries_out = apex_generate_toc_entries(
6165
+ document, options->id_format, options->toc_min, options->toc_max,
6166
+ options->toc_entries_count_out);
6167
+ return strdup("");
6168
+ }
6169
+ char *toc_md = apex_generate_toc_markdown(document, options->id_format,
6170
+ options->toc_min, options->toc_max);
6171
+ return toc_md ? toc_md : strdup("");
6172
+ }
6173
+
5690
6174
  /* If output format is Markdown, serialize AST to Markdown and return */
5691
6175
  if (options->output_format == APEX_OUTPUT_MARKDOWN ||
5692
6176
  options->output_format == APEX_OUTPUT_MMD ||
@@ -5729,13 +6213,19 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5729
6213
  return man_html ? man_html : strdup("<!DOCTYPE html><html><body><p>stub</p></body></html>");
5730
6214
  }
5731
6215
 
6216
+ /* If output format is RTF, serialize AST and return */
6217
+ if (options->output_format == APEX_OUTPUT_RTF) {
6218
+ char *rtf = apex_cmark_to_rtf(document, options);
6219
+ return rtf ? rtf : strdup("{\\rtf1\\ansi\\deff0\\pard\\par}\n");
6220
+ }
6221
+
5732
6222
  /* Render to HTML
5733
6223
  * Use custom renderer when we have attributes (IAL, ALDs, or image attributes)
5734
6224
  * Otherwise use standard renderer
5735
6225
  */
5736
6226
  PROFILE_START(rendering);
5737
6227
  char *html;
5738
- if (img_attrs || alds || options->mode == APEX_MODE_KRAMDOWN || options->mode == APEX_MODE_UNIFIED) {
6228
+ if (img_attrs || alds || apex_mode_is_kramdown_or_unified_family(options->mode)) {
5739
6229
  /* Use custom renderer to inject attributes */
5740
6230
  html = apex_render_html_with_attributes(document, cmark_opts);
5741
6231
  } else {
@@ -5761,7 +6251,7 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
5761
6251
  }
5762
6252
 
5763
6253
  /* Restore custom element tags from fenced-div wrappers (fixes out-of-order HTML) */
5764
- if (html && options->enable_divs && options->mode == APEX_MODE_UNIFIED) {
6254
+ if (html && options->enable_divs && apex_mode_is_unified_family(options->mode)) {
5765
6255
  char *fenced_divs_html = apex_postprocess_fenced_divs_html(html);
5766
6256
  if (fenced_divs_html) {
5767
6257
  free(html);
@@ -6041,12 +6531,18 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
6041
6531
  */
6042
6532
  if ((options->enable_marked_extensions || options->mode == APEX_MODE_MULTIMARKDOWN) && html) {
6043
6533
  PROFILE_START(toc);
6044
- char *with_toc = apex_process_toc(html, document, options->id_format);
6534
+ char *with_toc = apex_process_toc(html, document, options->id_format,
6535
+ options->toc_min, options->toc_max);
6045
6536
  PROFILE_END(toc);
6046
6537
  if (with_toc) {
6047
6538
  free(html);
6048
6539
  html = with_toc;
6049
6540
  }
6541
+ char *restored = apex_restore_escaped_toc_markers(html);
6542
+ if (restored) {
6543
+ free(html);
6544
+ html = restored;
6545
+ }
6050
6546
  }
6051
6547
 
6052
6548
  /* Apply ARIA labels if enabled */
@@ -6077,6 +6573,16 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
6077
6573
  }
6078
6574
  }
6079
6575
 
6576
+ if (apex_quarto_feature(options, options->enable_quarto_code_attrs) && html) {
6577
+ PROFILE_START(code_fence_attrs_postprocess);
6578
+ char *fence_html = apex_postprocess_code_fence_attrs_html(html);
6579
+ PROFILE_END(code_fence_attrs_postprocess);
6580
+ if (fence_html) {
6581
+ free(html);
6582
+ html = fence_html;
6583
+ }
6584
+ }
6585
+
6080
6586
  /* Replace abbreviations if any were found */
6081
6587
  if (abbreviations && html) {
6082
6588
  PROFILE_START(abbreviations);
@@ -6089,7 +6595,7 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
6089
6595
  }
6090
6596
 
6091
6597
  /* Replace GitHub emoji if in GFM or Unified mode */
6092
- if ((options->mode == APEX_MODE_GFM || options->mode == APEX_MODE_UNIFIED) && html) {
6598
+ if ((options->mode == APEX_MODE_GFM || apex_mode_is_unified_family(options->mode)) && html) {
6093
6599
  PROFILE_START(emoji);
6094
6600
  char *with_emoji = apex_replace_emoji(html);
6095
6601
  PROFILE_END(emoji);
@@ -6211,6 +6717,26 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
6211
6717
  }
6212
6718
  }
6213
6719
 
6720
+ if (apex_quarto_feature(options, options->enable_quarto_roman_lists) && html) {
6721
+ PROFILE_START(roman_lists_postprocess);
6722
+ char *roman_html = apex_postprocess_roman_lists_html(html);
6723
+ PROFILE_END(roman_lists_postprocess);
6724
+ if (roman_html) {
6725
+ free(html);
6726
+ html = roman_html;
6727
+ }
6728
+ }
6729
+
6730
+ if (apex_quarto_feature(options, options->enable_quarto_xrefs) && html) {
6731
+ PROFILE_START(quarto_xrefs_postprocess);
6732
+ char *xref_html = apex_postprocess_quarto_xrefs_html(html);
6733
+ PROFILE_END(quarto_xrefs_postprocess);
6734
+ if (xref_html) {
6735
+ free(html);
6736
+ html = xref_html;
6737
+ }
6738
+ }
6739
+
6214
6740
  /* Remove empty paragraphs created by ^ marker (zero-width space only) */
6215
6741
  if (html && options->enable_marked_extensions) {
6216
6742
  PROFILE_START(remove_empty_paragraphs);
@@ -6230,7 +6756,18 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
6230
6756
  cmark_parser_free(parser);
6231
6757
  free(working_text);
6232
6758
  if (ial_preprocessed) free(ial_preprocessed);
6759
+ if (escaped_toc_protected) free(escaped_toc_protected);
6233
6760
  if (spans_preprocessed) free(spans_preprocessed);
6761
+ if (grid_tables_processed) free(grid_tables_processed);
6762
+ if (raw_content_processed) free(raw_content_processed);
6763
+ if (code_fence_attrs_processed) free(code_fence_attrs_processed);
6764
+ if (quarto_diagrams_processed) free(quarto_diagrams_processed);
6765
+ if (example_lists_processed) free(example_lists_processed);
6766
+ if (line_blocks_processed) free(line_blocks_processed);
6767
+ if (roman_lists_processed) free(roman_lists_processed);
6768
+ if (strict_lists_processed) free(strict_lists_processed);
6769
+ if (quarto_callouts_processed) free(quarto_callouts_processed);
6770
+ if (py_callouts_processed) free(py_callouts_processed);
6234
6771
  if (includes_processed) free(includes_processed);
6235
6772
  if (emoji_autocorrect_processed) free(emoji_autocorrect_processed);
6236
6773
  if (markers_processed_early) {
@@ -6250,6 +6787,7 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
6250
6787
  if (deflist_processed) free(deflist_processed);
6251
6788
  if (fenced_divs_processed) free(fenced_divs_processed);
6252
6789
  if (metadata_replaced) free(metadata_replaced);
6790
+ if (concordance_processed) free(concordance_processed);
6253
6791
  if (autolinks_processed) free(autolinks_processed);
6254
6792
  if (html_markdown_processed) free(html_markdown_processed);
6255
6793
  if (hashtags_processed) free(hashtags_processed);
@@ -6284,28 +6822,55 @@ char *apex_markdown_to_html(const char *markdown, size_t len, const apex_options
6284
6822
 
6285
6823
  /* Build script HTML (if any) from script_tags before wrapping or appending */
6286
6824
  char *scripts_html = NULL;
6287
- if (local_opts.script_tags) {
6825
+ const char *auto_mermaid_script = NULL;
6826
+ if (local_opts.standalone && html && local_opts.enable_quarto_diagrams &&
6827
+ apex_html_has_mermaid_diagram(html)) {
6828
+ bool has_mermaid_script = false;
6829
+ if (local_opts.script_tags) {
6830
+ for (char **p = local_opts.script_tags; *p; ++p) {
6831
+ if (*p && strstr(*p, "mermaid")) {
6832
+ has_mermaid_script = true;
6833
+ break;
6834
+ }
6835
+ }
6836
+ }
6837
+ if (!has_mermaid_script) {
6838
+ auto_mermaid_script = apex_mermaid_script_tag();
6839
+ }
6840
+ }
6841
+
6842
+ if (local_opts.script_tags || auto_mermaid_script) {
6288
6843
  /* Join script tag snippets with newlines */
6289
6844
  size_t total_len = 0;
6290
6845
  size_t count = 0;
6291
- for (char **p = local_opts.script_tags; *p; ++p) {
6846
+ for (char **p = local_opts.script_tags; p && *p; ++p) {
6292
6847
  size_t len = strlen(*p);
6293
6848
  if (len == 0) continue;
6294
6849
  total_len += len + 1; /* +1 for newline */
6295
6850
  count++;
6296
6851
  }
6852
+ if (auto_mermaid_script) {
6853
+ total_len += strlen(auto_mermaid_script) + 1;
6854
+ count++;
6855
+ }
6297
6856
 
6298
6857
  if (count > 0 && total_len > 0) {
6299
6858
  scripts_html = malloc(total_len + 1); /* +1 for null terminator */
6300
6859
  if (scripts_html) {
6301
6860
  size_t pos = 0;
6302
- for (char **p = local_opts.script_tags; *p; ++p) {
6861
+ for (char **p = local_opts.script_tags; p && *p; ++p) {
6303
6862
  size_t len = strlen(*p);
6304
6863
  if (len == 0) continue;
6305
6864
  memcpy(scripts_html + pos, *p, len);
6306
6865
  pos += len;
6307
6866
  scripts_html[pos++] = '\n';
6308
6867
  }
6868
+ if (auto_mermaid_script) {
6869
+ size_t len = strlen(auto_mermaid_script);
6870
+ memcpy(scripts_html + pos, auto_mermaid_script, len);
6871
+ pos += len;
6872
+ scripts_html[pos++] = '\n';
6873
+ }
6309
6874
  scripts_html[pos] = '\0';
6310
6875
  }
6311
6876
  }
@@ -7016,6 +7581,11 @@ char *apex_wrap_html_document(const char *content, const char *title, const char
7016
7581
  " ins { background: #d4fcbc; text-decoration: none; }\n"
7017
7582
  " del { background: #fbb6c2; text-decoration: line-through; }\n"
7018
7583
  " mark { background: #fff3cd; }\n"
7584
+ " .smallcaps { font-variant: small-caps; }\n"
7585
+ " .underline { text-decoration: underline; }\n"
7586
+ " span.mark { background: #fff3cd; }\n"
7587
+ " .hidden { display: none; }\n"
7588
+ " .quarto-xref { font-variant: normal; }\n"
7019
7589
  " .critic.comment { background: #e7e7e7; color: #666; font-style: italic; }\n"
7020
7590
  " .mkhashtag { color: #666; }\n"
7021
7591
  " .mkstyledtag {\n"