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
@@ -4,11 +4,79 @@
4
4
 
5
5
  #include "test_helpers.h"
6
6
  #include "apex/apex.h"
7
+ #include "../src/html_renderer.h"
7
8
  #include "../src/extensions/advanced_footnotes.h"
8
9
  #include <stdio.h>
9
10
  #include <stdlib.h>
10
11
  #include <string.h>
11
12
 
13
+ static char *replace_first_substring(const char *text,
14
+ const char *needle,
15
+ const char *replacement) {
16
+ const char *pos = strstr(text, needle);
17
+ if (!pos) {
18
+ return NULL;
19
+ }
20
+
21
+ size_t prefix_len = (size_t)(pos - text);
22
+ size_t needle_len = strlen(needle);
23
+ size_t replacement_len = strlen(replacement);
24
+ size_t suffix_len = strlen(pos + needle_len);
25
+ char *result = malloc(prefix_len + replacement_len + suffix_len + 1);
26
+ if (!result) {
27
+ return NULL;
28
+ }
29
+
30
+ memcpy(result, text, prefix_len);
31
+ memcpy(result + prefix_len, replacement, replacement_len);
32
+ memcpy(result + prefix_len + replacement_len, pos + needle_len, suffix_len + 1);
33
+ return result;
34
+ }
35
+
36
+ static char *include_preparse_plugin_callback(const char *text,
37
+ __attribute__((unused)) const char *id_plugin,
38
+ apex_plugin_phase_mask phase,
39
+ __attribute__((unused)) const apex_options *options) {
40
+ if (!(phase & APEX_PLUGIN_PHASE_PRE_PARSE) || !text) {
41
+ return NULL;
42
+ }
43
+
44
+ char *current = strdup(text);
45
+ if (!current) {
46
+ return NULL;
47
+ }
48
+
49
+ bool changed = false;
50
+ char *next = replace_first_substring(current, "Included Content", "Plugin Included Content");
51
+ if (next) {
52
+ free(current);
53
+ current = next;
54
+ changed = true;
55
+ }
56
+
57
+ next = replace_first_substring(current, "## Section 2", "## Plugin Section 2");
58
+ if (next) {
59
+ free(current);
60
+ current = next;
61
+ changed = true;
62
+ }
63
+
64
+ if (!changed) {
65
+ free(current);
66
+ return NULL;
67
+ }
68
+
69
+ return current;
70
+ }
71
+
72
+ static void include_preparse_plugin_register(apex_plugin_manager *manager,
73
+ __attribute__((unused)) const apex_options *options) {
74
+ apex_plugin_register(manager,
75
+ "include-preparse-plugin",
76
+ APEX_PLUGIN_PHASE_PRE_PARSE,
77
+ include_preparse_plugin_callback);
78
+ }
79
+
12
80
  void test_math(void) {
13
81
  int suite_failures = suite_start();
14
82
  print_suite_title("Math Support Tests", false, true);
@@ -172,10 +240,331 @@ void test_processor_modes(void) {
172
240
  assert_contains(html, "<h1", "Unified mode works");
173
241
  apex_free_string(html);
174
242
 
243
+ /* Test Quarto mode */
244
+ apex_options quarto_opts = apex_options_for_mode(APEX_MODE_QUARTO);
245
+ html = apex_markdown_to_html(markdown, strlen(markdown), &quarto_opts);
246
+ assert_contains(html, "<h1", "Quarto mode works");
247
+ test_result(quarto_opts.enable_quarto_callouts == true, "Quarto mode enables quarto callouts by default");
248
+ test_result(quarto_opts.enable_quarto_extensions == true, "Quarto mode enables quarto extensions by default");
249
+ test_result(quarto_opts.enable_quarto_diagrams == true, "Quarto mode enables quarto diagrams by default");
250
+ test_result(quarto_opts.enable_quarto_shortcodes == true, "Quarto mode enables quarto shortcodes by default");
251
+ test_result(quarto_opts.enable_quarto_xrefs == true, "Quarto mode enables quarto xrefs by default");
252
+ test_result(quarto_opts.enable_quarto_strict_lists == false, "Quarto mode keeps strict lists off by default");
253
+ apex_free_string(html);
254
+
175
255
  bool had_failures = suite_end(suite_failures);
176
256
  print_suite_title("Processor Modes Tests", had_failures, false);
177
257
  }
178
258
 
259
+ void test_quarto_mode(void) {
260
+ int suite_failures = suite_start();
261
+ print_suite_title("Quarto Mode Tests", false, true);
262
+
263
+ apex_options opts = apex_options_for_mode(APEX_MODE_QUARTO);
264
+ char *html;
265
+
266
+ const char *callout =
267
+ "::: {.callout-warning}\n"
268
+ "Warning body.\n"
269
+ "::: ";
270
+ html = apex_markdown_to_html(callout, strlen(callout), &opts);
271
+ assert_contains(html, "class=\"callout", "Quarto mode renders callout blocks");
272
+ assert_contains(html, "Warning body", "Quarto callout body preserved");
273
+ apex_free_string(html);
274
+
275
+ const char *fig_alt = "![Caption](elephant.png){fig-alt=\"Alt text\"}";
276
+ html = apex_markdown_to_html(fig_alt, strlen(fig_alt), &opts);
277
+ assert_contains(html, "<figure>", "fig-alt image wrapped in figure");
278
+ assert_contains(html, "<figcaption>Caption</figcaption>", "fig-alt uses markdown alt as caption");
279
+ assert_contains(html, "alt=\"Alt text\"", "fig-alt maps to img alt attribute");
280
+ assert_not_contains(html, "fig-alt=", "fig-alt attribute stripped from output");
281
+ apex_free_string(html);
282
+
283
+ /* Direct convert test: fig-alt rewrites img alt for accessibility */
284
+ {
285
+ const char *in = "<p><img src=\"elephant.png\" alt=\"Caption\" fig-alt=\"Alt text\" /></p>";
286
+ char *out = apex_convert_image_captions(in, true, false);
287
+ assert_contains(out, "alt=\"Alt text\"", "fig-alt maps to img alt attribute (direct convert)");
288
+ assert_contains(out, "<figcaption>Caption</figcaption>", "fig-alt caption from markdown alt (direct convert)");
289
+ free(out);
290
+ }
291
+
292
+ const char *div_block = "::: {.sidebar}\nSidebar content.\n:::";
293
+ html = apex_markdown_to_html(div_block, strlen(div_block), &opts);
294
+ assert_contains(html, "class=\"sidebar\"", "Quarto mode renders fenced divs");
295
+ apex_free_string(html);
296
+
297
+ const char *raw_html_block =
298
+ "Before\n\n"
299
+ "```{=html}\n"
300
+ "<strong>raw</strong>\n"
301
+ "```\n\n"
302
+ "After";
303
+ html = apex_markdown_to_html(raw_html_block, strlen(raw_html_block), &opts);
304
+ assert_contains(html, "<strong>raw</strong>", "raw {=html} block passthrough");
305
+ assert_not_contains(html, "**raw**", "raw {=html} block not markdown-processed");
306
+ assert_not_contains(html, "<code", "raw {=html} block not wrapped in code");
307
+ apex_free_string(html);
308
+
309
+ const char *raw_latex_block =
310
+ "```{=latex}\n"
311
+ "\\textbf{Bold}\n"
312
+ "```";
313
+ html = apex_markdown_to_html(raw_latex_block, strlen(raw_latex_block), &opts);
314
+ assert_contains(html, "<!-- raw format=latex -->", "raw {=latex} block wrapped in comment");
315
+ assert_contains(html, "\\textbf{Bold}", "raw {=latex} body preserved in comment");
316
+ assert_not_contains(html, "<code", "raw {=latex} block not wrapped in code");
317
+ apex_free_string(html);
318
+
319
+ const char *raw_html_inline = "Text `<em>inline</em>`{=html} end.";
320
+ html = apex_markdown_to_html(raw_html_inline, strlen(raw_html_inline), &opts);
321
+ assert_contains(html, "<em>inline</em>", "inline {=html} passthrough");
322
+ assert_not_contains(html, "`{=html}`", "inline {=html} marker stripped");
323
+ apex_free_string(html);
324
+
325
+ /* Unified mode without quarto extensions leaves {=html} fences intact */
326
+ apex_options unified_opts = apex_options_for_mode(APEX_MODE_UNIFIED);
327
+ unified_opts.enable_quarto_extensions = false;
328
+ html = apex_markdown_to_html("```{=html}\n<b>x</b>\n```", 18, &unified_opts);
329
+ assert_contains(html, "<code", "non-quarto mode keeps {=html} as code fence");
330
+ apex_free_string(html);
331
+
332
+ const char *example_lists =
333
+ "(@) My first example will be numbered (1).\n"
334
+ "(@) My second example will be numbered (2).\n";
335
+ html = apex_markdown_to_html(example_lists, strlen(example_lists), &opts);
336
+ assert_contains(html, "<ol>", "example list (@) produces ordered list");
337
+ assert_contains(html, "My first example", "example list first item");
338
+ assert_contains(html, "My second example", "example list second item");
339
+ assert_not_contains(html, "(@)", "example list marker stripped");
340
+ apex_free_string(html);
341
+
342
+ const char *example_lists_break =
343
+ "(@) My first example will be numbered (1).\n"
344
+ "(@) My second example will be numbered (2).\n"
345
+ "\n"
346
+ "Explanation of examples.\n"
347
+ "\n"
348
+ "(@) My third example will be numbered (3).\n";
349
+ html = apex_markdown_to_html(example_lists_break, strlen(example_lists_break), &opts);
350
+ assert_contains(html, "Explanation of examples", "example list keeps interruption text");
351
+ assert_contains(html, "My third example", "example list third item after break");
352
+ assert_contains(html, "start=\"3\"", "example list continues numbering after interruption");
353
+ assert_not_contains(html, "(@)", "example list break strips marker");
354
+ apex_free_string(html);
355
+
356
+ const char *example_lists_labeled =
357
+ "(@good) This is a good example.\n";
358
+ html = apex_markdown_to_html(example_lists_labeled, strlen(example_lists_labeled), &opts);
359
+ assert_contains(html, "This is a good example", "labeled example list item content");
360
+ assert_not_contains(html, "(@good)", "labeled example list marker stripped");
361
+ apex_free_string(html);
362
+
363
+ const char *roman_list =
364
+ "i) First\n"
365
+ "ii) Second\n"
366
+ "iii) Third\n";
367
+ html = apex_markdown_to_html(roman_list, strlen(roman_list), &opts);
368
+ assert_contains(html, "list-style-type: lower-roman", "roman list lower-roman style");
369
+ assert_contains(html, "First", "roman list first item");
370
+ assert_contains(html, "Second", "roman list second item");
371
+ assert_not_contains(html, "<!-- apex-alpha-list-", "roman list marker comment stripped");
372
+ apex_free_string(html);
373
+
374
+ const char *line_block =
375
+ "| Line one\n"
376
+ "| preserved spaces\n"
377
+ "| Line three\n";
378
+ html = apex_markdown_to_html(line_block, strlen(line_block), &opts);
379
+ assert_contains(html, "class=\"line-block\"", "line block wrapper");
380
+ assert_contains(html, "class=\"line\">Line one", "line block first line");
381
+ assert_contains(html, "class=\"line\"> preserved spaces", "line block preserves inner spaces");
382
+ assert_contains(html, "class=\"line\">Line three", "line block third line");
383
+ apex_free_string(html);
384
+
385
+ const char *code_fence_attrs =
386
+ "```{.python filename=\"run.py\"}\n"
387
+ "print(\"hello\")\n"
388
+ "```\n";
389
+ html = apex_markdown_to_html(code_fence_attrs, strlen(code_fence_attrs), &opts);
390
+ assert_contains(html, "data-filename=\"run.py\"", "code fence filename attribute on pre");
391
+ assert_contains(html, "print", "code fence body preserved");
392
+ assert_not_contains(html, "{.python", "code fence braced info string normalized");
393
+ assert_not_contains(html, "apex-code-fence-attrs", "code fence marker comment stripped");
394
+ apex_free_string(html);
395
+
396
+ const char *code_fence_linenos =
397
+ "```{.python linenos=true}\n"
398
+ "print(\"hi\")\n"
399
+ "```\n";
400
+ html = apex_markdown_to_html(code_fence_linenos, strlen(code_fence_linenos), &opts);
401
+ assert_contains(html, "data-linenos=\"true\"", "code fence linenos attribute on pre");
402
+ apex_free_string(html);
403
+
404
+ const char *mermaid_fence =
405
+ "```{mermaid}\n"
406
+ "flowchart LR\n"
407
+ " A --> B\n"
408
+ "```\n";
409
+ html = apex_markdown_to_html(mermaid_fence, strlen(mermaid_fence), &opts);
410
+ assert_contains(html, "<pre class=\"mermaid\">", "mermaid fence renders pre.mermaid");
411
+ assert_contains(html, "flowchart LR", "mermaid fence body preserved");
412
+ assert_contains(html, "A --> B", "mermaid fence diagram content preserved");
413
+ assert_not_contains(html, "{mermaid}", "mermaid fence marker stripped");
414
+ assert_not_contains(html, "<code", "mermaid fence not wrapped in code");
415
+ apex_free_string(html);
416
+
417
+ const char *dot_fence =
418
+ "```{dot}\n"
419
+ "digraph { A -> B; }\n"
420
+ "```\n";
421
+ html = apex_markdown_to_html(dot_fence, strlen(dot_fence), &opts);
422
+ assert_contains(html, "<pre class=\"graphviz\">", "dot fence renders pre.graphviz");
423
+ assert_contains(html, "digraph", "dot fence body preserved");
424
+ apex_free_string(html);
425
+
426
+ const char *graphviz_fence =
427
+ "```{graphviz}\n"
428
+ "graph { A -- B; }\n"
429
+ "```\n";
430
+ html = apex_markdown_to_html(graphviz_fence, strlen(graphviz_fence), &opts);
431
+ assert_contains(html, "<pre class=\"graphviz\">", "graphviz fence renders pre.graphviz");
432
+ assert_contains(html, "graph { A -- B", "graphviz fence body preserved");
433
+ apex_free_string(html);
434
+
435
+ apex_options no_diagrams_opts = apex_options_for_mode(APEX_MODE_QUARTO);
436
+ no_diagrams_opts.enable_quarto_diagrams = false;
437
+ html = apex_markdown_to_html(mermaid_fence, strlen(mermaid_fence), &no_diagrams_opts);
438
+ assert_contains(html, "<code", "disabled quarto diagrams keeps mermaid as code fence");
439
+ assert_not_contains(html, "class=\"mermaid\"", "disabled quarto diagrams skips pre.mermaid");
440
+ apex_free_string(html);
441
+
442
+ apex_options no_unsafe_opts = apex_options_for_mode(APEX_MODE_QUARTO);
443
+ no_unsafe_opts.unsafe = false;
444
+ html = apex_markdown_to_html(mermaid_fence, strlen(mermaid_fence), &no_unsafe_opts);
445
+ assert_contains(html, "<code", "no-unsafe keeps mermaid fence as code block");
446
+ assert_not_contains(html, "class=\"mermaid\"", "no-unsafe skips raw pre.mermaid");
447
+ apex_free_string(html);
448
+
449
+ apex_options standalone_opts = apex_options_for_mode(APEX_MODE_QUARTO);
450
+ standalone_opts.standalone = true;
451
+ html = apex_markdown_to_html(mermaid_fence, strlen(mermaid_fence), &standalone_opts);
452
+ assert_contains(html, "mermaid.min.js", "standalone quarto auto-injects mermaid script");
453
+ assert_contains(html, "</body>", "standalone mermaid output is full document");
454
+ apex_free_string(html);
455
+
456
+ apex_options standalone_with_script = apex_options_for_mode(APEX_MODE_QUARTO);
457
+ standalone_with_script.standalone = true;
458
+ standalone_with_script.script_tags = (char *[]){ strdup("<!-- mermaid already -->"), NULL };
459
+ html = apex_markdown_to_html(mermaid_fence, strlen(mermaid_fence), &standalone_with_script);
460
+ assert_not_contains(html, "mermaid.min.js", "standalone skips auto mermaid when script_tags mention mermaid");
461
+ assert_contains(html, "<!-- mermaid already -->", "standalone preserves user script_tags");
462
+ apex_free_string(html);
463
+ free(standalone_with_script.script_tags[0]);
464
+
465
+ const char *pagebreak_shortcode = "Page 1\n\n{{< pagebreak >}}\n\nPage 2";
466
+ html = apex_markdown_to_html(pagebreak_shortcode, strlen(pagebreak_shortcode), &opts);
467
+ assert_contains(html, "page-break-after", "pagebreak shortcode renders page break");
468
+ assert_contains(html, "Page 2", "content after pagebreak shortcode preserved");
469
+ assert_not_contains(html, "{{< pagebreak >}}", "pagebreak shortcode marker stripped");
470
+ apex_free_string(html);
471
+
472
+ const char *kbd_shortcode = "Press {{< kbd $@3 >}} to save.";
473
+ html = apex_markdown_to_html(kbd_shortcode, strlen(kbd_shortcode), &opts);
474
+ assert_contains(html, "{% kbd $@3 %}", "kbd shortcode converts to liquid tag");
475
+ assert_not_contains(html, "{{< kbd", "kbd shortcode marker stripped");
476
+ apex_free_string(html);
477
+
478
+ apex_options include_opts = apex_options_for_mode(APEX_MODE_QUARTO);
479
+ include_opts.enable_file_includes = true;
480
+ #ifdef TEST_FIXTURES_DIR
481
+ include_opts.base_directory = TEST_FIXTURES_DIR;
482
+ #else
483
+ include_opts.base_directory = "tests/fixtures/includes";
484
+ #endif
485
+ const char *include_shortcode = "Before\n\n{{< include simple.md >}}\n\nAfter";
486
+ html = apex_markdown_to_html(include_shortcode, strlen(include_shortcode), &include_opts);
487
+ assert_contains(html, "Included Content", "include shortcode expands file content");
488
+ assert_contains(html, "After", "content after include shortcode preserved");
489
+ assert_not_contains(html, "{{< include", "include shortcode marker stripped");
490
+ apex_free_string(html);
491
+
492
+ apex_options no_shortcodes_opts = apex_options_for_mode(APEX_MODE_QUARTO);
493
+ no_shortcodes_opts.enable_quarto_shortcodes = false;
494
+ html = apex_markdown_to_html(pagebreak_shortcode, strlen(pagebreak_shortcode), &no_shortcodes_opts);
495
+ assert_contains(html, "{{< pagebreak", "disabled quarto shortcodes leave marker in output");
496
+ assert_not_contains(html, "page-break-after", "disabled quarto shortcodes skip pagebreak expansion");
497
+ apex_free_string(html);
498
+
499
+ const char *unknown_shortcode = "Text {{< widget foo >}} end.";
500
+ html = apex_markdown_to_html(unknown_shortcode, strlen(unknown_shortcode), &opts);
501
+ assert_contains(html, "{{< widget foo", "unknown shortcode left unchanged");
502
+ apex_free_string(html);
503
+
504
+ const char *empty_div_reset =
505
+ "1. First\n\n::: {}\n\n1. Second\n";
506
+ html = apex_markdown_to_html(empty_div_reset, strlen(empty_div_reset), &opts);
507
+ assert_contains(html, "<ol>", "empty div list reset produces ordered lists");
508
+ {
509
+ const char *first_ol = strstr(html, "<ol>");
510
+ const char *second_ol = first_ol ? strstr(first_ol + 1, "<ol>") : NULL;
511
+ test_result(second_ol != NULL, "empty div separates two ordered lists");
512
+ }
513
+ apex_free_string(html);
514
+
515
+ const char *cross_ref = "See @fig-elephant and @sec-intro for details.";
516
+ html = apex_markdown_to_html(cross_ref, strlen(cross_ref), &opts);
517
+ assert_contains(html, "<span class=\"quarto-xref\">@fig-elephant</span>", "fig cross-ref wrapped");
518
+ assert_contains(html, "<span class=\"quarto-xref\">@sec-intro</span>", "sec cross-ref wrapped");
519
+ apex_free_string(html);
520
+
521
+ {
522
+ apex_options cite_opts = apex_options_for_mode(APEX_MODE_QUARTO);
523
+ cite_opts.base_directory = "tests";
524
+ const char *bib_files[] = { "test_refs.bib", NULL };
525
+ cite_opts.bibliography_files = (char **)bib_files;
526
+ const char *mixed_cite_xref =
527
+ "See [@doe99] and @fig-elephant.";
528
+ html = apex_markdown_to_html(mixed_cite_xref, strlen(mixed_cite_xref), &cite_opts);
529
+ assert_contains(html, "citation", "bracket citation processed with bibliography");
530
+ assert_contains(html, "doe99", "bracket citation key preserved with bibliography");
531
+ assert_contains(html, "<span class=\"quarto-xref\">@fig-elephant</span>",
532
+ "bare fig xref not parsed as citation when bibliography set");
533
+ assert_not_contains(html, "data-cites=\"fig-elephant\"", "fig xref not in citation data-cites");
534
+ apex_free_string(html);
535
+
536
+ const char *bracket_fig = "See [@fig-elephant].";
537
+ html = apex_markdown_to_html(bracket_fig, strlen(bracket_fig), &cite_opts);
538
+ assert_contains(html, "citation", "bracketed @fig- key still processed as citation");
539
+ assert_contains(html, "fig-elephant", "bracketed fig- citation key preserved");
540
+ apex_free_string(html);
541
+
542
+ apex_options no_xref_opts = cite_opts;
543
+ no_xref_opts.enable_quarto_xrefs = false;
544
+ const char *bare_fig = "See @fig-elephant.";
545
+ html = apex_markdown_to_html(bare_fig, strlen(bare_fig), &no_xref_opts);
546
+ assert_contains(html, "citation", "bare @fig- treated as citation when quarto-xrefs off");
547
+ assert_not_contains(html, "quarto-xref", "no xref wrapper when quarto-xrefs off");
548
+ apex_free_string(html);
549
+ }
550
+
551
+ apex_options hidden_opts = apex_options_for_mode(APEX_MODE_QUARTO);
552
+ hidden_opts.standalone = true;
553
+ html = apex_markdown_to_html("::: {.hidden}\nHidden content.\n:::", 35, &hidden_opts);
554
+ assert_contains(html, "class=\"hidden\"", "hidden div class preserved");
555
+ assert_contains(html, ".hidden { display: none; }", "standalone quarto CSS hides .hidden");
556
+ apex_free_string(html);
557
+
558
+ apex_options unified_opts2 = apex_options_for_mode(APEX_MODE_UNIFIED);
559
+ unified_opts2.enable_quarto_extensions = false;
560
+ html = apex_markdown_to_html("```{.python}\nx\n```", 18, &unified_opts2);
561
+ assert_contains(html, "{.python}", "non-quarto mode keeps braced fence info string");
562
+ apex_free_string(html);
563
+
564
+ bool had_failures = suite_end(suite_failures);
565
+ print_suite_title("Quarto Mode Tests", had_failures, false);
566
+ }
567
+
179
568
  /**
180
569
  * Test cmark_init callback for custom extension registration
181
570
  */
@@ -401,6 +790,87 @@ void test_multimarkdown_image_attributes(void) {
401
790
  apex_free_string(html);
402
791
  }
403
792
 
793
+ /* Picture conversion should preserve image IAL attrs on fallback <img> */
794
+ {
795
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
796
+ const char *picture_ial_md =
797
+ "[![text-basic widget screenshot](img/text-basic.jpg avif @2x){:loading=lazy width=400 height=400}]"
798
+ "(img/text-basic@2x.jpg){.widget-shot.js-lightbox}";
799
+ char *html = apex_markdown_to_html(picture_ial_md, strlen(picture_ial_md), &opts);
800
+ assert_contains(html, "<a href=\"img/text-basic@2x.jpg\" class=\"widget-shot js-lightbox\">",
801
+ "picture+ial: outer link classes preserved");
802
+ assert_contains(html, "<picture>", "picture+ial: picture element");
803
+ assert_contains(html, "img/text-basic.avif 1x, img/text-basic@2x.avif 2x",
804
+ "picture+ial: avif srcset with @2x");
805
+ assert_contains(html,
806
+ "<img src=\"img/text-basic.jpg\" alt=\"text-basic widget screenshot\" width=\"400\" height=\"400\" loading=\"lazy\">",
807
+ "picture+ial: fallback img preserves IAL attrs");
808
+ apex_free_string(html);
809
+ }
810
+
811
+ /* Raw HTML <a> must not steal/drop later markdown link IAL classes */
812
+ {
813
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
814
+ opts.enable_autolink = false;
815
+ opts.enable_image_captions = false;
816
+ const char *md =
817
+ "<div class=\"note\">\n"
818
+ "<p>See <a href=\"/integrations#getting-started\">Integrations</a>.</p>\n"
819
+ "</div>\n\n"
820
+ "- [![alt](/x.jpg avif){:loading=lazy width=400 height=400}]"
821
+ "(/x@2x.jpg){.widget-shot.js-lightbox}\n"
822
+ "{.g}\n";
823
+ char *html = apex_markdown_to_html(md, strlen(md), &opts);
824
+ assert_contains(html,
825
+ "<a href=\"/x@2x.jpg\" class=\"widget-shot js-lightbox\">",
826
+ "raw html <a> must not steal/drop later link IAL classes");
827
+ assert_contains(html,
828
+ "href=\"/integrations#getting-started\">Integrations</a>",
829
+ "raw html callout link remains unchanged");
830
+ assert_not_contains(html,
831
+ "href=\"/integrations#getting-started\" class=\"widget-shot",
832
+ "IAL must not be injected onto the raw html <a>");
833
+ apex_free_string(html);
834
+ }
835
+
836
+ /* Distinct href IALs after a raw HTML <a> keep correct classes */
837
+ {
838
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
839
+ opts.enable_autolink = false;
840
+ const char *md =
841
+ "<p><a href=\"/raw\">Raw</a></p>\n\n"
842
+ "[One](/one){.class-one}\n\n"
843
+ "[Two](/two){.class-two}\n";
844
+ char *html = apex_markdown_to_html(md, strlen(md), &opts);
845
+ assert_contains(html, "<a href=\"/one\" class=\"class-one\">",
846
+ "distinct href after raw <a>: first link IAL");
847
+ assert_contains(html, "<a href=\"/two\" class=\"class-two\">",
848
+ "distinct href after raw <a>: second link IAL");
849
+ assert_not_contains(html, "href=\"/raw\" class=",
850
+ "distinct href after raw <a>: raw link unchanged");
851
+ apex_free_string(html);
852
+ }
853
+
854
+ /* Same-href markdown links after raw HTML <a>: disambiguate by order */
855
+ {
856
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
857
+ opts.enable_autolink = false;
858
+ const char *md =
859
+ "<p><a href=\"/same\">Raw</a></p>\n\n"
860
+ "[First](/same){.first}\n\n"
861
+ "[Second](/same){.second}\n";
862
+ char *html = apex_markdown_to_html(md, strlen(md), &opts);
863
+ assert_contains(html, "<a href=\"/same\" class=\"first\">First</a>",
864
+ "same href after raw <a>: first markdown link IAL");
865
+ assert_contains(html, "<a href=\"/same\" class=\"second\">Second</a>",
866
+ "same href after raw <a>: second markdown link IAL");
867
+ assert_not_contains(html, "href=\"/same\" class=\"first\">Raw",
868
+ "same href after raw <a>: raw link not given first IAL");
869
+ assert_not_contains(html, "href=\"/same\" class=\"second\">Raw",
870
+ "same href after raw <a>: raw link not given second IAL");
871
+ apex_free_string(html);
872
+ }
873
+
404
874
  /* Video URL: ![alt](video.mp4) emits <video> */
405
875
  {
406
876
  apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
@@ -566,6 +1036,21 @@ void test_file_includes(void) {
566
1036
  assert_contains(html, "Included Content", "MMD transclusion works");
567
1037
  apex_free_string(html);
568
1038
 
1039
+ apex_options plugin_opts = opts;
1040
+ plugin_opts.enable_plugins = true;
1041
+ plugin_opts.allow_external_plugin_detection = false;
1042
+ plugin_opts.plugin_register = include_preparse_plugin_register;
1043
+ html = apex_markdown_to_html("Include: {{simple.md}}", 22, &plugin_opts);
1044
+ assert_contains(html, "Plugin Included Content", "Included file runs through pre-parse plugin pipeline");
1045
+ apex_free_string(html);
1046
+
1047
+ const char *plugin_section_include = "![[sections#Section 2]]";
1048
+ html = apex_markdown_to_html(plugin_section_include, strlen(plugin_section_include), &plugin_opts);
1049
+ assert_contains(html, "Plugin Section 2", "Section include resolves before plugin rewrites section heading");
1050
+ assert_contains(html, "Nested section 2.1 text.", "Section include with plugin keeps nested subsection");
1051
+ assert_not_contains(html, "Section 3 text.", "Section include with plugin still stops at next same-level heading");
1052
+ apex_free_string(html);
1053
+
569
1054
  /* Test MMD wildcard transclusion: file.* (legacy behavior) */
570
1055
  html = apex_markdown_to_html("Include: {{simple.*}}", 22, &opts);
571
1056
  assert_contains(html, "Included Content", "MMD wildcard file.* resolves to simple.md");
@@ -697,6 +1182,63 @@ void test_file_includes(void) {
697
1182
  assert_not_contains(html, "Included Content", "Line range excludes header");
698
1183
  apex_free_string(html);
699
1184
 
1185
+ /* Section extraction parity across include syntaxes */
1186
+ const char *mmd_section_include = "{{sections.md#Section 2}}";
1187
+ html = apex_markdown_to_html(mmd_section_include, strlen(mmd_section_include), &opts);
1188
+ assert_contains(html, "Section 2 text.", "MMD section include: selected section content");
1189
+ assert_contains(html, "Nested section 2.1 text.", "MMD section include: nested subsection included");
1190
+ assert_not_contains(html, "Section 3 text.", "MMD section include: stops before next same-level heading");
1191
+ apex_free_string(html);
1192
+
1193
+ const char *marked_section_include = "<<[sections.md#Section 2]";
1194
+ html = apex_markdown_to_html(marked_section_include, strlen(marked_section_include), &opts);
1195
+ assert_contains(html, "Section 2 text.", "Marked section include: selected section content");
1196
+ assert_contains(html, "Nested section 2.1 text.", "Marked section include: nested subsection included");
1197
+ assert_not_contains(html, "Section 3 text.", "Marked section include: stops before next same-level heading");
1198
+ apex_free_string(html);
1199
+
1200
+ const char *ia_section_include = "/sections.md#Section 2";
1201
+ html = apex_markdown_to_html(ia_section_include, strlen(ia_section_include), &opts);
1202
+ assert_contains(html, "Section 2 text.", "iA Writer section include: selected section content");
1203
+ assert_contains(html, "Nested section 2.1 text.", "iA Writer section include: nested subsection included");
1204
+ assert_not_contains(html, "Section 3 text.", "iA Writer section include: stops before next same-level heading");
1205
+ apex_free_string(html);
1206
+
1207
+ const char *obsidian_section_include = "![[sections#Section 2]]";
1208
+ html = apex_markdown_to_html(obsidian_section_include, strlen(obsidian_section_include), &opts);
1209
+ assert_contains(html, "Section 2 text.", "Obsidian embed include: selected section content");
1210
+ assert_contains(html, "Nested section 2.1 text.", "Obsidian embed include: nested subsection included");
1211
+ assert_not_contains(html, "Section 3 text.", "Obsidian embed include: stops before next same-level heading");
1212
+ apex_free_string(html);
1213
+
1214
+ opts.wikilink_extension = "txt";
1215
+ html = apex_markdown_to_html(obsidian_section_include, strlen(obsidian_section_include), &opts);
1216
+ assert_contains(html, "Section 2 text from TXT fixture.", "Obsidian embed include: uses --wikilink-extension default");
1217
+ assert_not_contains(html, "Nested section 2.1 text.", "Obsidian embed include: extension override changes source file");
1218
+ assert_not_contains(html, "Section 3 text from TXT fixture.", "Obsidian embed include: extension override still trims to section");
1219
+ apex_free_string(html);
1220
+ opts.wikilink_extension = ".txt";
1221
+ html = apex_markdown_to_html(obsidian_section_include, strlen(obsidian_section_include), &opts);
1222
+ assert_contains(html, "Section 2 text from TXT fixture.", "Obsidian embed include: dot-prefixed wikilink extension works");
1223
+ apex_free_string(html);
1224
+ opts.wikilink_extension = "html";
1225
+ html = apex_markdown_to_html(obsidian_section_include, strlen(obsidian_section_include), &opts);
1226
+ assert_contains(html, "Section 2 text.", "Obsidian embed include: falls back to .md when configured extension file is missing");
1227
+ apex_free_string(html);
1228
+ opts.wikilink_extension = NULL;
1229
+
1230
+ const char *missing_section_obsidian = "![[sections#Does Not Exist]]";
1231
+ html = apex_markdown_to_html(missing_section_obsidian, strlen(missing_section_obsidian), &opts);
1232
+ assert_contains(html, "Intro paragraph.", "Obsidian embed include: missing section falls back to full document");
1233
+ assert_contains(html, "Section 3 text.", "Obsidian embed include: full document fallback includes later sections");
1234
+ apex_free_string(html);
1235
+
1236
+ const char *missing_section_mmd = "{{sections.md#Does Not Exist}}";
1237
+ html = apex_markdown_to_html(missing_section_mmd, strlen(missing_section_mmd), &opts);
1238
+ assert_contains(html, "Intro paragraph.", "MMD include: missing section falls back to full document");
1239
+ assert_contains(html, "Section 3 text.", "MMD include: full document fallback includes later sections");
1240
+ apex_free_string(html);
1241
+
700
1242
  /* Test Marked code include with address syntax */
701
1243
  html = apex_markdown_to_html("<<(code.py)[1,3]", 18, &opts);
702
1244
  assert_contains(html, "def hello()", "Code include with line range");
@@ -711,7 +1253,7 @@ void test_file_includes(void) {
711
1253
  assert_not_contains(html, "Included Content", "Regex range excludes before first match");
712
1254
  apex_free_string(html);
713
1255
 
714
- /* Verify iA Writer syntax is NOT affected (no address syntax) */
1256
+ /* iA Writer plain include still includes full file when no section is requested */
715
1257
  html = apex_markdown_to_html("/code.py", 8, &opts);
716
1258
  assert_contains(html, "def hello()", "iA Writer syntax unchanged");
717
1259
  assert_contains(html, "return True", "iA Writer includes full file");
@@ -927,6 +1469,107 @@ void test_definition_lists(void) {
927
1469
  assert_not_contains(html, "<dt>term</dt>", "No definition list from multi-line inline code");
928
1470
  apex_free_string(html);
929
1471
 
1472
+ /* Leanpub {::pagebreak /} must not trigger definition list reordering */
1473
+ const char *heading_pagebreak = "## Page breaks\n {::pagebreak /}";
1474
+ html = apex_markdown_to_html(heading_pagebreak, strlen(heading_pagebreak), &opts);
1475
+ assert_contains(html, "<h2", "Heading before indented Leanpub pagebreak marker");
1476
+ assert_contains(html, "Page", "Heading text preserved");
1477
+ assert_contains(html, "<pre><code>{::pagebreak /}", "Leanpub pagebreak marker in code block");
1478
+ assert_not_contains(html, "<dl>", "Leanpub pagebreak marker did not create definition list");
1479
+ const char *h2_pos = strstr(html, "<h2");
1480
+ const char *pre_pos = strstr(html, "<pre>");
1481
+ if (h2_pos && pre_pos && h2_pos < pre_pos) {
1482
+ test_result(true, "Heading appears before code block with Leanpub pagebreak marker");
1483
+ } else {
1484
+ test_result(false, "Heading should appear before code block with Leanpub pagebreak marker");
1485
+ }
1486
+ apex_free_string(html);
1487
+
1488
+ /* Buffered term before fenced code block must not be discarded */
1489
+ const char *term_before_code =
1490
+ "You can now use these environment variables in your Run Command actions:\n\n"
1491
+ "```bash\n"
1492
+ "echo hello\n"
1493
+ "```";
1494
+ html = apex_markdown_to_html(term_before_code, strlen(term_before_code), &opts);
1495
+ assert_contains(html, "Run Command actions", "Intro paragraph preserved before code block");
1496
+ {
1497
+ const char *intro_pos = strstr(html, "Run Command actions");
1498
+ const char *code_pos = strstr(html, "<pre");
1499
+ test_result(intro_pos && code_pos && intro_pos < code_pos,
1500
+ "Intro paragraph appears before fenced code block");
1501
+ }
1502
+ apex_free_string(html);
1503
+
1504
+ /* Fence immediately after definition list must become a real code block */
1505
+ {
1506
+ const char *dl_then_fence =
1507
+ "Term\n"
1508
+ ": Def\n"
1509
+ "\n"
1510
+ "```\n"
1511
+ "code\n"
1512
+ "```\n";
1513
+ html = apex_markdown_to_html(dl_then_fence, strlen(dl_then_fence), &opts);
1514
+ assert_contains(html, "<dl>", "DL before bare fence still renders");
1515
+ assert_contains(html, "<pre", "Bare fence after DL becomes pre/code");
1516
+ assert_contains(html, "<code", "Bare fence after DL contains code element");
1517
+ assert_not_contains(html, "</dl>\n```", "Fence after DL is not left as raw backticks");
1518
+ apex_free_string(html);
1519
+ }
1520
+
1521
+ /* Paragraph between DL and fence stays before the fence */
1522
+ {
1523
+ const char *dl_para_fence =
1524
+ "Term\n"
1525
+ ": Def\n"
1526
+ "\n"
1527
+ "This should stay before the diagram\n"
1528
+ "\n"
1529
+ "```mermaid\n"
1530
+ "graph TD\n"
1531
+ " A --> B\n"
1532
+ "```\n";
1533
+ html = apex_markdown_to_html(dl_para_fence, strlen(dl_para_fence), &opts);
1534
+ const char *para = strstr(html, "This should stay before the diagram");
1535
+ const char *pre = strstr(html, "<pre");
1536
+ test_result(para && pre && para < pre,
1537
+ "Paragraph between DL and fence stays before pre");
1538
+ apex_free_string(html);
1539
+ }
1540
+
1541
+ /* DL + callout + following paragraph: blanks must not be eaten */
1542
+ {
1543
+ apex_options callout_opts = apex_options_default();
1544
+ callout_opts.mode = APEX_MODE_UNIFIED;
1545
+ callout_opts.enable_callouts = true;
1546
+ callout_opts.enable_definition_lists = true;
1547
+ const char *dl_callout =
1548
+ "Term\n"
1549
+ ": Def\n"
1550
+ "\n"
1551
+ "> [!Note]\n"
1552
+ "> note body\n"
1553
+ "\n"
1554
+ "After callout\n";
1555
+ html = apex_markdown_to_html(dl_callout, strlen(dl_callout), &callout_opts);
1556
+ assert_contains(html, "callout", "Callout after DL still renders");
1557
+ assert_contains(html, "After callout", "Paragraph after callout preserved");
1558
+ /* "After callout" must not sit inside the callout content blockquote */
1559
+ const char *after = strstr(html, "After callout");
1560
+ const char *callout_end = strstr(html, "</div>\n</div>");
1561
+ if (!callout_end) callout_end = strstr(html, "callout-content");
1562
+ test_result(after != NULL, "After callout text present in output");
1563
+ if (after && strstr(html, "callout-content")) {
1564
+ /* Find the callout-content section and ensure After is outside it */
1565
+ const char *content = strstr(html, "callout-content");
1566
+ const char *content_close = content ? strstr(content, "</div>") : NULL;
1567
+ test_result(content_close && after > content_close,
1568
+ "After callout is outside callout-content");
1569
+ }
1570
+ apex_free_string(html);
1571
+ }
1572
+
930
1573
  bool had_failures = suite_end(suite_failures);
931
1574
  print_suite_title("Definition Lists Tests", had_failures, false);
932
1575
  }
@@ -994,6 +1637,68 @@ void test_callouts(void) {
994
1637
  }
995
1638
  apex_free_string(html);
996
1639
 
1640
+ /* Python-Markdown callouts are disabled by default */
1641
+ const char *py_callout = "!!! note \"Py Title\"\n Py body line";
1642
+ html = apex_markdown_to_html(py_callout, strlen(py_callout), &opts);
1643
+ assert_not_contains(html, "class=\"callout", "Python callout syntax ignored when flag disabled");
1644
+ apex_free_string(html);
1645
+
1646
+ /* Python-Markdown callouts enabled behind flag */
1647
+ opts.enable_py_callouts = true;
1648
+ html = apex_markdown_to_html(py_callout, strlen(py_callout), &opts);
1649
+ assert_contains(html, "class=\"callout", "Python callout recognized when enabled");
1650
+ assert_contains(html, "callout-note", "Python note callout type");
1651
+ assert_contains(html, "Py Title", "Python callout title preserved");
1652
+ apex_free_string(html);
1653
+ opts.enable_py_callouts = false;
1654
+
1655
+ /* markdown-callouts NOTE: syntax enabled behind py flag */
1656
+ opts.enable_py_callouts = true;
1657
+ html = apex_markdown_to_html("NOTE: Inline py callout body", 28, &opts);
1658
+ assert_contains(html, "class=\"callout", "NOTE: syntax recognized with py-callouts");
1659
+ assert_contains(html, "callout-note", "NOTE: maps to note callout");
1660
+ assert_contains(html, "Inline py callout body", "NOTE: body preserved");
1661
+ apex_free_string(html);
1662
+
1663
+ /* markdown-callouts collapsed syntax */
1664
+ const char *collapsed_py = ">? NOTE: Collapsed note\n> Collapsed body";
1665
+ html = apex_markdown_to_html(collapsed_py, strlen(collapsed_py), &opts);
1666
+ assert_contains(html, "<details", ">? syntax creates collapsible callout");
1667
+ assert_contains(html, "callout-note", ">? NOTE maps to note callout");
1668
+ assert_contains(html, "Collapsed body", ">? NOTE body preserved");
1669
+ apex_free_string(html);
1670
+
1671
+ opts.enable_py_callouts = false;
1672
+
1673
+ /* Quarto callouts are disabled by default */
1674
+ const char *quarto_callout =
1675
+ "::: {.callout-warning}\n"
1676
+ "## Quarto Title\n"
1677
+ "Quarto warning body.\n"
1678
+ ":::\n";
1679
+ html = apex_markdown_to_html(quarto_callout, strlen(quarto_callout), &opts);
1680
+ assert_not_contains(html, "class=\"callout callout-warning\"", "Quarto callout syntax ignored when flag disabled");
1681
+ apex_free_string(html);
1682
+
1683
+ /* Quarto callouts enabled behind flag */
1684
+ opts.enable_quarto_callouts = true;
1685
+ html = apex_markdown_to_html(quarto_callout, strlen(quarto_callout), &opts);
1686
+ assert_contains(html, "class=\"callout", "Quarto callout recognized when enabled");
1687
+ assert_contains(html, "callout-warning", "Quarto warning callout type");
1688
+ assert_contains(html, "Quarto warning body", "Quarto callout body preserved");
1689
+ apex_free_string(html);
1690
+
1691
+ /* Quarto flag should still allow non-callout divs to render as divs */
1692
+ const char *regular_div =
1693
+ "::: {.sidebar}\n"
1694
+ "Regular div content.\n"
1695
+ ":::\n";
1696
+ html = apex_markdown_to_html(regular_div, strlen(regular_div), &opts);
1697
+ assert_contains(html, "<div class=\"sidebar\"", "Non-callout fenced div still rendered with Quarto mode enabled");
1698
+ assert_not_contains(html, "class=\"callout", "Non-callout fenced div not converted into callout");
1699
+ apex_free_string(html);
1700
+ opts.enable_quarto_callouts = false;
1701
+
997
1702
  bool had_failures = suite_end(suite_failures);
998
1703
  print_suite_title("Callouts Tests", had_failures, false);
999
1704
  }
@@ -1063,6 +1768,28 @@ void test_blockquote_lists(void) {
1063
1768
  /* Definition lists may or may not be parsed depending on mode */
1064
1769
  apex_free_string(html);
1065
1770
 
1771
+ /* Reference image after blockquote without blank line should not stay in blockquote */
1772
+ const char *ref_img_after_quote =
1773
+ "> Block quote\n"
1774
+ "![][2]\n"
1775
+ "\n"
1776
+ "[2]: /path/to/image.jpg\n"
1777
+ "\n"
1778
+ "Additional paragraph.";
1779
+ html = apex_markdown_to_html(ref_img_after_quote, strlen(ref_img_after_quote), &opts);
1780
+ assert_contains(html, "<blockquote>", "Blockquote present for ref image test");
1781
+ assert_contains(html, "Block quote", "Blockquote text preserved");
1782
+ assert_not_contains(html, "<blockquote>\n<p>Block quote\n<img", "Reference image not lazy-continued into blockquote");
1783
+ assert_contains(html, "<img src=\"/path/to/image.jpg\"", "Reference image resolved outside blockquote");
1784
+ assert_contains(html, "Additional paragraph.", "Paragraph after reference image preserved");
1785
+ apex_free_string(html);
1786
+
1787
+ /* Inline image after blockquote without blank line */
1788
+ html = apex_markdown_to_html("> Block quote\n![alt](test.png)\n\nNext paragraph.", 52, &opts);
1789
+ assert_not_contains(html, "<blockquote>\n<p>Block quote\n<", "Inline image not inside blockquote paragraph");
1790
+ assert_contains(html, "test.png", "Inline image rendered outside blockquote");
1791
+ apex_free_string(html);
1792
+
1066
1793
  bool had_failures = suite_end(suite_failures);
1067
1794
  print_suite_title("Blockquote Lists Tests", had_failures, false);
1068
1795
  }
@@ -1120,6 +1847,31 @@ void test_html_markdown_attributes(void) {
1120
1847
  assert_contains(html, "<div>", "HTML preserved without markdown attribute");
1121
1848
  apex_free_string(html);
1122
1849
 
1850
+ /* Reference-style links in markdown="1" blocks resolve definitions from full document */
1851
+ const char *ref_in_block =
1852
+ "<blockquote class=\"tip\" markdown=\"1\">\n"
1853
+ "See the [extension page][ext] for details.\n"
1854
+ "</blockquote>\n\n"
1855
+ "[ext]: https://example.com/ext \"Extension page\"";
1856
+ html = apex_markdown_to_html(ref_in_block, strlen(ref_in_block), &opts);
1857
+ assert_contains(html, "<a href=\"https://example.com/ext\"", "markdown=\"1\" resolves reference-style links");
1858
+ assert_contains(html, "extension page</a>", "markdown=\"1\" reference link text preserved");
1859
+ apex_free_string(html);
1860
+
1861
+ /* Reference footnotes in markdown="1" blocks resolve definitions from full document */
1862
+ const char *fn_in_block =
1863
+ "<blockquote class=\"tip\" markdown=\"1\">\n"
1864
+ "See footnote[^note] here.\n"
1865
+ "</blockquote>\n\n"
1866
+ "[^note]: Footnote text.";
1867
+ html = apex_markdown_to_html(fn_in_block, strlen(fn_in_block), &opts);
1868
+ assert_contains(html, "class=\"footnote-ref\"", "markdown=\"1\" resolves reference footnotes");
1869
+ assert_contains(html, "href=\"#fn-note\"", "markdown=\"1\" footnote links to block anchor");
1870
+ assert_contains(html, "<section class=\"footnotes\"", "markdown=\"1\" footnotes section rendered in block");
1871
+ assert_contains(html, "Footnote text.", "markdown=\"1\" footnote definition rendered in block");
1872
+ assert_not_contains(html, "See footnote[^note]", "markdown=\"1\" footnote not left literal");
1873
+ apex_free_string(html);
1874
+
1123
1875
  bool had_failures = suite_end(suite_failures);
1124
1876
  print_suite_title("HTML Markdown Attributes Tests", had_failures, false);
1125
1877
  }
@@ -1506,6 +2258,37 @@ void test_abbreviations(void) {
1506
2258
  assert_contains(html, "<abbr title=\"abbreviation syntax\">ABBR</abbr>", "Inline abbr when mixed");
1507
2259
  apex_free_string(html);
1508
2260
 
2261
+ /* Regression for issue #31: preprocessing must preserve physical lines
2262
+ * longer than the old 1024-byte abbreviation and 2048-byte ALD buffers. */
2263
+ const char *tail_marker = "END-OF-LONG-LINE";
2264
+ const size_t prefix_len = 2160;
2265
+ size_t marker_len = strlen(tail_marker);
2266
+ char *long_line = malloc(prefix_len + marker_len + 2);
2267
+ if (long_line) {
2268
+ memset(long_line, 'a', prefix_len);
2269
+ memcpy(long_line + prefix_len, tail_marker, marker_len);
2270
+ long_line[prefix_len + marker_len] = '\n';
2271
+ long_line[prefix_len + marker_len + 1] = '\0';
2272
+
2273
+ const apex_mode_t modes[] = {
2274
+ APEX_MODE_MULTIMARKDOWN,
2275
+ APEX_MODE_KRAMDOWN,
2276
+ APEX_MODE_UNIFIED,
2277
+ APEX_MODE_QUARTO
2278
+ };
2279
+ const char *mode_names[] = {"MMD", "Kramdown", "Unified", "Quarto"};
2280
+ for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
2281
+ apex_options long_opts = apex_options_for_mode(modes[i]);
2282
+ html = apex_markdown_to_html(long_line, strlen(long_line), &long_opts);
2283
+ test_resultf(html && strstr(html, tail_marker) != NULL,
2284
+ "%s mode preserves lines longer than 2048 bytes", mode_names[i]);
2285
+ apex_free_string(html);
2286
+ }
2287
+ free(long_line);
2288
+ } else {
2289
+ test_result(false, "Allocate issue #31 long-line regression input");
2290
+ }
2291
+
1509
2292
  bool had_failures = suite_end(suite_failures);
1510
2293
  print_suite_title("Abbreviations Tests", had_failures, false);
1511
2294
  }
@@ -1746,6 +2529,26 @@ void test_special_markers(void) {
1746
2529
  assert_contains(html, "Section 3", "Last section");
1747
2530
  apex_free_string(html);
1748
2531
 
2532
+ /* Special markers NOT converted inside inline code span */
2533
+ html = apex_markdown_to_html("`<!--PAUSE:15-->`", 17, &opts);
2534
+ assert_contains(html, "<code>&lt;!--PAUSE:15--&gt;</code>", "Pause marker preserved in inline code");
2535
+ assert_not_contains(html, "data-pause", "Pause marker not rendered in inline code");
2536
+ apex_free_string(html);
2537
+
2538
+ /* Special markers NOT converted inside indented code block */
2539
+ const char *indented_break = " <!--BREAK-->";
2540
+ html = apex_markdown_to_html(indented_break, strlen(indented_break), &opts);
2541
+ assert_contains(html, "&lt;!--BREAK--&gt;", "Break marker preserved in indented code");
2542
+ assert_not_contains(html, "page-break-after", "Break marker not rendered in indented code");
2543
+ apex_free_string(html);
2544
+
2545
+ /* Special markers NOT converted inside fenced code block */
2546
+ const char *fenced = "```\n<!--BREAK-->\n```";
2547
+ html = apex_markdown_to_html(fenced, strlen(fenced), &opts);
2548
+ assert_contains(html, "&lt;!--BREAK--&gt;", "Break marker preserved in fenced code");
2549
+ assert_not_contains(html, "page-break-after", "Break marker not rendered in fenced code");
2550
+ apex_free_string(html);
2551
+
1749
2552
  bool had_failures = suite_end(suite_failures);
1750
2553
  print_suite_title("Special Markers Tests", had_failures, false);
1751
2554
  }
@@ -2265,12 +3068,29 @@ void test_mixed_lists(void) {
2265
3068
  html = apex_markdown_to_html(alpha_with_nested_alpha, strlen(alpha_with_nested_alpha), &unified_opts);
2266
3069
  assert_contains(html, "<ol style=\"list-style-type: lower-alpha\">", "Alpha list keeps style with nested alpha sublist");
2267
3070
  assert_not_contains(html, "[apex-alpha-list:", "No leaked alpha marker token after nested alpha sublist");
3071
+ assert_not_contains(html, "<p>Test</p>\n<ol>", "Synthetic nested alpha sublist stays tight without paragraph wrapper");
3072
+ apex_free_string(html);
3073
+
3074
+ /* Numeric parent list with indented alpha sublist → nested <ol style="lower-alpha"> */
3075
+ const char *numeric_with_nested_alpha = "1. One\n2. Two\n\ta. Two-One\n\tb. Two-Two\n3. Three\n";
3076
+ html = apex_markdown_to_html(numeric_with_nested_alpha, strlen(numeric_with_nested_alpha), &unified_opts);
3077
+ assert_contains(html, "<ol style=\"list-style-type: lower-alpha\">",
3078
+ "Alpha sublist under numeric list gets lower-alpha styling");
3079
+ assert_contains(html, "<li>Two-One</li>", "Nested alpha item text preserved");
3080
+ assert_not_contains(html, "<!-- apex-alpha-list-", "Alpha HTML comment marker stripped from output");
2268
3081
  apex_free_string(html);
2269
3082
 
2270
3083
  const char *numeric_with_nested_ordered = "1. Test\n2. Test\n\t3. Test\n\t4. Test\n5. Test\n";
2271
3084
  html = apex_markdown_to_html(numeric_with_nested_ordered, strlen(numeric_with_nested_ordered), &unified_opts);
2272
3085
  assert_contains(html, "<ol start=\"3\">", "Numeric nested ordered sublist renders as nested ordered list");
2273
3086
  assert_not_contains(html, "2. Test\n 3. Test", "Nested ordered items are not flattened into parent text");
3087
+ assert_not_contains(html, "<p>Test</p>\n<ol start=\"3\">", "Synthetic nested ordered list stays tight without paragraph wrapper");
3088
+ apex_free_string(html);
3089
+
3090
+ const char *numeric_with_explicit_loose_nested_ordered = "1. Test\n2. Test\n\n\t3. Test\n\t4. Test\n5. Test\n";
3091
+ html = apex_markdown_to_html(numeric_with_explicit_loose_nested_ordered, strlen(numeric_with_explicit_loose_nested_ordered), &unified_opts);
3092
+ assert_contains(html, "<ol start=\"3\">", "Explicit blank line nested ordered sublist renders");
3093
+ assert_contains(html, "<p>Test</p>\n<ol start=\"3\">", "Explicit blank line keeps loose list paragraph wrapper");
2274
3094
  apex_free_string(html);
2275
3095
 
2276
3096
  bool had_failures = suite_end(suite_failures);