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
@@ -18,6 +18,105 @@ void test_toc(void) {
18
18
  opts.enable_marked_extensions = true;
19
19
  char *html;
20
20
 
21
+ /* -t toc: Markdown list, default depth 1-3 */
22
+ apex_options toc_opts = apex_options_default();
23
+ toc_opts.output_format = APEX_OUTPUT_TOC;
24
+ const char *doc =
25
+ "# Introduction\n\n"
26
+ "## Getting Started\n\n"
27
+ "### Installation\n\n"
28
+ "#### Too Deep\n\n"
29
+ "## Configuration\n\n"
30
+ "# API\n"
31
+ "{: .no_toc}\n";
32
+ char *md = apex_markdown_to_html(doc, strlen(doc), &toc_opts);
33
+ assert_contains(md, "- [Introduction](#introduction)\n", "TOC md has H1");
34
+ assert_contains(md, " - [Getting Started](#getting-started)\n", "TOC md indents H2");
35
+ assert_contains(md, " - [Installation](#installation)\n", "TOC md indents H3");
36
+ assert_not_contains(md, "Too Deep", "TOC md excludes H4 by default");
37
+ assert_not_contains(md, "API", "TOC md excludes no_toc heading");
38
+ assert_not_contains(md, "<nav", "TOC md is not HTML");
39
+ apex_free_string(md);
40
+
41
+ toc_opts.toc_min = 2;
42
+ toc_opts.toc_max = 4;
43
+ md = apex_markdown_to_html(doc, strlen(doc), &toc_opts);
44
+ assert_not_contains(md, "Introduction", "toc-min-max excludes H1");
45
+ assert_contains(md, "Too Deep", "toc-min-max includes H4");
46
+ apex_free_string(md);
47
+
48
+ /* id-format mmd */
49
+ toc_opts = apex_options_default();
50
+ toc_opts.output_format = APEX_OUTPUT_TOC;
51
+ toc_opts.id_format = 1; /* MMD */
52
+ md = apex_markdown_to_html("# Hello World\n", 14, &toc_opts);
53
+ assert_contains(md, "- [Hello World](#helloworld)\n", "TOC md respects MMD id-format");
54
+ apex_free_string(md);
55
+
56
+ /* manual ID */
57
+ toc_opts = apex_options_default();
58
+ toc_opts.output_format = APEX_OUTPUT_TOC;
59
+ md = apex_markdown_to_html("# Custom {#my-custom}\n", 22, &toc_opts);
60
+ assert_contains(md, "- [Custom](#my-custom)\n", "TOC md uses manual heading ID");
61
+ apex_free_string(md);
62
+
63
+ /* id-format kramdown */
64
+ toc_opts = apex_options_default();
65
+ toc_opts.output_format = APEX_OUTPUT_TOC;
66
+ toc_opts.id_format = 2; /* Kramdown */
67
+ md = apex_markdown_to_html("# Trailing Dash-\n", 17, &toc_opts);
68
+ assert_contains(md, "- [Trailing Dash-](#trailing-dash-)\n", "TOC md respects Kramdown id-format");
69
+ apex_free_string(md);
70
+
71
+ /* empty */
72
+ toc_opts = apex_options_default();
73
+ toc_opts.output_format = APEX_OUTPUT_TOC;
74
+ md = apex_markdown_to_html("No headings here.\n", 18, &toc_opts);
75
+ if (md && (md[0] == '\0' || strcmp(md, "\n") == 0)) {
76
+ test_result(true, "TOC md empty when no headings");
77
+ } else {
78
+ test_result(false, "TOC md empty when no headings");
79
+ }
80
+ apex_free_string(md);
81
+
82
+ /* Bare {{TOC}} should use option defaults (1-3): exclude H4 */
83
+ const char *bare_default_depth =
84
+ "# H1\n\n{{TOC}}\n\n## H2\n\n### H3\n\n#### H4";
85
+ html = apex_markdown_to_html(bare_default_depth, strlen(bare_default_depth), &opts);
86
+ assert_contains(html, "href=\"#h1\"", "Bare TOC includes H1 by default");
87
+ assert_contains(html, "href=\"#h3\"", "Bare TOC includes H3 by default");
88
+ assert_not_contains(html, "href=\"#h4\"", "Bare TOC excludes H4 by default (max 3)");
89
+ apex_free_string(html);
90
+
91
+ /* --toc-min-max via options: 2,4 */
92
+ apex_options depth_opts = opts;
93
+ depth_opts.toc_min = 2;
94
+ depth_opts.toc_max = 4;
95
+ html = apex_markdown_to_html(bare_default_depth, strlen(bare_default_depth), &depth_opts);
96
+ assert_not_contains(html, "href=\"#h1\"", "toc_min=2 excludes H1");
97
+ assert_contains(html, "href=\"#h4\"", "toc_max=4 includes H4");
98
+ apex_free_string(html);
99
+
100
+ /* Marker override wins over options */
101
+ const char *override_toc =
102
+ "# H1\n\n{{TOC:1-6}}\n\n## H2\n\n### H3\n\n#### H4\n\n##### H5";
103
+ html = apex_markdown_to_html(override_toc, strlen(override_toc), &depth_opts);
104
+ assert_contains(html, "href=\"#h1\"", "Marker range overrides toc_min");
105
+ assert_contains(html, "href=\"#h5\"", "Marker range overrides toc_max");
106
+ apex_free_string(html);
107
+
108
+ /* Partial marker max under custom toc_min/toc_max: unspecified min is 1, not option min */
109
+ apex_options partial_opts = opts;
110
+ partial_opts.toc_min = 2;
111
+ partial_opts.toc_max = 4;
112
+ const char *partial_max =
113
+ "# H1\n\n<!--TOC max5-->\n\n## H2\n\n### H3\n\n#### H4\n\n##### H5\n\n###### H6";
114
+ html = apex_markdown_to_html(partial_max, strlen(partial_max), &partial_opts);
115
+ assert_contains(html, "href=\"#h1\"", "Partial max marker uses min=1 not option toc_min");
116
+ assert_contains(html, "href=\"#h5\"", "Partial max marker includes H5");
117
+ assert_not_contains(html, "href=\"#h6\"", "Partial max marker excludes H6");
118
+ apex_free_string(html);
119
+
21
120
  /* Test basic TOC marker */
22
121
  const char *doc_with_toc = "# Header 1\n\n<!--TOC-->\n\n## Header 2\n\n### Header 3";
23
122
  html = apex_markdown_to_html(doc_with_toc, strlen(doc_with_toc), &opts);
@@ -56,6 +155,36 @@ void test_toc(void) {
56
155
  }
57
156
  apex_free_string(html);
58
157
 
158
+ /* Regression: {{TOC:2}} must not parse beyond marker into later IDs */
159
+ const char *single_depth_toc =
160
+ "# Table of Contents Test\n\n{{TOC:2}}\n\nLorem ipsum\n\n## Section 1\n\n## Section 2\n\n## Section 3";
161
+ html = apex_markdown_to_html(single_depth_toc, strlen(single_depth_toc), &opts);
162
+ assert_contains(html, "<nav class=\"toc\">", "Single depth TOC nav rendered");
163
+ assert_contains(html, "href=\"#section-1\"", "Single depth TOC includes Section 1");
164
+ assert_contains(html, "href=\"#section-2\"", "Single depth TOC includes Section 2");
165
+ assert_contains(html, "href=\"#section-3\"", "Single depth TOC includes Section 3");
166
+ apex_free_string(html);
167
+
168
+ /* Regression: same-level headings in {{TOC:2-6}} should remain siblings */
169
+ const char *same_level_range_toc =
170
+ "# Table of Contents Test\n\n{{TOC:2-6}}\n\nLorem ipsum\n\n## Section 1\n\n## Section 2\n\n## Section 3";
171
+ html = apex_markdown_to_html(same_level_range_toc, strlen(same_level_range_toc), &opts);
172
+ assert_contains(html, "href=\"#section-1\"", "Range TOC includes Section 1");
173
+ assert_contains(html, "href=\"#section-2\"", "Range TOC includes Section 2");
174
+ assert_contains(html, "href=\"#section-3\"", "Range TOC includes Section 3");
175
+ assert_not_contains(html, "href=\"#section-1\">Section 1</a>\n <ul>",
176
+ "Range TOC does not nest same-level headings");
177
+ apex_free_string(html);
178
+
179
+ /* Regression: TOC labels should not include source indentation/newline whitespace */
180
+ const char *toc_whitespace =
181
+ "# Top\n\n{{TOC:2}}\n\n## Section 1\n\n##\tSection 2\n\n## Section 3";
182
+ html = apex_markdown_to_html(toc_whitespace, strlen(toc_whitespace), &opts);
183
+ assert_contains(html, "href=\"#section-1\">Section 1</a>", "TOC trims label whitespace");
184
+ assert_contains(html, "href=\"#section-2\">Section 2</a>", "TOC normalizes tab/newline whitespace");
185
+ assert_contains(html, "href=\"#section-3\">Section 3</a>", "TOC collapses internal whitespace runs");
186
+ apex_free_string(html);
187
+
59
188
  /* Test TOC with max depth only */
60
189
  const char *max_toc = "# H1\n\n<!--TOC max2-->\n\n## H2\n\n### H3";
61
190
  html = apex_markdown_to_html(max_toc, strlen(max_toc), &opts);
@@ -86,6 +215,52 @@ void test_toc(void) {
86
215
  assert_contains(html, "Section", "TOC includes section");
87
216
  apex_free_string(html);
88
217
 
218
+ /* Escaped MMD TOC markers must not be expanded */
219
+ const char *escaped_toc =
220
+ "# Title\n\nMarked also recognizes MultiMarkdown-style \\{\\{TOC\\}\\}, "
221
+ "and Pandoc-style `{{TOC:2-6}}`.\n\n## Section";
222
+ html = apex_markdown_to_html(escaped_toc, strlen(escaped_toc), &opts);
223
+ assert_contains(html, "{{TOC}}", "Escaped MMD TOC marker preserved as literal text");
224
+ assert_not_contains(html, "<nav class=\"toc\">", "Escaped MMD TOC marker not expanded");
225
+ assert_contains(html, "<code>{{TOC:2-6}}</code>", "Pandoc-style TOC marker preserved in inline code");
226
+ apex_free_string(html);
227
+
228
+ /* Kramdown {:toc} inside indented code must not be converted */
229
+ apex_options kram_opts2 = apex_options_for_mode(APEX_MODE_KRAMDOWN);
230
+ kram_opts2.enable_marked_extensions = true;
231
+ const char *kramdown_toc_code = "# Title\n\n {:toc}\n\n## Section";
232
+ html = apex_markdown_to_html(kramdown_toc_code, strlen(kramdown_toc_code), &kram_opts2);
233
+ assert_contains(html, "{:toc}", "Kramdown {:toc} preserved in indented code");
234
+ assert_not_contains(html, "<nav class=\"toc\">", "Kramdown {:toc} not expanded in indented code");
235
+ apex_free_string(html);
236
+
237
+ /* Pandoc-style {{TOC:2-6}} inside fenced code must not be rendered */
238
+ const char *toc_range_fenced = "# Title\n\n```\n{{TOC:2-6}}\n```\n\n## Section\n\n### Sub";
239
+ html = apex_markdown_to_html(toc_range_fenced, strlen(toc_range_fenced), &opts);
240
+ assert_contains(html, "{{TOC:2-6}}", "Pandoc-style TOC marker preserved in fenced code");
241
+ assert_not_contains(html, "<nav class=\"toc\">", "Pandoc-style TOC marker not expanded in fenced code");
242
+ apex_free_string(html);
243
+
244
+ /* Pandoc-style {{TOC:2-6}} inside indented code must not be rendered */
245
+ const char *toc_range_indented = "# Title\n\n {{TOC:2-6}}\n\n## Section\n\n### Sub";
246
+ html = apex_markdown_to_html(toc_range_indented, strlen(toc_range_indented), &opts);
247
+ assert_contains(html, "{{TOC:2-6}}", "Pandoc-style TOC marker preserved in indented code");
248
+ assert_not_contains(html, "<nav class=\"toc\">", "Pandoc-style TOC marker not expanded in indented code");
249
+ apex_free_string(html);
250
+
251
+ /* HTML <!--TOC--> inside inline code must not be rendered */
252
+ html = apex_markdown_to_html("# Title\n\nUse `<!--TOC-->` here.\n\n## Section", 44, &opts);
253
+ assert_contains(html, "<code>&lt;!--TOC--&gt;</code>", "HTML TOC marker preserved in inline code");
254
+ assert_not_contains(html, "<nav class=\"toc\">", "HTML TOC marker not expanded in inline code");
255
+ apex_free_string(html);
256
+
257
+ /* Kramdown {:toc} inside fenced code must not be converted */
258
+ const char *kramdown_toc_fenced = "# Title\n\n```\n{:toc}\n```\n\n## Section";
259
+ html = apex_markdown_to_html(kramdown_toc_fenced, strlen(kramdown_toc_fenced), &kram_opts2);
260
+ assert_contains(html, "{:toc}", "Kramdown {:toc} preserved in fenced code");
261
+ assert_not_contains(html, "<nav class=\"toc\">", "Kramdown {:toc} not expanded in fenced code");
262
+ apex_free_string(html);
263
+
89
264
  /* Test document without TOC marker */
90
265
  const char *no_toc = "# Header\n\nContent";
91
266
  html = apex_markdown_to_html(no_toc, strlen(no_toc), &opts);
@@ -165,6 +340,58 @@ void test_toc(void) {
165
340
  }
166
341
  apex_free_string(html);
167
342
 
343
+ /* Manual {#id} must appear in TOC hrefs */
344
+ const char *manual_id_toc =
345
+ "# Custom {#my-custom}\n\n{{TOC}}\n\n## Other";
346
+ html = apex_markdown_to_html(manual_id_toc, strlen(manual_id_toc), &opts);
347
+ assert_contains(html, "href=\"#my-custom\"", "TOC uses manual heading ID");
348
+ apex_free_string(html);
349
+
350
+ /* Structured TOC entries API for library / Swift consumers */
351
+ {
352
+ const char *entries_doc =
353
+ "# Introduction\n\n"
354
+ "## Getting Started\n\n"
355
+ "### Installation\n\n"
356
+ "#### Too Deep\n\n"
357
+ "# Skip Me\n"
358
+ "{: .no_toc}\n";
359
+ size_t count = 0;
360
+ apex_toc_entry *entries = apex_markdown_to_toc_entries(
361
+ entries_doc, strlen(entries_doc), NULL, &count);
362
+ assert_option_bool(count == 3, true, "toc entries default depth count is 3");
363
+ if (entries && count >= 3) {
364
+ assert_option_bool(entries[0].level == 1, true, "toc entries[0] level is 1");
365
+ assert_option_string(entries[0].text, "Introduction", "toc entries[0] text");
366
+ assert_option_string(entries[0].id, "introduction", "toc entries[0] id");
367
+ assert_option_bool(entries[1].level == 2, true, "toc entries[1] level is 2");
368
+ assert_option_string(entries[1].id, "getting-started", "toc entries[1] id");
369
+ assert_option_bool(entries[2].level == 3, true, "toc entries[2] level is 3");
370
+ assert_option_string(entries[2].id, "installation", "toc entries[2] id");
371
+ } else {
372
+ test_result(false, "toc entries array populated");
373
+ }
374
+ apex_toc_entries_free(entries, count);
375
+
376
+ apex_options depth_opts = apex_options_default();
377
+ depth_opts.toc_min = 2;
378
+ depth_opts.toc_max = 4;
379
+ entries = apex_markdown_to_toc_entries(entries_doc, strlen(entries_doc),
380
+ &depth_opts, &count);
381
+ assert_option_bool(count == 3, true, "toc entries toc_min/max count");
382
+ if (entries && count >= 1) {
383
+ assert_option_bool(entries[0].level == 2, true, "toc entries respects toc_min");
384
+ assert_option_string(entries[count - 1].text, "Too Deep",
385
+ "toc entries includes H4 when toc_max=4");
386
+ }
387
+ apex_toc_entries_free(entries, count);
388
+
389
+ entries = apex_markdown_to_toc_entries("No headings.\n", 12, NULL, &count);
390
+ assert_option_bool(count == 0, true, "toc entries empty when no headings");
391
+ assert_option_bool(entries == NULL, true, "toc entries NULL when empty");
392
+ apex_toc_entries_free(entries, count);
393
+ }
394
+
168
395
  bool had_failures = suite_end(suite_failures);
169
396
  print_suite_title("TOC Generation Tests", had_failures, false);
170
397
  }
@@ -273,6 +500,13 @@ void test_pretty_html(void) {
273
500
  assert_contains(html, "<strong>bold</strong>", "Inline elements not split");
274
501
  apex_free_string(html);
275
502
 
503
+ /* TOC links should not get indentation whitespace inside anchor text */
504
+ html = apex_markdown_to_html("# Top\n\n{{TOC:2}}\n\n## Section 1", 31, &opts);
505
+ assert_contains(html, "<a href=\"#section-1\">Section 1</a>", "Pretty mode keeps TOC link text clean");
506
+ assert_not_contains(html, "<a href=\"#section-1\"> Section 1</a>",
507
+ "Pretty mode does not pad TOC link text");
508
+ apex_free_string(html);
509
+
276
510
  /* Test table formatting */
277
511
  const char *table = "| A | B |\n|---|---|\n| C | D |";
278
512
  html = apex_markdown_to_html(table, strlen(table), &opts);
@@ -473,10 +707,118 @@ void test_terminal_output(void) {
473
707
  test_result(strstr(out, "![") == NULL, "Missing local image uses link style not markdown image");
474
708
  apex_free_string(out);
475
709
 
710
+ /* paginate_symbols: must not crash; non-TTY uses link-style images */
711
+ opts = apex_options_default();
712
+ opts.output_format = APEX_OUTPUT_TERMINAL256;
713
+ opts.paginate = true;
714
+ opts.paginate_symbols = true;
715
+ out = apex_markdown_to_html("![z](local.png)", 17, &opts);
716
+ test_result(out != NULL, "paginate_symbols terminal256 produces output");
717
+ apex_free_string(out);
718
+
476
719
  bool had_failures = suite_end(suite_failures);
477
720
  print_suite_title("Terminal Output Tests", had_failures, false);
478
721
  }
479
722
 
723
+ /**
724
+ * Test RTF output
725
+ */
726
+ void test_rtf_output(void) {
727
+ int suite_failures = suite_start();
728
+ print_suite_title("RTF Output Tests", false, true);
729
+
730
+ apex_options opts = apex_options_default();
731
+ opts.output_format = APEX_OUTPUT_RTF;
732
+ char *out;
733
+
734
+ const char *md =
735
+ "# Title\n\n"
736
+ "A paragraph with **bold**, *italic*, ~~strike~~, and `code`.\n\n"
737
+ "Visit [Example](https://example.com).\n\n"
738
+ "- Item one\n"
739
+ "- Item two\n\n"
740
+ "> A quote\n\n"
741
+ "```\ncode block\n```\n\n"
742
+ "| A | B |\n| --- | --- |\n| 1 | 2 |\n\n"
743
+ "H~2~O and E=mc^2^.\n\n"
744
+ "---\n";
745
+
746
+ out = apex_markdown_to_html(md, strlen(md), &opts);
747
+ assert_contains(out, "{\\rtf1", "RTF has document header");
748
+ assert_contains(out, "\\fonttbl", "RTF has font table");
749
+ assert_contains(out, "Title", "RTF includes heading text");
750
+ assert_contains(out, "\\b ", "RTF has bold");
751
+ assert_contains(out, "\\i ", "RTF has italic");
752
+ assert_contains(out, "\\strike", "RTF has strikethrough");
753
+ assert_contains(out, "\\f1", "RTF uses mono font for code");
754
+ assert_contains(out, "HYPERLINK", "RTF has hyperlink field");
755
+ assert_contains(out, "example.com", "RTF link URL present");
756
+ assert_contains(out, "\\bullet", "RTF has bullet list");
757
+ assert_contains(out, "\\trowd", "RTF has table row");
758
+ assert_contains(out, "code block", "RTF has code block text");
759
+ assert_contains(out, "\\super", "RTF has superscript");
760
+ assert_contains(out, "\\sub", "RTF has subscript");
761
+ test_result(out != NULL && strstr(out, "}") != NULL, "RTF document contains closing brace");
762
+ apex_free_string(out);
763
+
764
+ /* Ordered list alone so mixed-list merging cannot absorb it into a ul */
765
+ out = apex_markdown_to_html("1. First\n2. Second\n", 20, &opts);
766
+ assert_contains(out, "1. ", "RTF has ordered list marker");
767
+ assert_contains(out, "First", "RTF ordered list item text");
768
+ apex_free_string(out);
769
+
770
+ /* Footnotes endnotes */
771
+ const char *fn = "Note.[^1]\n\n[^1]: Footnote body.\n";
772
+ out = apex_markdown_to_html(fn, strlen(fn), &opts);
773
+ assert_contains(out, "{\\rtf1", "Footnote doc is RTF");
774
+ assert_contains(out, "Footnotes", "RTF endnotes section");
775
+ assert_contains(out, "Footnote body", "RTF footnote body");
776
+ apex_free_string(out);
777
+
778
+ /* Direct serializer */
779
+ out = apex_markdown_to_html("Hi", 2, &opts);
780
+ test_result(out != NULL && strncmp(out, "{\\rtf1", 6) == 0, "apex_markdown_to_html RTF path");
781
+ apex_free_string(out);
782
+
783
+ /* Remote images: alt becomes a hyperlink; no raw URL dump in body */
784
+ {
785
+ const char *remote =
786
+ "![Version: 1.1.12](https://img.shields.io/badge/Version-1.1.12-528c9e)\n";
787
+ out = apex_markdown_to_html(remote, strlen(remote), &opts);
788
+ assert_contains(out, "HYPERLINK", "Remote image uses HYPERLINK fallback");
789
+ assert_contains(out, "Version: 1.1.12", "Remote image shows alt text");
790
+ assert_contains(out, "img.shields.io", "Remote image URL in HYPERLINK field");
791
+ test_result(out != NULL && strstr(out, "] (") == NULL, "Remote image does not dump ] (url)");
792
+ test_result(out != NULL && strstr(out, "\\pict") == NULL, "Remote image is not embedded as pict");
793
+ apex_free_string(out);
794
+ }
795
+
796
+ {
797
+ const char *empty_alt = "![](https://example.com/x.png)\n";
798
+ out = apex_markdown_to_html(empty_alt, strlen(empty_alt), &opts);
799
+ assert_contains(out, "HYPERLINK", "Empty-alt remote image is still a link");
800
+ assert_contains(out, "image", "Empty-alt remote image uses image label");
801
+ apex_free_string(out);
802
+ }
803
+
804
+ /* Local JPEG embeds without embed_images */
805
+ opts.embed_images = false;
806
+ opts.base_directory = "tests/fixtures/images";
807
+ {
808
+ const char *local = "![Local](img/app-pass-1-profile-menu.jpg)\n";
809
+ out = apex_markdown_to_html(local, strlen(local), &opts);
810
+ assert_contains(out, "\\pict", "Local JPEG embeds as RTF pict");
811
+ assert_contains(out, "jpegblip", "Local JPEG uses jpegblip");
812
+ test_result(out != NULL && strstr(out, "HYPERLINK") == NULL,
813
+ "Embedded local image is not a hyperlink fallback");
814
+ apex_free_string(out);
815
+ }
816
+ opts.base_directory = NULL;
817
+
818
+ bool had_failures = suite_end(suite_failures);
819
+ print_suite_title("RTF Output Tests", had_failures, false);
820
+ }
821
+
480
822
  /**
481
823
  * Test header ID generation
482
824
  */
@@ -817,6 +1159,186 @@ void test_indices(void) {
817
1159
  const char *textindex_explicit = "This uses [key combinations]{^}.";
818
1160
  html = apex_markdown_to_html(textindex_explicit, strlen(textindex_explicit), &opts);
819
1161
  assert_contains(html, "class=\"index\"", "TextIndex explicit term generates marker");
1162
+ assert_contains(html, "key combinations", "TextIndex explicit term keeps visible text");
1163
+ assert_not_contains(html, "[key combinations]", "TextIndex explicit term strips square brackets");
1164
+ assert_contains(html, "key combinations <a class=\"index-return\"", "TextIndex index lists explicit term");
1165
+ apex_free_string(html);
1166
+
1167
+ /* Bare {^} indexes only the preceding word; [phrase]{^} is for multi-word terms */
1168
+ const char *textindex_word =
1169
+ "Most mechanical keyboard firmware{^} supports the use of [key combinations]{^}.";
1170
+ html = apex_markdown_to_html(textindex_word, strlen(textindex_word), &opts);
1171
+ assert_contains(html, "firmware<span class=\"index\"", "TextIndex bare {^} leaves preceding word");
1172
+ assert_not_contains(html, "Most mechanical keyboard firmware <a", "TextIndex bare {^} indexes word only, not whole phrase");
1173
+ assert_contains(html, "firmware <a class=\"index-return\"", "TextIndex bare {^} index entry is firmware");
1174
+ assert_not_contains(html, "[key combinations]", "TextIndex phrase form strips brackets in sentence");
1175
+ assert_contains(html, "of key combinations<span class=\"index\"", "TextIndex phrase form keeps unbracketed text");
1176
+ apex_free_string(html);
1177
+
1178
+ /* Quoted heading override / standalone mark (TextIndex Fig. 2) */
1179
+ const char *textindex_quoted =
1180
+ "This is a standalone mark: {^\"foo bar\"} and this one has an overridden [heading]{^\"foo baz\"}.";
1181
+ html = apex_markdown_to_html(textindex_quoted, strlen(textindex_quoted), &opts);
1182
+ assert_not_contains(html, "{^", "TextIndex quoted marks are consumed (not left for superscript)");
1183
+ assert_not_contains(html, "{<sup>", "TextIndex quoted marks are not turned into superscript");
1184
+ assert_contains(html, "standalone mark: <span class=\"index\"", "TextIndex standalone quoted mark leaves only index span");
1185
+ assert_contains(html, "overridden heading<span class=\"index\"", "TextIndex override keeps visible heading without brackets");
1186
+ assert_not_contains(html, "[heading]", "TextIndex override strips square brackets");
1187
+ assert_contains(html, "foo bar <a class=\"index-return\"", "TextIndex standalone quoted mark indexes foo bar");
1188
+ assert_contains(html, "foo baz <a class=\"index-return\"", "TextIndex override indexes quoted term not visible heading");
1189
+ assert_not_contains(html, "heading <a class=\"index-return\"", "TextIndex override does not index visible heading text");
1190
+ apex_free_string(html);
1191
+
1192
+ /* TextIndex Fig. 4-5: underscore emphasis and * / ** wildcards */
1193
+ const char *textindex_emph =
1194
+ "This entry will be _emphasised_{^} in the index. "
1195
+ "This expanded entry won't be _emphasised_{^\"* (nope)\"}.";
1196
+ html = apex_markdown_to_html(textindex_emph, strlen(textindex_emph), &opts);
1197
+ assert_contains(html, "<em>emphasised</em><span class=\"index\"",
1198
+ "TextIndex emphasised heading keeps emphasis in body");
1199
+ assert_contains(html, "<em>emphasised</em> <a class=\"index-return\"",
1200
+ "TextIndex emphasised heading is emphasised in index");
1201
+ assert_not_contains(html, "_emphasised_",
1202
+ "TextIndex emphasised heading does not keep raw underscores in index");
1203
+ assert_contains(html, "emphasised (nope) <a class=\"index-return\"",
1204
+ "TextIndex * wildcard expands preceding without emphasis");
1205
+ assert_not_contains(html, "* (nope)",
1206
+ "TextIndex * wildcard is not left literal in index");
1207
+ apex_free_string(html);
1208
+
1209
+ const char *textindex_emph_strip = "This entry won't be _emphasised_{^*}.";
1210
+ html = apex_markdown_to_html(textindex_emph_strip, strlen(textindex_emph_strip), &opts);
1211
+ assert_contains(html, "emphasised <a class=\"index-return\"",
1212
+ "TextIndex {^*} strips emphasis via wildcard");
1213
+ assert_not_contains(html, "<em>emphasised</em> <a class=\"index-return\"",
1214
+ "TextIndex {^*} index entry is not emphasised");
1215
+ assert_not_contains(html, "_emphasised_",
1216
+ "TextIndex {^*} does not keep raw underscores in index");
1217
+ apex_free_string(html);
1218
+
1219
+ const char *textindex_emph_lower = "Introducing _Widget_{^\"**\"}.";
1220
+ html = apex_markdown_to_html(textindex_emph_lower, strlen(textindex_emph_lower), &opts);
1221
+ assert_contains(html, "widget <a class=\"index-return\"",
1222
+ "TextIndex ** wildcard lowercases stripped preceding");
1223
+ apex_free_string(html);
1224
+
1225
+ /* TextIndex Fig. 6: prefix wildcards — same heading merges locators */
1226
+ const char *textindex_prefix =
1227
+ "Sir Winston Leonard Spencer Churchill{^\"*, Sir Winston Leonard Spencer\"} "
1228
+ "was Prime Minister from 1940 to 1945.\n\n"
1229
+ "Churchill{^*^} was born on 30 November 1874.";
1230
+ html = apex_markdown_to_html(textindex_prefix, strlen(textindex_prefix), &opts);
1231
+ assert_not_contains(html, "Churchill^",
1232
+ "TextIndex *^ is not expanded as a plain * wildcard");
1233
+ assert_contains(html, "href=\"#idxref-0\"", "TextIndex merged entry includes first locator");
1234
+ assert_contains(html, "href=\"#idxref-1\"", "TextIndex merged entry includes second locator");
1235
+ {
1236
+ int heading_hits = 0;
1237
+ for (const char *p = html; (p = strstr(p, "Churchill, Sir Winston Leonard Spencer <a class=\"index-return\"")) != NULL; p++) {
1238
+ heading_hits++;
1239
+ }
1240
+ test_result(heading_hits == 1, "TextIndex *^ merges into one index entry with both locators");
1241
+ }
1242
+ apex_free_string(html);
1243
+
1244
+ /* Duplicate mmark marks also merge locators */
1245
+ const char *mmark_dup = "Alpha (!Alpha) here.\n\nMore about Alpha (!Alpha) there.";
1246
+ html = apex_markdown_to_html(mmark_dup, strlen(mmark_dup), &opts);
1247
+ assert_contains(html, "href=\"#idxref-0\"", "mmark merged entry has first locator");
1248
+ assert_contains(html, "href=\"#idxref-1\"", "mmark merged entry has second locator");
1249
+ {
1250
+ int hits = 0;
1251
+ for (const char *p = html; (p = strstr(p, "Alpha <a class=\"index-return\"")) != NULL; p++) {
1252
+ hits++;
1253
+ }
1254
+ test_result(hits == 1, "mmark duplicate marks merge into one entry");
1255
+ }
1256
+ apex_free_string(html);
1257
+
1258
+ /* TextIndex Fig. 11-12: see / see-also cross-references */
1259
+ const char *textindex_see =
1260
+ "This function can be [dangerous]{^\"destructive operation\"|safety>\"of functions\"}.";
1261
+ html = apex_markdown_to_html(textindex_see, strlen(textindex_see), &opts);
1262
+ assert_contains(html, "be dangerous", "TextIndex see-type keeps visible bracketed text");
1263
+ assert_not_contains(html, "[dangerous]", "TextIndex see-type strips brackets");
1264
+ assert_contains(html, "destructive operation", "TextIndex see-type indexes overridden heading");
1265
+ assert_contains(html, "<em>See</em>", "TextIndex see-type renders See in italics");
1266
+ assert_contains(html, "safety: of functions", "TextIndex see-type shows hierarchical path with colon");
1267
+ assert_not_contains(html, "href=\"#idxref-", "TextIndex see-type does not record a locator");
1268
+ apex_free_string(html);
1269
+
1270
+ const char *textindex_see_also =
1271
+ "Even typing on a keyboard can be surprisingly risky{^\"risk\"|ergonomics;+safety}.";
1272
+ html = apex_markdown_to_html(textindex_see_also, strlen(textindex_see_also), &opts);
1273
+ assert_contains(html, "<em>See</em>", "TextIndex mixed xref renders See");
1274
+ assert_contains(html, "ergonomics", "TextIndex mixed xref includes see target");
1275
+ assert_contains(html, "<em>See also</em>", "TextIndex mixed xref renders See also");
1276
+ assert_contains(html, "safety", "TextIndex mixed xref includes also target");
1277
+ assert_not_contains(html, "href=\"#idxref-", "TextIndex see-type in mark suppresses locator");
1278
+ apex_free_string(html);
1279
+
1280
+ const char *textindex_also_only =
1281
+ "A mention of risk{^\"risk\"|+safety} alone.";
1282
+ html = apex_markdown_to_html(textindex_also_only, strlen(textindex_also_only), &opts);
1283
+ assert_contains(html, "<em>See also</em>", "TextIndex also-only renders See also");
1284
+ assert_contains(html, "href=\"#idxref-", "TextIndex also-only keeps locator");
1285
+ assert_not_contains(html, "<em>See</em> safety", "TextIndex also-only is not a see-type");
1286
+ apex_free_string(html);
1287
+
1288
+ /* TextIndex Fig. 13: aliases */
1289
+ const char *textindex_alias =
1290
+ "The various [operating systems]{^\"Apple (company)\">\"OS platforms\"#apple} "
1291
+ "on Apple devices share a common heritage.\n\n"
1292
+ "The majority of Apple devices are iPhones{^\"iPhone\"|+#apple}, by a large margin.";
1293
+ html = apex_markdown_to_html(textindex_alias, strlen(textindex_alias), &opts);
1294
+ assert_contains(html, "operating systems<span class=\"index\"",
1295
+ "TextIndex alias definition keeps visible text");
1296
+ assert_contains(html, "Apple (company)", "TextIndex alias definition indexes parent heading");
1297
+ assert_contains(html, "OS platforms", "TextIndex alias definition indexes nested heading");
1298
+ assert_contains(html, "iPhone", "TextIndex alias use indexes iPhone");
1299
+ assert_contains(html, "<em>See also</em>", "TextIndex alias use expands as see-also");
1300
+ assert_contains(html, "Apple (company): OS platforms",
1301
+ "TextIndex #apple expands to full path in see-also");
1302
+ assert_not_contains(html, "#apple", "TextIndex alias name is not left literal");
1303
+ apex_free_string(html);
1304
+
1305
+ /* TextIndex Fig. 14: unreferenced aliases */
1306
+ const char *textindex_unref_alias =
1307
+ "{^\"indeterminacy principle (Heisenberg, Werner Karl)\"##q-uncert} "
1308
+ "A key component of the Enterprise's transporter system is the "
1309
+ "Heisenberg compensator{^|#q-uncert}.";
1310
+ html = apex_markdown_to_html(textindex_unref_alias, strlen(textindex_unref_alias), &opts);
1311
+ assert_contains(html, "compensator", "TextIndex unref alias use keeps preceding word");
1312
+ assert_contains(html, "<em>See</em>", "TextIndex unref alias used as see-reference");
1313
+ assert_contains(html, "indeterminacy principle (Heisenberg, Werner Karl)",
1314
+ "TextIndex ## alias expands in see target");
1315
+ assert_not_contains(html, "#q-uncert", "TextIndex ## alias name is not left literal");
1316
+ assert_not_contains(html, "indeterminacy principle (Heisenberg, Werner Karl) <a class=\"index-return\"",
1317
+ "TextIndex ## alias does not create a locator for the definition");
1318
+ apex_free_string(html);
1319
+
1320
+ /* TextIndex Fig. 16: disable/enable processing with {^-} / {^+} */
1321
+ const char *textindex_toggle =
1322
+ "{^-} This index mark won't be processed: foo{^} {^+} after{^}.";
1323
+ html = apex_markdown_to_html(textindex_toggle, strlen(textindex_toggle), &opts);
1324
+ assert_contains(html, "foo{^}", "TextIndex {^-} leaves marks untouched while disabled");
1325
+ assert_contains(html, "after<span class=\"index\"", "TextIndex {^+} re-enables processing");
1326
+ assert_contains(html, "after <a class=\"index-return\"", "TextIndex mark after {^+} is indexed");
1327
+ assert_not_contains(html, "foo <a class=\"index-return\"", "TextIndex mark inside {^-} region is not indexed");
1328
+ assert_not_contains(html, "{^-}", "TextIndex {^-} toggle mark is consumed when it disables");
1329
+ assert_not_contains(html, "{^+}", "TextIndex {^+} toggle mark is consumed when it enables");
1330
+ assert_not_contains(html, "> - <a class=\"index-return\"", "TextIndex {^-} is not indexed as a heading");
1331
+ assert_not_contains(html, "> + <a class=\"index-return\"", "TextIndex {^+} is not indexed as a heading");
1332
+ apex_free_string(html);
1333
+
1334
+ const char *textindex_toggle_redundant =
1335
+ "{^-} hidden{^} {^-} still{^} {^+} shown{^} {^+} trailing.";
1336
+ html = apex_markdown_to_html(textindex_toggle_redundant, strlen(textindex_toggle_redundant), &opts);
1337
+ assert_contains(html, "hidden{^}", "TextIndex redundant region keeps first disabled mark literal");
1338
+ assert_contains(html, "{^-}", "TextIndex redundant {^-} stays when already disabled");
1339
+ assert_contains(html, "still{^}", "TextIndex marks stay literal until {^+}");
1340
+ assert_contains(html, "shown<span class=\"index\"", "TextIndex processing resumes after effective {^+}");
1341
+ assert_contains(html, "{^+}", "TextIndex redundant {^+} stays when already enabled");
820
1342
  apex_free_string(html);
821
1343
 
822
1344
  /* Test Leanpub index syntax */
@@ -963,6 +1485,36 @@ void test_indices(void) {
963
1485
  assert_contains(html, "href=\"#idxref-", "Index entries link to anchors");
964
1486
  apex_free_string(html);
965
1487
 
1488
+ /* Concordance files: auto-mark terms from TSV before TextIndex processing */
1489
+ {
1490
+ apex_options opts_conc = apex_options_default();
1491
+ opts_conc.mode = APEX_MODE_UNIFIED;
1492
+ opts_conc.enable_indices = true;
1493
+ opts_conc.enable_textindex_syntax = true;
1494
+ opts_conc.base_directory = "tests";
1495
+ const char *conc_files[] = {"test_concordance.tsv", NULL};
1496
+ opts_conc.concordance_files = (char **)conc_files;
1497
+
1498
+ const char *conc_doc =
1499
+ "A widget and another Widget appear. The MacBook is nice. Configuration helps.";
1500
+ html = apex_markdown_to_html(conc_doc, strlen(conc_doc), &opts_conc);
1501
+ assert_contains(html, "gadget <a class=\"index-return\"",
1502
+ "Concordance maps widget to gadget heading");
1503
+ assert_contains(html, "Apple laptop <a class=\"index-return\"",
1504
+ "Concordance case-sensitive MacBook uses quoted heading");
1505
+ assert_contains(html, "See", "Concordance see-ref from column 2 is rendered");
1506
+ assert_contains(html, "firmware", "Concordance see target firmware appears");
1507
+ /* Existing marks must not be rematched */
1508
+ const char *conc_existing = "Already marked widget{^custom}.";
1509
+ apex_free_string(html);
1510
+ html = apex_markdown_to_html(conc_existing, strlen(conc_existing), &opts_conc);
1511
+ assert_contains(html, "custom <a class=\"index-return\"",
1512
+ "Concordance skips text already inside an index mark");
1513
+ assert_not_contains(html, "gadget <a class=\"index-return\"",
1514
+ "Concordance does not rematch marked widget as gadget");
1515
+ apex_free_string(html);
1516
+ }
1517
+
966
1518
  bool had_failures = suite_end(suite_failures);
967
1519
  print_suite_title("Index Tests", had_failures, false);
968
1520
  }
@@ -1054,6 +1606,20 @@ void test_citations(void) {
1054
1606
  assert_contains(html, "mailto:", "Email autolinking still works");
1055
1607
  apex_free_string(html);
1056
1608
 
1609
+ /* Test markdown mailto links with @ and query params are not re-autolinked */
1610
+ const char *mailto_markdown =
1611
+ "If you have purchased a permanent unlock or lifetime license through the Mac App Store, please "
1612
+ "[email the developer](mailto:marked@brettterpstra.com?subject=Marked%20License%20Crossgrade&body=Please%20include%20your%20UUID%20%28Help-%3ECopy%20UUID%20in%20Marked%29%20in%20this%20email%20for%20receipt%20verification.) "
1613
+ "to request a free lifetime Paddle license.";
1614
+ html = apex_markdown_to_html(mailto_markdown, strlen(mailto_markdown), &opts_autolink);
1615
+ assert_contains(
1616
+ html,
1617
+ "<a href=\"mailto:marked@brettterpstra.com?subject=Marked%20License%20Crossgrade&amp;body=Please%20include%20your%20UUID%20%28Help-%3ECopy%20UUID%20in%20Marked%29%20in%20this%20email%20for%20receipt%20verification.\">email the developer</a>",
1618
+ "Markdown mailto link with query params renders as a single clean anchor");
1619
+ assert_not_contains(html, "[email the", "Markdown link syntax is not leaked into output");
1620
+ assert_not_contains(html, "mailto:developer](", "No nested autolink corruption in mailto link");
1621
+ apex_free_string(html);
1622
+
1057
1623
  /* Test that autolink does not run inside indented code blocks */
1058
1624
  const char *indented_code =
1059
1625
  " x-marked://extract?url=https://example.com\n";
@@ -1309,7 +1875,7 @@ void test_combine_gitbook_like(void) {
1309
1875
  }
1310
1876
 
1311
1877
  /* Process intro with includes (none here, just sanity) */
1312
- char *intro_md = apex_process_includes(intro_src, base_dir, NULL, 0);
1878
+ char *intro_md = apex_process_includes(intro_src, base_dir, NULL, 0, NULL, NULL, NULL);
1313
1879
  char *intro_html = apex_markdown_to_html(intro_md ? intro_md : intro_src,
1314
1880
  strlen(intro_md ? intro_md : intro_src),
1315
1881
  &opts);
@@ -1342,7 +1908,7 @@ void test_combine_gitbook_like(void) {
1342
1908
  return;
1343
1909
  }
1344
1910
 
1345
- char *chapter_md = apex_process_includes(chapter_src, base_dir, NULL, 0);
1911
+ char *chapter_md = apex_process_includes(chapter_src, base_dir, NULL, 0, NULL, NULL, NULL);
1346
1912
  const char *chapter_final = chapter_md ? chapter_md : chapter_src;
1347
1913
 
1348
1914
  /* Combined Markdown should contain both Chapter 1 and Section 1.1 headings */