apex-ruby 1.0.17 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -3
  3. data/ext/apex_ext/apex_ext.c +260 -91
  4. data/ext/apex_ext/apex_src/CHANGELOG.md +440 -0
  5. data/ext/apex_ext/apex_src/CMakeLists.txt +15 -1
  6. data/ext/apex_ext/apex_src/Formula/apex.rb +7 -2
  7. data/ext/apex_ext/apex_src/Package.swift +15 -4
  8. data/ext/apex_ext/apex_src/README.md +94 -35
  9. data/ext/apex_ext/apex_src/VERSION +1 -1
  10. data/ext/apex_ext/apex_src/apex-debug.log +4 -0
  11. data/ext/apex_ext/apex_src/apex.code-workspace +38 -0
  12. data/ext/apex_ext/apex_src/apex.taskpaper +12 -0
  13. data/ext/apex_ext/apex_src/apex_deflist_debug.log +25 -0
  14. data/ext/apex_ext/apex_src/buildnotes.md +1435 -0
  15. data/ext/apex_ext/apex_src/cli/main.c +262 -33
  16. data/ext/apex_ext/apex_src/debug.log +7 -0
  17. data/ext/apex_ext/apex_src/docs/CITATIONS_EXAMPLES.md +274 -0
  18. data/ext/apex_ext/apex_src/docs/CITATIONS_IMPLEMENTATION_PLAN.md +749 -0
  19. data/ext/apex_ext/apex_src/docs/CITATIONS_PLAN.md +744 -0
  20. data/ext/apex_ext/apex_src/docs/INDEX_IMPLEMENTATION_ANALYSIS.md +382 -0
  21. data/ext/apex_ext/apex_src/docs/plans/2025-02-27-man-page-output-implementation.md +351 -0
  22. data/ext/apex_ext/apex_src/docs/superpowers/plans/2026-07-12-toc-output-format.md +745 -0
  23. data/ext/apex_ext/apex_src/docs/superpowers/plans/2026-07-18-bear-image-attributes.md +829 -0
  24. data/ext/apex_ext/apex_src/docs/superpowers/plans/2026-07-19-apex-node-npm.md +1148 -0
  25. data/ext/apex_ext/apex_src/docs/superpowers/specs/2026-07-12-toc-output-format-design.md +161 -0
  26. data/ext/apex_ext/apex_src/docs/superpowers/specs/2026-07-18-bear-image-attributes-design.md +184 -0
  27. data/ext/apex_ext/apex_src/docs/superpowers/specs/2026-07-19-apex-node-npm-design.md +211 -0
  28. data/ext/apex_ext/apex_src/documentation/app-settings-table.md +67 -0
  29. data/ext/apex_ext/apex_src/documentation/app-transformed/Citations.md +340 -0
  30. data/ext/apex_ext/apex_src/documentation/app-transformed/Credits.md +11 -0
  31. data/ext/apex_ext/apex_src/documentation/app-transformed/Header-IDs.md +234 -0
  32. data/ext/apex_ext/apex_src/documentation/app-transformed/Inline-Attribute-Lists.md +516 -0
  33. data/ext/apex_ext/apex_src/documentation/app-transformed/Metadata-Transforms.md +461 -0
  34. data/ext/apex_ext/apex_src/documentation/app-transformed/Modes.md +193 -0
  35. data/ext/apex_ext/apex_src/documentation/app-transformed/Multi-File-Documents.md +201 -0
  36. data/ext/apex_ext/apex_src/documentation/app-transformed/Plugins.md +678 -0
  37. data/ext/apex_ext/apex_src/documentation/app-transformed/Syntax.md +1367 -0
  38. data/ext/apex_ext/apex_src/documentation/docsets/Apex Command Line Options.cheatsheet.txt +6 -0
  39. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/C-API.html +774 -300
  40. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Callouts.html +1194 -0
  41. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Citations.html +416 -216
  42. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Command-Line-Options.html +3544 -1094
  43. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Configuration.html +371 -97
  44. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Credits.html +153 -23
  45. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Examples.html +358 -124
  46. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Filters.html +1656 -0
  47. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Generating-Man-Pages.html +1226 -0
  48. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Getting-Started.html +201 -56
  49. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Header-IDs.html +279 -109
  50. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Home.html +200 -53
  51. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Indices.html +2327 -0
  52. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Inline-Attribute-Lists.html +690 -271
  53. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Installation.html +201 -55
  54. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Limitations-and-Roadmap.html +238 -68
  55. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Metadata-Transforms.html +421 -221
  56. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Modes.html +340 -78
  57. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Multi-File-Documents.html +251 -86
  58. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Multi-Format-Images.html +1362 -0
  59. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Pandoc-Integration.html +277 -90
  60. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Plugins.html +673 -346
  61. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Quarto-Mode.html +1405 -0
  62. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Rendering-Markdown-In-Terminal.html +1902 -0
  63. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Syntax.html +1939 -1698
  64. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Tables.html +1781 -0
  65. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Troubleshooting.html +315 -140
  66. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Usage.html +307 -113
  67. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Using-Apex-with-Jekyll.html +1132 -0
  68. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Writing-Tests.html +1754 -0
  69. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Xcode-Integration.html +872 -376
  70. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/docSet.dsidx +0 -0
  71. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/optimizedIndex.dsidx +0 -0
  72. data/ext/apex_ext/apex_src/documentation/html/apex-app-docs.html +8746 -0
  73. data/ext/apex_ext/apex_src/documentation/shared_styles.css +20 -15
  74. data/ext/apex_ext/apex_src/documentation/transform_for_app.md +55 -0
  75. data/ext/apex_ext/apex_src/escaping-repro.md +103 -0
  76. data/ext/apex_ext/apex_src/examples/quick-actions/Markdown to RTF.workflow/Contents/Info.plist +8 -0
  77. data/ext/apex_ext/apex_src/examples/quick-actions/Markdown to RTF.workflow/Contents/QuickLook/Preview.png +0 -0
  78. data/ext/apex_ext/apex_src/examples/quick-actions/Markdown to RTF.workflow/Contents/document.wflow +547 -0
  79. data/ext/apex_ext/apex_src/examples/quick-actions/Markdown-to-RTF.workflow.zip +0 -0
  80. data/ext/apex_ext/apex_src/examples/quick-actions/README.md +33 -0
  81. data/ext/apex_ext/apex_src/include/apex/apex.h +92 -6
  82. data/ext/apex_ext/apex_src/include/apex/ast_rtf.h +23 -0
  83. data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +6 -0
  84. data/ext/apex_ext/apex_src/include/apex/module.modulemap +5 -0
  85. data/ext/apex_ext/apex_src/include/apex/plugins.h +105 -0
  86. data/ext/apex_ext/apex_src/include/apexc.h +1 -0
  87. data/ext/apex_ext/apex_src/man/apex.1 +82 -78
  88. data/ext/apex_ext/apex_src/man/apex.1.md +81 -4
  89. data/ext/apex_ext/apex_src/objc/Apex.swift +73 -0
  90. data/ext/apex_ext/apex_src/objc/ApexPlugins.swift +79 -0
  91. data/ext/apex_ext/apex_src/objc/NSString+Apex.h +83 -5
  92. data/ext/apex_ext/apex_src/objc/NSString+Apex.m +244 -1
  93. data/ext/apex_ext/apex_src/objc/NSString+ApexPlugins.h +76 -0
  94. data/ext/apex_ext/apex_src/objc/NSString+ApexPlugins.m +169 -0
  95. data/ext/apex_ext/apex_src/output.html +87 -0
  96. data/ext/apex_ext/apex_src/plans/quarto-mode.md +442 -0
  97. data/ext/apex_ext/apex_src/src/_README.md +85 -32
  98. data/ext/apex_ext/apex_src/src/apex.c +770 -200
  99. data/ext/apex_ext/apex_src/src/ast_rtf.c +848 -0
  100. data/ext/apex_ext/apex_src/src/ast_terminal.c +184 -7
  101. data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +10 -3
  102. data/ext/apex_ext/apex_src/src/extensions/advanced_tables.c +7 -11
  103. data/ext/apex_ext/apex_src/src/extensions/bear_image_attrs.c +467 -0
  104. data/ext/apex_ext/apex_src/src/extensions/bear_image_attrs.h +27 -0
  105. data/ext/apex_ext/apex_src/src/extensions/callouts.c +400 -4
  106. data/ext/apex_ext/apex_src/src/extensions/callouts.h +15 -5
  107. data/ext/apex_ext/apex_src/src/extensions/citations.c +26 -5
  108. data/ext/apex_ext/apex_src/src/extensions/code_fence_attrs.c +632 -0
  109. data/ext/apex_ext/apex_src/src/extensions/code_fence_attrs.h +22 -0
  110. data/ext/apex_ext/apex_src/src/extensions/concordance.c +632 -0
  111. data/ext/apex_ext/apex_src/src/extensions/definition_list.c +129 -15
  112. data/ext/apex_ext/apex_src/src/extensions/grid_tables.c +1050 -121
  113. data/ext/apex_ext/apex_src/src/extensions/html_markdown.c +569 -16
  114. data/ext/apex_ext/apex_src/src/extensions/ial.c +502 -52
  115. data/ext/apex_ext/apex_src/src/extensions/ial.h +2 -1
  116. data/ext/apex_ext/apex_src/src/extensions/includes.c +532 -33
  117. data/ext/apex_ext/apex_src/src/extensions/includes.h +7 -1
  118. data/ext/apex_ext/apex_src/src/extensions/index.c +1427 -140
  119. data/ext/apex_ext/apex_src/src/extensions/index.h +28 -0
  120. data/ext/apex_ext/apex_src/src/extensions/metadata.c +200 -161
  121. data/ext/apex_ext/apex_src/src/extensions/quarto_diagrams.c +251 -0
  122. data/ext/apex_ext/apex_src/src/extensions/quarto_diagrams.h +22 -0
  123. data/ext/apex_ext/apex_src/src/extensions/quarto_lists.c +624 -0
  124. data/ext/apex_ext/apex_src/src/extensions/quarto_lists.h +35 -0
  125. data/ext/apex_ext/apex_src/src/extensions/quarto_polish.c +264 -0
  126. data/ext/apex_ext/apex_src/src/extensions/quarto_polish.h +23 -0
  127. data/ext/apex_ext/apex_src/src/extensions/quarto_shortcodes.c +307 -0
  128. data/ext/apex_ext/apex_src/src/extensions/quarto_shortcodes.h +21 -0
  129. data/ext/apex_ext/apex_src/src/extensions/raw_content.c +351 -0
  130. data/ext/apex_ext/apex_src/src/extensions/raw_content.h +20 -0
  131. data/ext/apex_ext/apex_src/src/extensions/relaxed_tables.c +117 -3
  132. data/ext/apex_ext/apex_src/src/extensions/special_markers.c +53 -0
  133. data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +23 -1
  134. data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +83 -92
  135. data/ext/apex_ext/apex_src/src/extensions/toc.c +446 -25
  136. data/ext/apex_ext/apex_src/src/extensions/toc.h +26 -1
  137. data/ext/apex_ext/apex_src/src/extensions/wiki_links.c +28 -0
  138. data/ext/apex_ext/apex_src/src/html_renderer.c +387 -17
  139. data/ext/apex_ext/apex_src/src/plugin_catalog.c +517 -0
  140. data/ext/apex_ext/apex_src/src/plugins_remote.c +88 -14
  141. data/ext/apex_ext/apex_src/src/plugins_remote.h +34 -0
  142. data/ext/apex_ext/apex_src/src/pretty_html.c +6 -1
  143. data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +21 -0
  144. data/ext/apex_ext/apex_src/tests/fixtures/images/bear_image_attributes.md +10 -0
  145. data/ext/apex_ext/apex_src/tests/fixtures/includes/sections.md +19 -0
  146. data/ext/apex_ext/apex_src/tests/fixtures/includes/sections.txt +11 -0
  147. data/ext/apex_ext/apex_src/tests/fixtures/minimal_pb.md +2 -0
  148. data/ext/apex_ext/apex_src/tests/fixtures/obsidian-callouts.md +46 -0
  149. data/ext/apex_ext/apex_src/tests/fixtures/pagebreak_html_repro.md +5 -0
  150. data/ext/apex_ext/apex_src/tests/fixtures/python-callouts.md +47 -0
  151. data/ext/apex_ext/apex_src/tests/fixtures/quarto/callouts.md +3 -0
  152. data/ext/apex_ext/apex_src/tests/fixtures/quarto/code-attrs.md +3 -0
  153. data/ext/apex_ext/apex_src/tests/fixtures/quarto/diagrams.md +8 -0
  154. data/ext/apex_ext/apex_src/tests/fixtures/quarto/lists.md +19 -0
  155. data/ext/apex_ext/apex_src/tests/fixtures/quarto/raw-content.md +5 -0
  156. data/ext/apex_ext/apex_src/tests/fixtures/quarto/shortcodes.md +7 -0
  157. data/ext/apex_ext/apex_src/tests/fixtures/quarto/smoke.md +81 -0
  158. data/ext/apex_ext/apex_src/tests/fixtures/quarto/spans.md +5 -0
  159. data/ext/apex_ext/apex_src/tests/fixtures/quarto-callouts.md +47 -0
  160. data/ext/apex_ext/apex_src/tests/fixtures/speed.md +192 -0
  161. data/ext/apex_ext/apex_src/tests/fixtures/tables/grid-tables.md +37 -0
  162. data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +16 -0
  163. data/ext/apex_ext/apex_src/tests/test_bear_image_attrs.c +301 -0
  164. data/ext/apex_ext/apex_src/tests/test_concordance.tsv +4 -0
  165. data/ext/apex_ext/apex_src/tests/test_escaping_repro.c +82 -0
  166. data/ext/apex_ext/apex_src/tests/test_extensions.c +821 -1
  167. data/ext/apex_ext/apex_src/tests/test_ial.c +7 -0
  168. data/ext/apex_ext/apex_src/tests/test_marked_integration.c +25 -0
  169. data/ext/apex_ext/apex_src/tests/test_metadata.c +129 -0
  170. data/ext/apex_ext/apex_src/tests/test_output.c +568 -2
  171. data/ext/apex_ext/apex_src/tests/test_runner.c +11 -0
  172. data/ext/apex_ext/apex_src/tests/test_tables.c +420 -0
  173. data/ext/apex_ext/apex_src/vendor/cmark-gfm/CMakeLists.txt +1 -1
  174. data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/autolink.c +77 -1
  175. data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/table.c +135 -5
  176. data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/blocks.c +56 -1
  177. data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/config.h +38 -0
  178. data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/main.c +1 -1
  179. data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/__pycache__/cmark.cpython-314.pyc +0 -0
  180. data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/__pycache__/normalize.cpython-314.pyc +0 -0
  181. data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/extensions.txt +69 -0
  182. data/ext/apex_ext/apex_src/xcode_build/CMakeScripts/ReRunCMake.make +74 -0
  183. data/ext/apex_ext/apex_src/xcode_build/CTestTestfile.cmake +22 -0
  184. data/ext/apex_ext/apex_src/xcode_build/Debug/Apex.framework/Versions/A/Apex +0 -0
  185. data/ext/apex_ext/apex_src/xcode_build/Debug/Apex.framework/Versions/A/Resources/Info.plist +44 -0
  186. data/ext/apex_ext/apex_src/xcode_build/Debug/Apex.framework/Versions/A/_CodeSignature/CodeResources +128 -0
  187. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/project.pbxproj +5118 -0
  188. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  189. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/project.xcworkspace/xcuserdata/ttscoff.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  190. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/project.xcworkspace/xcuserdata/ttscoff.xcuserdatad/WorkspaceSettings.xcsettings +8 -0
  191. data/ext/apex_ext/apex_src/xcode_build/apex.xcodeproj/xcuserdata/ttscoff.xcuserdatad/xcschemes/xcschememanagement.plist +99 -0
  192. data/ext/apex_ext/apex_src/xcode_build/cmake_install.cmake +452 -0
  193. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/CMakeScripts/ReRunCMake.make +52 -0
  194. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/cmake_install.cmake +55 -0
  195. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/cmark-gfm.xcodeproj/project.pbxproj +2455 -0
  196. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/cmark-gfm.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  197. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/extensions/cmake_install.cmake +171 -0
  198. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/man/cmake_install.cmake +55 -0
  199. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/cmake_install.cmake +220 -0
  200. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/cmark-gfm_export.h +43 -0
  201. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/cmark-gfm_version.h +7 -0
  202. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/config.h +76 -0
  203. data/ext/apex_ext/apex_src/xcode_build/vendor/cmark-gfm/src/libcmark-gfm.pc +10 -0
  204. data/ext/apex_ext/extconf.rb +35 -0
  205. data/lib/apex/version.rb +2 -2
  206. metadata +117 -2
  207. data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/tempOptimizedIndex.dsidx +0 -0
@@ -25,6 +25,8 @@ static int apex_idx_size_to_int(size_t v) {
25
25
  /* Index placeholder prefix - we'll use a unique marker */
26
26
  #define INDEX_PLACEHOLDER_PREFIX "<!--IDX:"
27
27
  #define INDEX_PLACEHOLDER_SUFFIX "-->"
28
+ /* Protects literal {^...} marks in {^-} regions from later superscript */
29
+ #define TEXTINDEX_CARET_PLACEHOLDER "APEXTICARET"
28
30
 
29
31
  /**
30
32
  * Check if character is valid in index term
@@ -54,6 +56,700 @@ static char *trim_string(char *str) {
54
56
  return str;
55
57
  }
56
58
 
59
+ /**
60
+ * Strip Markdown single-underscore emphasis markers, keeping the inner text.
61
+ * TextIndex uses _…_ in headings; wildcards insert the stripped form.
62
+ */
63
+ static char *strip_underscore_emphasis(const char *s) {
64
+ if (!s) return NULL;
65
+
66
+ size_t len = strlen(s);
67
+ char *out = malloc(len + 1);
68
+ if (!out) return NULL;
69
+
70
+ size_t wi = 0;
71
+ for (size_t i = 0; i < len; i++) {
72
+ if (s[i] == '_') {
73
+ /* Look for a matching closing _ */
74
+ size_t j = i + 1;
75
+ while (j < len && s[j] != '_') j++;
76
+ if (j < len && j > i + 1) {
77
+ /* Copy inner text without the underscores */
78
+ for (size_t k = i + 1; k < j; k++) {
79
+ out[wi++] = s[k];
80
+ }
81
+ i = j;
82
+ continue;
83
+ }
84
+ }
85
+ out[wi++] = s[i];
86
+ }
87
+ out[wi] = '\0';
88
+ return out;
89
+ }
90
+
91
+ /**
92
+ * Convert TextIndex Markdown underscore emphasis to HTML <em> tags.
93
+ */
94
+ static char *underscore_emphasis_to_html(const char *s) {
95
+ if (!s) return NULL;
96
+
97
+ size_t len = strlen(s);
98
+ /* Worst case: every char becomes part of <em></em> expansion */
99
+ char *out = malloc(len * 6 + 1);
100
+ if (!out) return NULL;
101
+
102
+ size_t wi = 0;
103
+ for (size_t i = 0; i < len; i++) {
104
+ if (s[i] == '_') {
105
+ size_t j = i + 1;
106
+ while (j < len && s[j] != '_') j++;
107
+ if (j < len && j > i + 1) {
108
+ memcpy(out + wi, "<em>", 4);
109
+ wi += 4;
110
+ for (size_t k = i + 1; k < j; k++) {
111
+ out[wi++] = s[k];
112
+ }
113
+ memcpy(out + wi, "</em>", 5);
114
+ wi += 5;
115
+ i = j;
116
+ continue;
117
+ }
118
+ }
119
+ out[wi++] = s[i];
120
+ }
121
+ out[wi] = '\0';
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * Expand TextIndex * / ** wildcards using the preceding heading.
127
+ * * → preceding with emphasis stripped
128
+ * ** → same, lowercased
129
+ * Returns a new string; template is unchanged.
130
+ */
131
+ static char *expand_textindex_wildcards(const char *template, const char *preceding) {
132
+ if (!template) return NULL;
133
+
134
+ char *stripped = preceding ? strip_underscore_emphasis(preceding) : strdup("");
135
+ if (!stripped) return NULL;
136
+
137
+ char *lowered = strdup(stripped);
138
+ if (!lowered) {
139
+ free(stripped);
140
+ return NULL;
141
+ }
142
+ for (char *p = lowered; *p; p++) {
143
+ *p = (char)tolower((unsigned char)*p);
144
+ }
145
+
146
+ size_t tlen = strlen(template);
147
+ size_t slen = strlen(stripped);
148
+ size_t llen = strlen(lowered);
149
+ /* Generous: every char could expand to lowered preceding */
150
+ size_t cap = tlen * (llen > slen ? llen : slen) + tlen + 1;
151
+ if (cap < tlen + 1) cap = tlen + 1;
152
+ char *out = malloc(cap);
153
+ if (!out) {
154
+ free(stripped);
155
+ free(lowered);
156
+ return NULL;
157
+ }
158
+
159
+ size_t wi = 0;
160
+ for (size_t i = 0; i < tlen; i++) {
161
+ if (template[i] == '*') {
162
+ const char *ins = stripped;
163
+ size_t ilen = slen;
164
+ if (i + 1 < tlen && template[i + 1] == '*') {
165
+ ins = lowered;
166
+ ilen = llen;
167
+ i++; /* consume second * */
168
+ }
169
+ if (wi + ilen + 1 > cap) {
170
+ cap = (wi + ilen + 1) * 2;
171
+ char *n = realloc(out, cap);
172
+ if (!n) {
173
+ free(out);
174
+ free(stripped);
175
+ free(lowered);
176
+ return NULL;
177
+ }
178
+ out = n;
179
+ }
180
+ memcpy(out + wi, ins, ilen);
181
+ wi += ilen;
182
+ continue;
183
+ }
184
+ if (wi + 2 > cap) {
185
+ cap = (wi + 2) * 2;
186
+ char *n = realloc(out, cap);
187
+ if (!n) {
188
+ free(out);
189
+ free(stripped);
190
+ free(lowered);
191
+ return NULL;
192
+ }
193
+ out = n;
194
+ }
195
+ out[wi++] = template[i];
196
+ }
197
+ out[wi] = '\0';
198
+ free(stripped);
199
+ free(lowered);
200
+ return out;
201
+ }
202
+
203
+ /**
204
+ * True if template uses a TextIndex * / ** wildcard.
205
+ */
206
+ static bool textindex_has_wildcard(const char *s) {
207
+ return s && strchr(s, '*') != NULL;
208
+ }
209
+
210
+ /**
211
+ * Strip HTML tags for sorting / letter grouping of index terms.
212
+ */
213
+ static char *strip_html_tags(const char *s) {
214
+ if (!s) return NULL;
215
+ size_t len = strlen(s);
216
+ char *out = malloc(len + 1);
217
+ if (!out) return NULL;
218
+ size_t wi = 0;
219
+ for (size_t i = 0; i < len; i++) {
220
+ if (s[i] == '<') {
221
+ while (i < len && s[i] != '>') i++;
222
+ continue;
223
+ }
224
+ out[wi++] = s[i];
225
+ }
226
+ out[wi] = '\0';
227
+ return out;
228
+ }
229
+
230
+ /**
231
+ * Detect TextIndex prefix wildcards: *^ (full path) or *^- (label only).
232
+ * Returns 1 for *^, 2 for *^-, 0 otherwise. Ignores trailing ! or /.
233
+ */
234
+ static int textindex_prefix_wildcard_kind(const char *s) {
235
+ if (!s || s[0] != '*') return 0;
236
+ char buf[16];
237
+ size_t n = 0;
238
+ for (const char *p = s; *p && n + 1 < sizeof(buf); p++) {
239
+ if (*p == '!' || *p == '/') break;
240
+ buf[n++] = *p;
241
+ }
242
+ buf[n] = '\0';
243
+ while (n > 0 && isspace((unsigned char)buf[n - 1])) buf[--n] = '\0';
244
+ if (strcmp(buf, "*^-") == 0) return 2;
245
+ if (strcmp(buf, "*^") == 0) return 1;
246
+ return 0;
247
+ }
248
+
249
+ /**
250
+ * Case-sensitive prefix match of label against an index heading (HTML stripped).
251
+ */
252
+ static bool textindex_heading_has_prefix(const char *heading, const char *label) {
253
+ if (!heading || !label || !label[0]) return false;
254
+ char *plain = strip_html_tags(heading);
255
+ if (!plain) return false;
256
+ size_t llen = strlen(label);
257
+ bool match = strncmp(plain, label, llen) == 0;
258
+ free(plain);
259
+ return match;
260
+ }
261
+
262
+ /**
263
+ * Find the earliest registry entry whose item or subitem starts with label.
264
+ * label should already have underscore emphasis stripped.
265
+ */
266
+ static const apex_index_entry *textindex_find_prefix_entry(const apex_index_registry *registry,
267
+ const char *label) {
268
+ if (!registry || !label || !label[0]) return NULL;
269
+
270
+ const apex_index_entry *best = NULL;
271
+ for (const apex_index_entry *e = registry->entries; e; e = e->next) {
272
+ bool match = textindex_heading_has_prefix(e->item, label);
273
+ if (!match && e->subitem) {
274
+ match = textindex_heading_has_prefix(e->subitem, label);
275
+ }
276
+ if (match && (!best || e->position < best->position)) {
277
+ best = e;
278
+ }
279
+ }
280
+ return best;
281
+ }
282
+
283
+ /**
284
+ * Parse one TextIndex path segment (quoted or bare) into newly allocated text.
285
+ * Advances *pp past the segment. Returns NULL on empty/failure.
286
+ */
287
+ static char *textindex_parse_path_segment(const char **pp) {
288
+ const char *p = *pp;
289
+ while (*p && isspace((unsigned char)*p)) p++;
290
+ if (!*p || *p == '>' || *p == ';' || *p == '|' || *p == '~' || *p == '[') {
291
+ *pp = p;
292
+ return NULL;
293
+ }
294
+
295
+ char *seg = NULL;
296
+ if (*p == '"' || *p == '\'') {
297
+ char quote = *p++;
298
+ const char *start = p;
299
+ while (*p) {
300
+ if (*p == '\\' && p[1]) {
301
+ p += 2;
302
+ continue;
303
+ }
304
+ if (*p == quote) break;
305
+ p++;
306
+ }
307
+ size_t len = (size_t)(p - start);
308
+ seg = malloc(len + 1);
309
+ if (seg) {
310
+ size_t wi = 0;
311
+ for (size_t i = 0; i < len; i++) {
312
+ if (start[i] == '\\' && i + 1 < len) {
313
+ seg[wi++] = start[++i];
314
+ } else {
315
+ seg[wi++] = start[i];
316
+ }
317
+ }
318
+ seg[wi] = '\0';
319
+ }
320
+ if (*p == quote) p++;
321
+ } else {
322
+ const char *start = p;
323
+ while (*p && *p != '>' && *p != ';' && *p != '|' && *p != '~' &&
324
+ *p != '[' && !isspace((unsigned char)*p)) {
325
+ p++;
326
+ }
327
+ size_t len = (size_t)(p - start);
328
+ if (len == 0) {
329
+ *pp = p;
330
+ return NULL;
331
+ }
332
+ seg = malloc(len + 1);
333
+ if (seg) {
334
+ memcpy(seg, start, len);
335
+ seg[len] = '\0';
336
+ }
337
+ }
338
+
339
+ while (*p && isspace((unsigned char)*p)) p++;
340
+ *pp = p;
341
+ return seg;
342
+ }
343
+
344
+ /**
345
+ * Convert a TextIndex heading path (a>b>"c d") to display form "a: b: c d"
346
+ * with underscore emphasis converted to <em>.
347
+ */
348
+ static char *textindex_path_to_display(const char *path) {
349
+ if (!path || !path[0]) return NULL;
350
+
351
+ const char *p = path;
352
+ char *out = NULL;
353
+ size_t out_len = 0;
354
+
355
+ while (*p) {
356
+ while (*p && isspace((unsigned char)*p)) p++;
357
+ if (!*p) break;
358
+ if (*p == '>') {
359
+ p++;
360
+ continue;
361
+ }
362
+
363
+ char *seg = textindex_parse_path_segment(&p);
364
+ if (!seg) break;
365
+
366
+ char *html_seg = underscore_emphasis_to_html(seg);
367
+ free(seg);
368
+ if (!html_seg) {
369
+ free(out);
370
+ return NULL;
371
+ }
372
+
373
+ size_t add = strlen(html_seg);
374
+ size_t need = out_len + add + (out ? 2 : 0) + 1;
375
+ char *n = realloc(out, need);
376
+ if (!n) {
377
+ free(html_seg);
378
+ free(out);
379
+ return NULL;
380
+ }
381
+ out = n;
382
+ if (out_len > 0) {
383
+ out[out_len++] = ':';
384
+ out[out_len++] = ' ';
385
+ }
386
+ memcpy(out + out_len, html_seg, add);
387
+ out_len += add;
388
+ out[out_len] = '\0';
389
+ free(html_seg);
390
+
391
+ while (*p && isspace((unsigned char)*p)) p++;
392
+ if (*p == '>') p++;
393
+ }
394
+
395
+ return out;
396
+ }
397
+
398
+ /**
399
+ * Free a cross-reference list.
400
+ */
401
+ static void apex_index_xref_free_all(apex_index_xref *xref) {
402
+ while (xref) {
403
+ apex_index_xref *next = xref->next;
404
+ free(xref->target);
405
+ free(xref);
406
+ xref = next;
407
+ }
408
+ }
409
+
410
+ static char *textindex_expand_alias_in_path(const apex_index_registry *registry, const char *path);
411
+ static apex_index_alias *textindex_find_alias(const apex_index_registry *registry, const char *name);
412
+
413
+ /**
414
+ * Parse TextIndex cross-references after '|': ergonomics;+safety;@foo;@+bar;#alias
415
+ * Sets *has_conventional_see if any non-inbound see-type is present.
416
+ * Expands #alias targets when registry is provided.
417
+ */
418
+ static apex_index_xref *textindex_parse_xrefs(const char *spec, bool *has_conventional_see,
419
+ const apex_index_registry *registry) {
420
+ if (has_conventional_see) *has_conventional_see = false;
421
+ if (!spec) return NULL;
422
+
423
+ apex_index_xref *head = NULL;
424
+ apex_index_xref *tail = NULL;
425
+ const char *p = spec;
426
+
427
+ while (*p) {
428
+ while (*p && (isspace((unsigned char)*p) || *p == ';')) p++;
429
+ if (!*p) break;
430
+
431
+ bool inbound = false;
432
+ bool also = false;
433
+ if (*p == '@') {
434
+ inbound = true;
435
+ p++;
436
+ }
437
+ if (*p == '+') {
438
+ also = true;
439
+ p++;
440
+ }
441
+
442
+ const char *path_start = p;
443
+ while (*p && *p != ';') p++;
444
+ size_t path_len = (size_t)(p - path_start);
445
+ while (path_len > 0 && isspace((unsigned char)path_start[path_len - 1])) path_len--;
446
+
447
+ if (path_len == 0) continue;
448
+
449
+ char *path_buf = malloc(path_len + 1);
450
+ if (!path_buf) continue;
451
+ memcpy(path_buf, path_start, path_len);
452
+ path_buf[path_len] = '\0';
453
+
454
+ char *expanded = textindex_expand_alias_in_path(registry, path_buf);
455
+ free(path_buf);
456
+ if (!expanded) continue;
457
+
458
+ char *display = NULL;
459
+ apex_index_alias *a = NULL;
460
+ if (expanded[0] == '#' ) {
461
+ /* Unexpanded alias — leave literal (will show in tests as failure) */
462
+ display = textindex_path_to_display(expanded);
463
+ } else {
464
+ /* If expansion produced a raw path, prefer alias display when exact match */
465
+ display = textindex_path_to_display(expanded);
466
+ /* Prefer stored display for pure alias expansion */
467
+ if (registry) {
468
+ for (a = registry->aliases; a; a = a->next) {
469
+ if (a->path && strcmp(a->path, expanded) == 0 && a->display) {
470
+ free(display);
471
+ display = strdup(a->display);
472
+ break;
473
+ }
474
+ }
475
+ }
476
+ }
477
+ free(expanded);
478
+ if (!display || !display[0]) {
479
+ free(display);
480
+ continue;
481
+ }
482
+
483
+ apex_index_xref *xref = calloc(1, sizeof(apex_index_xref));
484
+ if (!xref) {
485
+ free(display);
486
+ continue;
487
+ }
488
+ xref->target = display;
489
+ xref->also = also;
490
+ xref->inbound = inbound;
491
+ xref->next = NULL;
492
+
493
+ if (!also && !inbound && has_conventional_see) {
494
+ *has_conventional_see = true;
495
+ }
496
+
497
+ if (!head) head = xref;
498
+ else tail->next = xref;
499
+ tail = xref;
500
+ }
501
+
502
+ return head;
503
+ }
504
+
505
+ /**
506
+ * True if character is valid in a TextIndex alias name.
507
+ */
508
+ static bool textindex_alias_name_char(unsigned char c) {
509
+ return isalnum(c) || c == '-' || c == '_';
510
+ }
511
+
512
+ /**
513
+ * Lookup an alias by exact name.
514
+ */
515
+ static apex_index_alias *textindex_find_alias(const apex_index_registry *registry, const char *name) {
516
+ if (!registry || !name) return NULL;
517
+ for (apex_index_alias *a = registry->aliases; a; a = a->next) {
518
+ if (a->name && strcmp(a->name, name) == 0) return a;
519
+ }
520
+ return NULL;
521
+ }
522
+
523
+ /**
524
+ * Define or replace a TextIndex alias.
525
+ */
526
+ static void textindex_define_alias(apex_index_registry *registry,
527
+ const char *name,
528
+ const char *path,
529
+ const char *item,
530
+ const char *subitem) {
531
+ if (!registry || !name || !name[0] || !item) return;
532
+
533
+ apex_index_alias *existing = textindex_find_alias(registry, name);
534
+ if (existing) {
535
+ free(existing->path);
536
+ free(existing->item);
537
+ free(existing->subitem);
538
+ free(existing->display);
539
+ existing->path = path ? strdup(path) : NULL;
540
+ existing->item = strdup(item);
541
+ existing->subitem = subitem ? strdup(subitem) : NULL;
542
+ } else {
543
+ existing = calloc(1, sizeof(apex_index_alias));
544
+ if (!existing) return;
545
+ existing->name = strdup(name);
546
+ existing->path = path ? strdup(path) : NULL;
547
+ existing->item = strdup(item);
548
+ existing->subitem = subitem ? strdup(subitem) : NULL;
549
+ existing->next = registry->aliases;
550
+ registry->aliases = existing;
551
+ }
552
+
553
+ if (existing->subitem && existing->subitem[0]) {
554
+ size_t n = strlen(existing->item) + strlen(existing->subitem) + 3;
555
+ existing->display = malloc(n);
556
+ if (existing->display) {
557
+ snprintf(existing->display, n, "%s: %s", existing->item, existing->subitem);
558
+ }
559
+ } else {
560
+ existing->display = strdup(existing->item);
561
+ }
562
+ }
563
+
564
+ static void textindex_free_aliases(apex_index_alias *alias) {
565
+ while (alias) {
566
+ apex_index_alias *next = alias->next;
567
+ free(alias->name);
568
+ free(alias->path);
569
+ free(alias->item);
570
+ free(alias->subitem);
571
+ free(alias->display);
572
+ free(alias);
573
+ alias = next;
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Expand a leading #alias in a path string. Returns newly allocated expanded
579
+ * path (raw, with > separators) or strdup of input if no expansion.
580
+ */
581
+ static char *textindex_expand_alias_in_path(const apex_index_registry *registry, const char *path) {
582
+ if (!path) return NULL;
583
+ if (path[0] != '#') return strdup(path);
584
+
585
+ const char *name = path + 1;
586
+ size_t nlen = 0;
587
+ while (name[nlen] && textindex_alias_name_char((unsigned char)name[nlen])) nlen++;
588
+ if (nlen == 0) return strdup(path);
589
+
590
+ char *name_buf = malloc(nlen + 1);
591
+ if (!name_buf) return strdup(path);
592
+ memcpy(name_buf, name, nlen);
593
+ name_buf[nlen] = '\0';
594
+
595
+ apex_index_alias *a = textindex_find_alias(registry, name_buf);
596
+ free(name_buf);
597
+ if (!a) return strdup(path);
598
+
599
+ /* Remaining path after #alias (e.g. #apple>extra) */
600
+ const char *rest = path + 1 + nlen;
601
+ while (*rest && isspace((unsigned char)*rest)) rest++;
602
+
603
+ if (a->path && a->path[0]) {
604
+ if (*rest == '>') {
605
+ size_t n = strlen(a->path) + strlen(rest) + 1;
606
+ char *out = malloc(n);
607
+ if (!out) return strdup(a->path);
608
+ snprintf(out, n, "%s%s", a->path, rest);
609
+ return out;
610
+ }
611
+ if (*rest) {
612
+ /* Alias followed by other text — treat alias as first segment */
613
+ size_t n = strlen(a->path) + strlen(rest) + 2;
614
+ char *out = malloc(n);
615
+ if (!out) return strdup(a->path);
616
+ snprintf(out, n, "%s>%s", a->path, rest);
617
+ return out;
618
+ }
619
+ return strdup(a->path);
620
+ }
621
+
622
+ /* Fall back to item/subitem */
623
+ if (a->subitem && a->subitem[0]) {
624
+ if (*rest == '>' || *rest) {
625
+ size_t n = strlen(a->item) + strlen(a->subitem) + strlen(rest) + 4;
626
+ char *out = malloc(n);
627
+ if (!out) return NULL;
628
+ if (*rest == '>') {
629
+ snprintf(out, n, "%s>%s%s", a->item, a->subitem, rest);
630
+ } else if (*rest) {
631
+ snprintf(out, n, "%s>%s>%s", a->item, a->subitem, rest);
632
+ } else {
633
+ snprintf(out, n, "%s>%s", a->item, a->subitem);
634
+ }
635
+ return out;
636
+ }
637
+ size_t n = strlen(a->item) + strlen(a->subitem) + 2;
638
+ char *out = malloc(n);
639
+ if (!out) return NULL;
640
+ snprintf(out, n, "%s>%s", a->item, a->subitem);
641
+ return out;
642
+ }
643
+ return strdup(a->item);
644
+ }
645
+
646
+ /**
647
+ * Strip a trailing #name or ##name from heading_part (modified in place).
648
+ * Sets *alias_name and *unref. Returns true if an alias definition was found.
649
+ */
650
+ static bool textindex_strip_trailing_alias(char *heading_part, char **alias_name, bool *unref) {
651
+ if (alias_name) *alias_name = NULL;
652
+ if (unref) *unref = false;
653
+ if (!heading_part) return false;
654
+
655
+ /* Walk backwards outside of quotes to find a # alias definition */
656
+ size_t len = strlen(heading_part);
657
+ bool in_quote = false;
658
+ char quote = 0;
659
+ int hash_at = -1;
660
+ for (size_t i = 0; i < len; i++) {
661
+ if (in_quote) {
662
+ if (heading_part[i] == '\\' && i + 1 < len) {
663
+ i++;
664
+ continue;
665
+ }
666
+ if (heading_part[i] == quote) in_quote = false;
667
+ continue;
668
+ }
669
+ if (heading_part[i] == '"' || heading_part[i] == '\'') {
670
+ in_quote = true;
671
+ quote = heading_part[i];
672
+ continue;
673
+ }
674
+ if (heading_part[i] == '#') {
675
+ hash_at = (int)i;
676
+ /* ##alias — keep hash_at at the first '#', skip the second */
677
+ if (i + 1 < len && heading_part[i + 1] == '#') {
678
+ i++;
679
+ }
680
+ continue;
681
+ }
682
+ }
683
+ if (hash_at < 0) return false;
684
+
685
+ const char *p = heading_part + hash_at;
686
+ bool is_unref = false;
687
+ if (p[0] == '#' && p[1] == '#') {
688
+ is_unref = true;
689
+ p += 2;
690
+ } else {
691
+ p += 1;
692
+ }
693
+
694
+ if (!*p || !textindex_alias_name_char((unsigned char)*p)) return false;
695
+ const char *name_start = p;
696
+ while (*p && textindex_alias_name_char((unsigned char)*p)) p++;
697
+ /* Only trailing alias: rest must be whitespace */
698
+ const char *rest = p;
699
+ while (*rest && isspace((unsigned char)*rest)) rest++;
700
+ if (*rest) return false;
701
+
702
+ size_t nlen = (size_t)(p - name_start);
703
+ char *name = malloc(nlen + 1);
704
+ if (!name) return false;
705
+ memcpy(name, name_start, nlen);
706
+ name[nlen] = '\0';
707
+
708
+ /* Truncate heading_part before the # */
709
+ size_t cut = (size_t)hash_at;
710
+ while (cut > 0 && isspace((unsigned char)heading_part[cut - 1])) cut--;
711
+ heading_part[cut] = '\0';
712
+
713
+ if (alias_name) *alias_name = name;
714
+ else free(name);
715
+ if (unref) *unref = is_unref;
716
+ return true;
717
+ }
718
+
719
+ /**
720
+ * Split a raw TextIndex path into item + optional subitem (HTML-converted).
721
+ * For A>B>C: item=A, subitem="B: C".
722
+ */
723
+ static void textindex_path_to_item_subitem(const char *path, char **item_out, char **subitem_out) {
724
+ if (item_out) *item_out = NULL;
725
+ if (subitem_out) *subitem_out = NULL;
726
+ if (!path || !path[0]) return;
727
+
728
+ const char *p = path;
729
+ char *first = textindex_parse_path_segment(&p);
730
+ if (!first) return;
731
+
732
+ char *item_html = underscore_emphasis_to_html(first);
733
+ free(first);
734
+ if (!item_html) return;
735
+
736
+ while (*p && isspace((unsigned char)*p)) p++;
737
+ if (*p == '>') p++;
738
+ while (*p && isspace((unsigned char)*p)) p++;
739
+
740
+ if (!*p) {
741
+ if (item_out) *item_out = item_html;
742
+ else free(item_html);
743
+ return;
744
+ }
745
+
746
+ char *rest_display = textindex_path_to_display(p);
747
+ if (item_out) *item_out = item_html;
748
+ else free(item_html);
749
+ if (subitem_out) *subitem_out = rest_display;
750
+ else free(rest_display);
751
+ }
752
+
57
753
  /**
58
754
  * Parse mmark index syntax: (!item), (!item, subitem), (!!item, subitem)
59
755
  * Returns length consumed, or 0 if not a match
@@ -149,14 +845,23 @@ static int parse_mmark_index(const char *text, int pos, int len,
149
845
  }
150
846
 
151
847
  /**
152
- * Parse TextIndex syntax: {^}, [term]{^}, {^params}
153
- * Returns length consumed, or 0 if not a match
848
+ * Parse TextIndex syntax: {^}, [term]{^}, {^params}, {^"quoted"}, [vis]{^"override"}
849
+ * Supports _underscore_ emphasis in headings and * / ** wildcards (TextIndex Fig. 4-5).
850
+ * Returns length consumed from {^...}, or 0 if not a match.
154
851
  *
155
- * TextIndex syntax is: word{^} or [term]{^} or {^params}
156
- * We look for {^ pattern and extract the term from before it
852
+ * When the term is an explicit [term] before {^}, *bracketed_prefix_out is set to
853
+ * the length of that "[term]" span (including brackets) so the caller can strip
854
+ * the brackets from already-copied output. Otherwise *bracketed_prefix_out is 0.
855
+ *
856
+ * Bare word{^} indexes only the immediate preceding word (not a multi-word phrase);
857
+ * use [phrase]{^} for multi-word terms. A quoted heading inside the braces
858
+ * ({^"foo bar"} or [visible]{^"index term"}) overrides the index entry text.
157
859
  */
158
860
  static int parse_textindex(const char *text, int pos, int len,
159
- apex_index_entry **entry_out) {
861
+ apex_index_entry **entry_out,
862
+ int *bracketed_prefix_out,
863
+ apex_index_registry *registry) {
864
+ if (bracketed_prefix_out) *bracketed_prefix_out = 0;
160
865
  if (pos + 2 >= len) return 0;
161
866
 
162
867
  const char *p = text + pos;
@@ -188,10 +893,56 @@ static int parse_textindex(const char *text, int pos, int len,
188
893
  p++; /* Skip } */
189
894
  int consumed = apex_idx_ptrdiff_to_int(p - (text + pos));
190
895
 
191
- /* Check for explicit term before {^: [term]{^} */
192
- char *term = NULL;
896
+ /* Split params into heading path and optional |xrefs (pipe outside quotes) */
897
+ char *heading_part = NULL;
898
+ const char *xref_spec = NULL;
899
+ if (params && params[0]) {
900
+ const char *pipe = NULL;
901
+ bool in_q = false;
902
+ char qch = 0;
903
+ for (const char *s = params; *s; s++) {
904
+ if (in_q) {
905
+ if (*s == '\\' && s[1]) {
906
+ s++;
907
+ continue;
908
+ }
909
+ if (*s == qch) in_q = false;
910
+ continue;
911
+ }
912
+ if (*s == '"' || *s == '\'') {
913
+ in_q = true;
914
+ qch = *s;
915
+ continue;
916
+ }
917
+ if (*s == '|') {
918
+ pipe = s;
919
+ break;
920
+ }
921
+ }
922
+ if (pipe) {
923
+ size_t hlen = (size_t)(pipe - params);
924
+ heading_part = malloc(hlen + 1);
925
+ if (heading_part) {
926
+ memcpy(heading_part, params, hlen);
927
+ heading_part[hlen] = '\0';
928
+ trim_string(heading_part);
929
+ }
930
+ xref_spec = pipe + 1;
931
+ } else {
932
+ heading_part = strdup(params);
933
+ if (heading_part) {
934
+ size_t rlen = strlen(heading_part);
935
+ while (rlen > 0 && (heading_part[rlen - 1] == '!' || heading_part[rlen - 1] == '/')) {
936
+ heading_part[--rlen] = '\0';
937
+ }
938
+ trim_string(heading_part);
939
+ }
940
+ }
941
+ }
942
+
943
+ /* Check for explicit visible term before {^: [term]{^...} */
944
+ char *visible_term = NULL;
193
945
  if (brace_start > text && brace_start[-1] == ']') {
194
- /* Look backwards for [ */
195
946
  const char *bracket_start = brace_start - 1;
196
947
  int lookback = 0;
197
948
  while (bracket_start > text && *bracket_start != '[' && lookback < 200) {
@@ -200,33 +951,31 @@ static int parse_textindex(const char *text, int pos, int len,
200
951
  }
201
952
 
202
953
  if (*bracket_start == '[') {
203
- /* Term is content between [ and ], excluding the brackets */
204
954
  size_t term_len = (brace_start - 1) - (bracket_start + 1);
205
955
  if (term_len > 0 && term_len < 200) {
206
- term = malloc(term_len + 1);
207
- if (term) {
208
- memcpy(term, bracket_start + 1, term_len);
209
- term[term_len] = '\0';
210
- trim_string(term);
956
+ visible_term = malloc(term_len + 1);
957
+ if (visible_term) {
958
+ memcpy(visible_term, bracket_start + 1, term_len);
959
+ visible_term[term_len] = '\0';
960
+ trim_string(visible_term);
961
+ }
962
+ if (bracketed_prefix_out) {
963
+ *bracketed_prefix_out = apex_idx_ptrdiff_to_int(brace_start - bracket_start);
211
964
  }
212
965
  }
213
966
  }
214
967
  }
215
968
 
216
- /* If no explicit term, extract word/phrase before {^ */
217
- if (!term || strlen(term) == 0) {
969
+ /* Implicit preceding word when immediately adjacent (no whitespace). */
970
+ char *implicit_word = NULL;
971
+ if ((!visible_term || !visible_term[0]) &&
972
+ brace_start > text && !isspace((unsigned char)brace_start[-1])) {
218
973
  const char *word_end = brace_start;
219
- /* Skip backwards over whitespace */
220
- while (word_end > text && isspace((unsigned char)word_end[-1])) {
221
- word_end--;
222
- }
223
-
224
- /* Extract word/phrase (up to 50 chars backwards) */
225
974
  const char *word_start = word_end;
226
975
  int word_chars = 0;
227
976
  while (word_start > text && word_chars < 50) {
228
- char c = word_start[-1];
229
- if (isalnum(c) || c == ' ' || c == '-' || c == '_') {
977
+ unsigned char c = (unsigned char)word_start[-1];
978
+ if (isalnum(c) || c == '-' || c == '_') {
230
979
  word_start--;
231
980
  word_chars++;
232
981
  } else {
@@ -236,51 +985,165 @@ static int parse_textindex(const char *text, int pos, int len,
236
985
 
237
986
  if (word_chars > 0) {
238
987
  size_t term_len = word_end - word_start;
239
- if (term_len > 0) {
240
- free(term); /* Free if we allocated empty term above */
241
- term = malloc(term_len + 1);
242
- if (term) {
243
- memcpy(term, word_start, term_len);
244
- term[term_len] = '\0';
245
- trim_string(term);
246
- }
988
+ implicit_word = malloc(term_len + 1);
989
+ if (implicit_word) {
990
+ memcpy(implicit_word, word_start, term_len);
991
+ implicit_word[term_len] = '\0';
992
+ trim_string(implicit_word);
247
993
  }
248
994
  }
249
995
  }
250
996
 
251
- if (!term || strlen(term) == 0) {
252
- free(params);
253
- return 0; /* No term found */
997
+ char *preceding = NULL;
998
+ if (visible_term && visible_term[0]) {
999
+ preceding = visible_term;
1000
+ visible_term = NULL;
1001
+ } else if (implicit_word && implicit_word[0]) {
1002
+ preceding = implicit_word;
1003
+ implicit_word = NULL;
1004
+ }
1005
+ free(visible_term);
1006
+ free(implicit_word);
1007
+
1008
+ /* Trailing #alias / ##alias on the heading path */
1009
+ char *alias_def_name = NULL;
1010
+ bool unref_alias = false;
1011
+ if (heading_part) {
1012
+ textindex_strip_trailing_alias(heading_part, &alias_def_name, &unref_alias);
254
1013
  }
255
1014
 
256
- /* Parse params for subitem (simplified - TextIndex has complex param syntax) */
257
- char *subitem = NULL;
258
- if (params) {
259
- /* For now, if params contain a space or comma, treat as subitem */
260
- char *space = strchr(params, ' ');
261
- char *comma = strchr(params, ',');
262
- if (space || comma) {
263
- const char *sub_start = (space && (!comma || space < comma)) ? space + 1 : comma + 1;
264
- while (*sub_start && isspace((unsigned char)*sub_start)) sub_start++;
265
- if (*sub_start) {
266
- subitem = strdup(sub_start);
267
- trim_string(subitem);
1015
+ char *path_raw = NULL;
1016
+ char *item_html = NULL;
1017
+ char *subitem_html = NULL;
1018
+ bool heading_already_html = false;
1019
+
1020
+ if (heading_part && heading_part[0]) {
1021
+ /* Expand leading #alias, then handle wildcards / paths */
1022
+ char *expanded = textindex_expand_alias_in_path(registry, heading_part);
1023
+ free(heading_part);
1024
+ heading_part = NULL;
1025
+ path_raw = expanded;
1026
+
1027
+ /* Bare prefix / simple wildcards without hierarchy */
1028
+ int pkind = textindex_prefix_wildcard_kind(path_raw);
1029
+ if (pkind) {
1030
+ char *label = preceding ? strip_underscore_emphasis(preceding) : NULL;
1031
+ const apex_index_entry *match =
1032
+ (label && registry) ? textindex_find_prefix_entry(registry, label) : NULL;
1033
+ free(label);
1034
+ if (!match) {
1035
+ free(path_raw);
1036
+ free(alias_def_name);
1037
+ free(preceding);
1038
+ free(params);
1039
+ if (bracketed_prefix_out) *bracketed_prefix_out = 0;
1040
+ return 0;
1041
+ }
1042
+ if (pkind == 2) {
1043
+ char *lab2 = strip_underscore_emphasis(preceding);
1044
+ if (match->subitem && textindex_heading_has_prefix(match->subitem, lab2)) {
1045
+ item_html = strdup(match->subitem);
1046
+ } else {
1047
+ item_html = strdup(match->item);
1048
+ }
1049
+ free(lab2);
1050
+ } else {
1051
+ item_html = strdup(match->item);
1052
+ if (match->subitem) subitem_html = strdup(match->subitem);
268
1053
  }
1054
+ heading_already_html = true;
1055
+ free(path_raw);
1056
+ path_raw = NULL;
1057
+ } else if (textindex_has_wildcard(path_raw) && !strchr(path_raw, '>')) {
1058
+ /* Single-segment wildcard heading: strip quotes if present */
1059
+ char *tmpl = path_raw;
1060
+ char *owned = path_raw;
1061
+ if ((tmpl[0] == '"' || tmpl[0] == '\'') && strlen(tmpl) >= 2) {
1062
+ char quote = tmpl[0];
1063
+ size_t tlen = strlen(tmpl);
1064
+ if (tmpl[tlen - 1] == quote) {
1065
+ tmpl[tlen - 1] = '\0';
1066
+ tmpl++;
1067
+ }
1068
+ }
1069
+ if (!preceding) {
1070
+ free(owned);
1071
+ free(alias_def_name);
1072
+ free(preceding);
1073
+ free(params);
1074
+ if (bracketed_prefix_out) *bracketed_prefix_out = 0;
1075
+ return 0;
1076
+ }
1077
+ char *expanded_w = expand_textindex_wildcards(tmpl, preceding);
1078
+ free(owned);
1079
+ path_raw = expanded_w;
1080
+ /* Expanded wildcard text is a single heading, not a > path */
1081
+ if (path_raw) {
1082
+ item_html = underscore_emphasis_to_html(path_raw);
1083
+ }
1084
+ } else {
1085
+ textindex_path_to_item_subitem(path_raw, &item_html, &subitem_html);
269
1086
  }
1087
+ } else {
1088
+ free(heading_part);
1089
+ heading_part = NULL;
270
1090
  }
271
1091
 
272
- /* Create index entry */
273
- apex_index_entry *entry = apex_index_entry_new(term, APEX_INDEX_TEXTINDEX);
1092
+ if (!item_html && preceding) {
1093
+ item_html = underscore_emphasis_to_html(preceding);
1094
+ if (!path_raw && preceding) path_raw = strdup(preceding);
1095
+ }
1096
+
1097
+ /* Define alias for this path when #name / ##name was present */
1098
+ if (alias_def_name && item_html) {
1099
+ textindex_define_alias(registry, alias_def_name, path_raw, item_html, subitem_html);
1100
+ }
1101
+ free(alias_def_name);
1102
+ free(preceding);
1103
+
1104
+ /* ##alias: define only — consume mark, no index entry */
1105
+ if (unref_alias) {
1106
+ free(item_html);
1107
+ free(subitem_html);
1108
+ free(path_raw);
1109
+ free(params);
1110
+ *entry_out = NULL;
1111
+ return consumed;
1112
+ }
1113
+
1114
+ if (!item_html || !item_html[0]) {
1115
+ free(item_html);
1116
+ free(subitem_html);
1117
+ free(path_raw);
1118
+ free(params);
1119
+ if (bracketed_prefix_out) *bracketed_prefix_out = 0;
1120
+ return 0;
1121
+ }
1122
+
1123
+ /* Cross-references after '|' (with #alias expansion) */
1124
+ apex_index_xref *xrefs = NULL;
1125
+ bool has_conventional_see = false;
1126
+ if (xref_spec) {
1127
+ xrefs = textindex_parse_xrefs(xref_spec, &has_conventional_see, registry);
1128
+ }
1129
+
1130
+ (void)heading_already_html; /* item_html already final */
1131
+
1132
+ apex_index_entry *entry = apex_index_entry_new(item_html, APEX_INDEX_TEXTINDEX);
1133
+ free(item_html);
1134
+ free(path_raw);
274
1135
  if (entry) {
275
- entry->subitem = subitem;
1136
+ entry->subitem = subitem_html;
1137
+ entry->xrefs = xrefs;
1138
+ entry->suppress_locator = has_conventional_see;
276
1139
  *entry_out = entry;
277
1140
  } else {
278
- free(term);
279
- free(subitem);
1141
+ free(subitem_html);
1142
+ apex_index_xref_free_all(xrefs);
1143
+ if (bracketed_prefix_out) *bracketed_prefix_out = 0;
280
1144
  }
281
1145
 
282
1146
  free(params);
283
-
284
1147
  return consumed;
285
1148
  }
286
1149
 
@@ -429,9 +1292,11 @@ apex_index_entry *apex_index_entry_new(const char *item, apex_index_syntax_t syn
429
1292
  entry->item = strdup(item);
430
1293
  entry->subitem = NULL;
431
1294
  entry->primary = false;
1295
+ entry->suppress_locator = false;
432
1296
  entry->position = 0;
433
1297
  entry->anchor_id = NULL;
434
1298
  entry->syntax_type = syntax_type;
1299
+ entry->xrefs = NULL;
435
1300
  entry->next = NULL;
436
1301
 
437
1302
  return entry;
@@ -446,6 +1311,7 @@ void apex_index_entry_free(apex_index_entry *entry) {
446
1311
  free(entry->item);
447
1312
  free(entry->subitem);
448
1313
  free(entry->anchor_id);
1314
+ apex_index_xref_free_all(entry->xrefs);
449
1315
  free(entry);
450
1316
  }
451
1317
 
@@ -463,6 +1329,8 @@ void apex_free_index_registry(apex_index_registry *registry) {
463
1329
  }
464
1330
 
465
1331
  registry->entries = NULL;
1332
+ textindex_free_aliases(registry->aliases);
1333
+ registry->aliases = NULL;
466
1334
  registry->count = 0;
467
1335
  registry->next_ref_id = 0;
468
1336
  }
@@ -530,10 +1398,84 @@ char *apex_process_index_entries(const char *text, apex_index_registry *registry
530
1398
  const char *read = text;
531
1399
  char *write = output;
532
1400
  size_t remaining = capacity;
1401
+ bool textindex_processing = true; /* {^-} / {^+} toggles */
533
1402
 
534
1403
  while (*read) {
535
1404
  apex_index_entry *entry = NULL;
536
1405
  int consumed = 0;
1406
+ int bracketed_prefix = 0;
1407
+
1408
+ /* TextIndex processing toggles: {^-} disables, {^+} enables.
1409
+ * Effective toggles are removed; redundant ones are left untouched. */
1410
+ if (options->enable_textindex_syntax &&
1411
+ read[0] == '{' && read[1] == '^' &&
1412
+ (read[2] == '-' || read[2] == '+') && read[3] == '}') {
1413
+ if (read[2] == '-' && textindex_processing) {
1414
+ textindex_processing = false;
1415
+ read += 4;
1416
+ continue;
1417
+ }
1418
+ if (read[2] == '+' && !textindex_processing) {
1419
+ textindex_processing = true;
1420
+ read += 4;
1421
+ continue;
1422
+ }
1423
+ /* Redundant toggle: emit with caret placeholder (survives superscript) */
1424
+ const char *lit = (read[2] == '-') ? "{APEXTICARET-}" : "{APEXTICARET+}";
1425
+ size_t lit_len = strlen(lit);
1426
+ if (remaining < lit_len + 1) {
1427
+ size_t used = write - output;
1428
+ capacity = (used + lit_len + 1) * 2;
1429
+ char *new_output = realloc(output, capacity);
1430
+ if (!new_output) {
1431
+ free(output);
1432
+ return NULL;
1433
+ }
1434
+ output = new_output;
1435
+ write = output + used;
1436
+ remaining = capacity - used;
1437
+ }
1438
+ memcpy(write, lit, lit_len);
1439
+ write += lit_len;
1440
+ remaining -= lit_len;
1441
+ read += 4;
1442
+ continue;
1443
+ }
1444
+
1445
+ /* While disabled, leave TextIndex marks literal (protect ^ from superscript) */
1446
+ if (options->enable_textindex_syntax && !textindex_processing &&
1447
+ read[0] == '{' && read[1] == '^') {
1448
+ const char *end = read + 2;
1449
+ while (*end && *end != '}' && *end != '\n') end++;
1450
+ if (*end == '}') {
1451
+ size_t inner_len = (size_t)(end - (read + 2));
1452
+ size_t ph_len = strlen(TEXTINDEX_CARET_PLACEHOLDER);
1453
+ size_t lit_len = 1 + ph_len + inner_len + 1; /* { PLACEHOLDER inner } */
1454
+ if (remaining < lit_len + 1) {
1455
+ size_t used = write - output;
1456
+ capacity = (used + lit_len + 1) * 2;
1457
+ char *new_output = realloc(output, capacity);
1458
+ if (!new_output) {
1459
+ free(output);
1460
+ return NULL;
1461
+ }
1462
+ output = new_output;
1463
+ write = output + used;
1464
+ remaining = capacity - used;
1465
+ }
1466
+ *write++ = '{';
1467
+ memcpy(write, TEXTINDEX_CARET_PLACEHOLDER, ph_len);
1468
+ write += ph_len;
1469
+ if (inner_len > 0) {
1470
+ memcpy(write, read + 2, inner_len);
1471
+ write += inner_len;
1472
+ }
1473
+ *write++ = '}';
1474
+ remaining -= lit_len;
1475
+ read = end + 1;
1476
+ continue;
1477
+ }
1478
+ }
537
1479
 
538
1480
  /* Try mmark syntax first if enabled */
539
1481
  if (options->enable_mmark_index_syntax) {
@@ -541,9 +1483,9 @@ char *apex_process_index_entries(const char *text, apex_index_registry *registry
541
1483
  }
542
1484
 
543
1485
  /* Try TextIndex syntax if mmark didn't match and TextIndex is enabled */
544
- /* TextIndex uses {^} which we need to scan forward for */
545
- if (!entry && options->enable_textindex_syntax && *read == '{' && read + 1 < text + text_len && read[1] == '^') {
546
- consumed = parse_textindex(text, apex_idx_ptrdiff_to_int(read - text), apex_idx_size_to_int(text_len), &entry);
1486
+ if (!entry && options->enable_textindex_syntax && textindex_processing &&
1487
+ *read == '{' && read + 1 < text + text_len && read[1] == '^') {
1488
+ consumed = parse_textindex(text, apex_idx_ptrdiff_to_int(read - text), apex_idx_size_to_int(text_len), &entry, &bracketed_prefix, registry);
547
1489
  }
548
1490
 
549
1491
  /* Try Leanpub syntax if no match yet and Leanpub is enabled */
@@ -552,41 +1494,125 @@ char *apex_process_index_entries(const char *text, apex_index_registry *registry
552
1494
  consumed = parse_leanpub_index(text, apex_idx_ptrdiff_to_int(read - text), apex_idx_size_to_int(text_len), &entry);
553
1495
  }
554
1496
 
1497
+ if (consumed > 0 && !entry) {
1498
+ /* Unreferenced ##alias definition: strip brackets if any, drop mark */
1499
+ if (bracketed_prefix >= 2 && (write - output) >= bracketed_prefix) {
1500
+ char *span = write - bracketed_prefix;
1501
+ if (span[0] == '[' && span[bracketed_prefix - 1] == ']') {
1502
+ size_t inner_len = (size_t)bracketed_prefix - 2;
1503
+ if (inner_len > 0) {
1504
+ memmove(span, span + 1, inner_len);
1505
+ }
1506
+ write = span + inner_len;
1507
+ remaining += 2;
1508
+ }
1509
+ }
1510
+ read += consumed;
1511
+ continue;
1512
+ }
1513
+
555
1514
  if (entry && consumed > 0) {
1515
+ /* [term]{^}: [term] was already copied into output; strip the brackets,
1516
+ * keep the inner text, then append the index placeholder. */
1517
+ if (bracketed_prefix >= 2 && (write - output) >= bracketed_prefix) {
1518
+ char *span = write - bracketed_prefix;
1519
+ if (span[0] == '[' && span[bracketed_prefix - 1] == ']') {
1520
+ size_t inner_len = (size_t)bracketed_prefix - 2;
1521
+ if (inner_len > 0) {
1522
+ memmove(span, span + 1, inner_len);
1523
+ }
1524
+ write = span + inner_len;
1525
+ remaining += 2; /* freed [ and ] */
1526
+ }
1527
+ }
1528
+
556
1529
  /* Add entry to registry */
557
1530
  entry->position = apex_idx_ptrdiff_to_int(read - text);
558
- char anchor_id[64];
559
- snprintf(anchor_id, sizeof(anchor_id), "idxref-%d", registry->next_ref_id);
560
- entry->anchor_id = strdup(anchor_id);
561
- entry->next = registry->entries;
562
- registry->entries = entry;
563
- registry->count++;
564
- registry->next_ref_id++;
565
1531
 
566
- /* Replace with placeholder */
567
- size_t placeholder_len = strlen(INDEX_PLACEHOLDER_PREFIX) +
568
- strlen(anchor_id) +
569
- strlen(INDEX_PLACEHOLDER_SUFFIX);
1532
+ /* Apply inbound (@ / @+) cross-refs onto their target entries */
1533
+ apex_index_xref **xptr = &entry->xrefs;
1534
+ while (*xptr) {
1535
+ apex_index_xref *x = *xptr;
1536
+ if (!x->inbound) {
1537
+ xptr = &(*xptr)->next;
1538
+ continue;
1539
+ }
570
1540
 
571
- if (remaining < placeholder_len + 1) {
572
- /* Expand buffer */
573
- size_t used = write - output;
574
- capacity = (used + placeholder_len + 1) * 2;
575
- char *new_output = realloc(output, capacity);
576
- if (!new_output) {
577
- free(output);
578
- apex_index_entry_free(entry);
579
- return NULL;
1541
+ /* Find or create the external target entry */
1542
+ apex_index_entry *target = NULL;
1543
+ for (apex_index_entry *e = registry->entries; e; e = e->next) {
1544
+ char *ka = strip_html_tags(e->item);
1545
+ char *kb = strip_html_tags(x->target);
1546
+ bool same = ka && kb && strcasecmp(ka, kb) == 0 && !e->subitem;
1547
+ free(ka);
1548
+ free(kb);
1549
+ if (same) {
1550
+ target = e;
1551
+ break;
1552
+ }
580
1553
  }
581
- output = new_output;
582
- write = output + used;
583
- remaining = capacity - used;
1554
+ if (!target) {
1555
+ target = apex_index_entry_new(x->target, APEX_INDEX_TEXTINDEX);
1556
+ if (target) {
1557
+ target->suppress_locator = true;
1558
+ target->position = entry->position;
1559
+ target->next = registry->entries;
1560
+ registry->entries = target;
1561
+ registry->count++;
1562
+ }
1563
+ }
1564
+
1565
+ if (target) {
1566
+ apex_index_xref *rev = calloc(1, sizeof(apex_index_xref));
1567
+ if (rev) {
1568
+ rev->target = strdup(entry->item);
1569
+ rev->also = x->also;
1570
+ rev->inbound = false;
1571
+ rev->next = target->xrefs;
1572
+ target->xrefs = rev;
1573
+ }
1574
+ }
1575
+
1576
+ *xptr = x->next;
1577
+ free(x->target);
1578
+ free(x);
584
1579
  }
585
1580
 
586
- snprintf(write, remaining, "%s%s%s",
587
- INDEX_PLACEHOLDER_PREFIX, anchor_id, INDEX_PLACEHOLDER_SUFFIX);
588
- write += placeholder_len;
589
- remaining -= placeholder_len;
1581
+ if (!entry->suppress_locator) {
1582
+ char anchor_id[64];
1583
+ snprintf(anchor_id, sizeof(anchor_id), "idxref-%d", registry->next_ref_id);
1584
+ entry->anchor_id = strdup(anchor_id);
1585
+ registry->next_ref_id++;
1586
+
1587
+ /* Replace mark with placeholder */
1588
+ size_t placeholder_len = strlen(INDEX_PLACEHOLDER_PREFIX) +
1589
+ strlen(anchor_id) +
1590
+ strlen(INDEX_PLACEHOLDER_SUFFIX);
1591
+
1592
+ if (remaining < placeholder_len + 1) {
1593
+ size_t used = write - output;
1594
+ capacity = (used + placeholder_len + 1) * 2;
1595
+ char *new_output = realloc(output, capacity);
1596
+ if (!new_output) {
1597
+ free(output);
1598
+ apex_index_entry_free(entry);
1599
+ return NULL;
1600
+ }
1601
+ output = new_output;
1602
+ write = output + used;
1603
+ remaining = capacity - used;
1604
+ }
1605
+
1606
+ snprintf(write, remaining, "%s%s%s",
1607
+ INDEX_PLACEHOLDER_PREFIX, anchor_id, INDEX_PLACEHOLDER_SUFFIX);
1608
+ write += placeholder_len;
1609
+ remaining -= placeholder_len;
1610
+ }
1611
+ /* see-type marks: consume without inserting a locator span */
1612
+
1613
+ entry->next = registry->entries;
1614
+ registry->entries = entry;
1615
+ registry->count++;
590
1616
 
591
1617
  read += consumed;
592
1618
  } else {
@@ -613,20 +1639,60 @@ char *apex_process_index_entries(const char *text, apex_index_registry *registry
613
1639
  }
614
1640
 
615
1641
  /**
616
- * Render index markers in HTML output
1642
+ * Restore TextIndex caret placeholders left for literal marks in {^-} regions.
617
1643
  */
618
- char *apex_render_index_markers(const char *html, apex_index_registry *registry, const apex_options *options) {
619
- if (!html || !registry || registry->count == 0 || !options->enable_indices) {
1644
+ static char *apex_restore_textindex_carets(const char *html) {
1645
+ if (!html || !strstr(html, TEXTINDEX_CARET_PLACEHOLDER)) {
620
1646
  return NULL;
621
1647
  }
622
1648
 
623
1649
  size_t html_len = strlen(html);
624
- size_t capacity = html_len * 2;
1650
+ size_t ph_len = strlen(TEXTINDEX_CARET_PLACEHOLDER);
1651
+ size_t capacity = html_len + 1;
625
1652
  char *output = malloc(capacity);
626
1653
  if (!output) return NULL;
627
1654
 
628
1655
  const char *read = html;
629
1656
  char *write = output;
1657
+ while (*read) {
1658
+ if (strncmp(read, TEXTINDEX_CARET_PLACEHOLDER, ph_len) == 0) {
1659
+ *write++ = '^';
1660
+ read += ph_len;
1661
+ } else {
1662
+ *write++ = *read++;
1663
+ }
1664
+ }
1665
+ *write = '\0';
1666
+ return output;
1667
+ }
1668
+
1669
+ /**
1670
+ * Render index markers in HTML output
1671
+ */
1672
+ char *apex_render_index_markers(const char *html, apex_index_registry *registry, const apex_options *options) {
1673
+ if (!html || !options->enable_indices) {
1674
+ return NULL;
1675
+ }
1676
+
1677
+ /* Always restore caret placeholders from {^-} protected literal marks */
1678
+ char *caret_restored = apex_restore_textindex_carets(html);
1679
+ const char *work = caret_restored ? caret_restored : html;
1680
+
1681
+ if (!registry || registry->count == 0) {
1682
+ /* No markers to expand; return caret restore if any */
1683
+ return caret_restored;
1684
+ }
1685
+
1686
+ size_t html_len = strlen(work);
1687
+ size_t capacity = html_len * 2;
1688
+ char *output = malloc(capacity);
1689
+ if (!output) {
1690
+ free(caret_restored);
1691
+ return NULL;
1692
+ }
1693
+
1694
+ const char *read = work;
1695
+ char *write = output;
630
1696
  size_t remaining = capacity;
631
1697
 
632
1698
  while (*read) {
@@ -655,6 +1721,7 @@ char *apex_render_index_markers(const char *html, apex_index_registry *registry,
655
1721
  char *new_output = realloc(output, capacity);
656
1722
  if (!new_output) {
657
1723
  free(output);
1724
+ free(caret_restored);
658
1725
  return NULL;
659
1726
  }
660
1727
  output = new_output;
@@ -679,6 +1746,7 @@ char *apex_render_index_markers(const char *html, apex_index_registry *registry,
679
1746
  char *new_output = realloc(output, capacity);
680
1747
  if (!new_output) {
681
1748
  free(output);
1749
+ free(caret_restored);
682
1750
  return NULL;
683
1751
  }
684
1752
  output = new_output;
@@ -690,6 +1758,7 @@ char *apex_render_index_markers(const char *html, apex_index_registry *registry,
690
1758
  }
691
1759
 
692
1760
  *write = '\0';
1761
+ free(caret_restored);
693
1762
  return output;
694
1763
  }
695
1764
 
@@ -700,40 +1769,139 @@ static int compare_index_entries(const void *a, const void *b) {
700
1769
  const apex_index_entry *entry_a = *(const apex_index_entry **)a;
701
1770
  const apex_index_entry *entry_b = *(const apex_index_entry **)b;
702
1771
 
703
- /* Compare items (case-insensitive) */
704
- int item_cmp = strcasecmp(entry_a->item, entry_b->item);
1772
+ char *key_a = strip_html_tags(entry_a->item);
1773
+ char *key_b = strip_html_tags(entry_b->item);
1774
+ int item_cmp = strcasecmp(key_a ? key_a : "", key_b ? key_b : "");
1775
+ free(key_a);
1776
+ free(key_b);
705
1777
  if (item_cmp != 0) return item_cmp;
706
1778
 
707
1779
  /* If items are equal, compare subitems */
708
1780
  if (entry_a->subitem && entry_b->subitem) {
709
- return strcasecmp(entry_a->subitem, entry_b->subitem);
1781
+ char *sa = strip_html_tags(entry_a->subitem);
1782
+ char *sb = strip_html_tags(entry_b->subitem);
1783
+ int sub_cmp = strcasecmp(sa ? sa : "", sb ? sb : "");
1784
+ free(sa);
1785
+ free(sb);
1786
+ if (sub_cmp != 0) return sub_cmp;
710
1787
  } else if (entry_a->subitem) {
711
1788
  return 1; /* Entry with subitem comes after entry without */
712
1789
  } else if (entry_b->subitem) {
713
1790
  return -1;
714
1791
  }
715
1792
 
1793
+ /* Same heading: document order for locator merging */
1794
+ if (entry_a->position < entry_b->position) return -1;
1795
+ if (entry_a->position > entry_b->position) return 1;
716
1796
  return 0;
717
1797
  }
718
1798
 
1799
+ /**
1800
+ * True if two entries share the same main heading (for merging locators).
1801
+ */
1802
+ static bool same_index_item(const apex_index_entry *a, const apex_index_entry *b) {
1803
+ if (!a || !b || !a->item || !b->item) return false;
1804
+ char *ka = strip_html_tags(a->item);
1805
+ char *kb = strip_html_tags(b->item);
1806
+ bool same = ka && kb && strcasecmp(ka, kb) == 0;
1807
+ free(ka);
1808
+ free(kb);
1809
+ return same;
1810
+ }
1811
+
1812
+ /**
1813
+ * True if two entries share the same subitem (both NULL counts as same).
1814
+ */
1815
+ static bool same_index_subitem(const apex_index_entry *a, const apex_index_entry *b) {
1816
+ if (!a || !b) return false;
1817
+ if (!a->subitem && !b->subitem) return true;
1818
+ if (!a->subitem || !b->subitem) return false;
1819
+ char *sa = strip_html_tags(a->subitem);
1820
+ char *sb = strip_html_tags(b->subitem);
1821
+ bool same = sa && sb && strcasecmp(sa, sb) == 0;
1822
+ free(sa);
1823
+ free(sb);
1824
+ return same;
1825
+ }
1826
+
719
1827
  /**
720
1828
  * Get first letter of index term (for grouping)
721
1829
  */
722
1830
  static char get_first_letter(const char *term) {
723
1831
  if (!term || *term == '\0') return '?';
724
1832
 
725
- /* Skip leading whitespace and punctuation */
726
- while (*term && (!isalnum((unsigned char)*term))) {
1833
+ /* Skip HTML tags, whitespace, and punctuation */
1834
+ while (*term) {
1835
+ if (*term == '<') {
1836
+ while (*term && *term != '>') term++;
1837
+ if (*term == '>') term++;
1838
+ continue;
1839
+ }
1840
+ if (isalnum((unsigned char)*term)) {
1841
+ return (char)toupper((unsigned char)*term);
1842
+ }
727
1843
  term++;
728
1844
  }
729
1845
 
730
- if (*term) {
731
- return toupper((unsigned char)*term);
732
- }
733
-
734
1846
  return '?';
735
1847
  }
736
1848
 
1849
+ /**
1850
+ * Append TextIndex see / see-also run-in text for entries[start..end).
1851
+ * Deduplicates by (also, target). Uses APPEND macro from caller.
1852
+ */
1853
+ #define APPEND_INDEX_XREFS(start, end) do { \
1854
+ bool _have_see = false, _have_also = false; \
1855
+ for (size_t _xi = (start); _xi < (end); _xi++) { \
1856
+ for (apex_index_xref *_x = entries[_xi]->xrefs; _x; _x = _x->next) { \
1857
+ if (_x->inbound || !_x->target) continue; \
1858
+ if (_x->also) _have_also = true; else _have_see = true; \
1859
+ } \
1860
+ } \
1861
+ if (_have_see) { \
1862
+ APPEND(". <em>See</em> "); \
1863
+ bool _first = true; \
1864
+ for (size_t _xi = (start); _xi < (end); _xi++) { \
1865
+ for (apex_index_xref *_x = entries[_xi]->xrefs; _x; _x = _x->next) { \
1866
+ if (_x->inbound || _x->also || !_x->target) continue; \
1867
+ bool _dup = false; \
1868
+ for (size_t _yj = (start); _yj <= _xi && !_dup; _yj++) { \
1869
+ for (apex_index_xref *_y = entries[_yj]->xrefs; _y; _y = _y->next) { \
1870
+ if (_y == _x) break; \
1871
+ if (!_y->inbound && !_y->also && _y->target && \
1872
+ strcasecmp(_y->target, _x->target) == 0) { _dup = true; break; } \
1873
+ } \
1874
+ } \
1875
+ if (_dup) continue; \
1876
+ if (!_first) APPEND("; "); \
1877
+ APPEND(_x->target); \
1878
+ _first = false; \
1879
+ } \
1880
+ } \
1881
+ } \
1882
+ if (_have_also) { \
1883
+ APPEND(_have_see ? ". <em>See also</em> " : ". <em>See also</em> "); \
1884
+ bool _first = true; \
1885
+ for (size_t _xi = (start); _xi < (end); _xi++) { \
1886
+ for (apex_index_xref *_x = entries[_xi]->xrefs; _x; _x = _x->next) { \
1887
+ if (_x->inbound || !_x->also || !_x->target) continue; \
1888
+ bool _dup = false; \
1889
+ for (size_t _yj = (start); _yj <= _xi && !_dup; _yj++) { \
1890
+ for (apex_index_xref *_y = entries[_yj]->xrefs; _y; _y = _y->next) { \
1891
+ if (_y == _x) break; \
1892
+ if (!_y->inbound && _y->also && _y->target && \
1893
+ strcasecmp(_y->target, _x->target) == 0) { _dup = true; break; } \
1894
+ } \
1895
+ } \
1896
+ if (_dup) continue; \
1897
+ if (!_first) APPEND("; "); \
1898
+ APPEND(_x->target); \
1899
+ _first = false; \
1900
+ } \
1901
+ } \
1902
+ } \
1903
+ } while (0)
1904
+
737
1905
  /**
738
1906
  * Generate index HTML from collected entries
739
1907
  */
@@ -797,7 +1965,7 @@ char *apex_generate_index_html(apex_index_registry *registry, const apex_options
797
1965
  char current_letter = '\0';
798
1966
  bool in_group = false;
799
1967
 
800
- for (size_t i = 0; i < registry->count; i++) {
1968
+ for (size_t i = 0; i < registry->count; ) {
801
1969
  apex_index_entry *entry = entries[i];
802
1970
  char letter = get_first_letter(entry->item);
803
1971
 
@@ -814,35 +1982,95 @@ char *apex_generate_index_html(apex_index_registry *registry, const apex_options
814
1982
  in_group = true;
815
1983
  }
816
1984
 
817
- /* Collect all entries with same item */
1985
+ /* Merge consecutive entries with the same main heading */
1986
+ size_t start = i;
1987
+ i++;
1988
+ while (i < registry->count && same_index_item(entries[start], entries[i])) {
1989
+ i++;
1990
+ }
1991
+
818
1992
  char item_html[2048];
819
- snprintf(item_html, sizeof(item_html), "<li>\n%s", entry->item);
1993
+ snprintf(item_html, sizeof(item_html), "<li>\n%s", entries[start]->item);
820
1994
  APPEND(item_html);
821
1995
 
822
- if (entry->primary) {
823
- APPEND(" <strong>");
1996
+ for (size_t li = start; li < i; li++) {
1997
+ if (entries[li]->subitem) continue;
1998
+ if (!entries[li]->anchor_id) continue;
1999
+ char link_html[256];
2000
+ if (entries[li]->primary) {
2001
+ snprintf(link_html, sizeof(link_html),
2002
+ " <strong><a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a></strong>",
2003
+ entries[li]->anchor_id);
2004
+ } else {
2005
+ snprintf(link_html, sizeof(link_html),
2006
+ " <a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a>",
2007
+ entries[li]->anchor_id);
2008
+ }
2009
+ APPEND(link_html);
824
2010
  }
825
2011
 
826
- /* Add link */
827
- char link_html[256];
828
- snprintf(link_html, sizeof(link_html), " <a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a>",
829
- entry->anchor_id ? entry->anchor_id : "");
830
- APPEND(link_html);
831
-
832
- if (entry->primary) {
833
- APPEND("</strong>");
2012
+ size_t j = start;
2013
+ bool opened_sub = false;
2014
+ while (j < i) {
2015
+ if (!entries[j]->subitem) {
2016
+ j++;
2017
+ continue;
2018
+ }
2019
+ if (!opened_sub) {
2020
+ APPEND("<ul>\n");
2021
+ opened_sub = true;
2022
+ }
2023
+ size_t sub_start = j;
2024
+ j++;
2025
+ while (j < i && entries[j]->subitem &&
2026
+ same_index_subitem(entries[sub_start], entries[j])) {
2027
+ j++;
2028
+ }
2029
+ APPEND("<li>\n");
2030
+ APPEND(entries[sub_start]->subitem);
2031
+ for (size_t k = sub_start; k < j; k++) {
2032
+ if (!entries[k]->anchor_id) continue;
2033
+ char link_html[256];
2034
+ snprintf(link_html, sizeof(link_html),
2035
+ " <a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a>",
2036
+ entries[k]->anchor_id);
2037
+ APPEND(link_html);
2038
+ }
2039
+ APPEND("</li>\n");
834
2040
  }
835
-
836
- /* Add subitems if any */
837
- if (entry->subitem) {
838
- APPEND("<ul>\n<li>\n");
839
- APPEND(entry->subitem);
840
- snprintf(link_html, sizeof(link_html), " <a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a>",
841
- entry->anchor_id ? entry->anchor_id : "");
842
- APPEND(link_html);
843
- APPEND("</li>\n</ul>\n");
2041
+ if (opened_sub) {
2042
+ /* Also-refs with sub-entries: append as subordinate list items */
2043
+ for (size_t xi = start; xi < i; xi++) {
2044
+ for (apex_index_xref *x = entries[xi]->xrefs; x; x = x->next) {
2045
+ if (x->inbound || !x->also || !x->target) continue;
2046
+ APPEND("<li>\n<em>See also</em> ");
2047
+ APPEND(x->target);
2048
+ APPEND("</li>\n");
2049
+ }
2050
+ }
2051
+ APPEND("</ul>\n");
2052
+ /* See-refs still run-in after the list */
2053
+ bool have_see = false;
2054
+ for (size_t xi = start; xi < i; xi++) {
2055
+ for (apex_index_xref *x = entries[xi]->xrefs; x; x = x->next) {
2056
+ if (!x->inbound && !x->also && x->target) have_see = true;
2057
+ }
2058
+ }
2059
+ if (have_see) {
2060
+ APPEND(". <em>See</em> ");
2061
+ bool first = true;
2062
+ for (size_t xi = start; xi < i; xi++) {
2063
+ for (apex_index_xref *x = entries[xi]->xrefs; x; x = x->next) {
2064
+ if (x->inbound || x->also || !x->target) continue;
2065
+ if (!first) APPEND("; ");
2066
+ APPEND(x->target);
2067
+ first = false;
2068
+ }
2069
+ }
2070
+ }
2071
+ } else {
2072
+ APPEND_INDEX_XREFS(start, i);
844
2073
  }
845
-
846
2074
  APPEND("</li>\n");
847
2075
  }
848
2076
 
@@ -853,35 +2081,93 @@ char *apex_generate_index_html(apex_index_registry *registry, const apex_options
853
2081
  /* Simple list without grouping */
854
2082
  APPEND("<ul>\n");
855
2083
 
856
- for (size_t i = 0; i < registry->count; i++) {
857
- apex_index_entry *entry = entries[i];
2084
+ for (size_t i = 0; i < registry->count; ) {
2085
+ size_t start = i;
2086
+ i++;
2087
+ while (i < registry->count && same_index_item(entries[start], entries[i])) {
2088
+ i++;
2089
+ }
858
2090
 
859
2091
  char item_html[2048];
860
- snprintf(item_html, sizeof(item_html), "<li>\n%s", entry->item);
2092
+ snprintf(item_html, sizeof(item_html), "<li>\n%s", entries[start]->item);
861
2093
  APPEND(item_html);
862
2094
 
863
- if (entry->primary) {
864
- APPEND(" <strong>");
2095
+ for (size_t li = start; li < i; li++) {
2096
+ if (entries[li]->subitem) continue;
2097
+ if (!entries[li]->anchor_id) continue;
2098
+ char link_html[256];
2099
+ if (entries[li]->primary) {
2100
+ snprintf(link_html, sizeof(link_html),
2101
+ " <strong><a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a></strong>",
2102
+ entries[li]->anchor_id);
2103
+ } else {
2104
+ snprintf(link_html, sizeof(link_html),
2105
+ " <a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a>",
2106
+ entries[li]->anchor_id);
2107
+ }
2108
+ APPEND(link_html);
865
2109
  }
866
2110
 
867
- char link_html[256];
868
- snprintf(link_html, sizeof(link_html), " <a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a>",
869
- entry->anchor_id ? entry->anchor_id : "");
870
- APPEND(link_html);
871
-
872
- if (entry->primary) {
873
- APPEND("</strong>");
2111
+ size_t j = start;
2112
+ bool opened_sub = false;
2113
+ while (j < i) {
2114
+ if (!entries[j]->subitem) {
2115
+ j++;
2116
+ continue;
2117
+ }
2118
+ if (!opened_sub) {
2119
+ APPEND("<ul>\n");
2120
+ opened_sub = true;
2121
+ }
2122
+ size_t sub_start = j;
2123
+ j++;
2124
+ while (j < i && entries[j]->subitem &&
2125
+ same_index_subitem(entries[sub_start], entries[j])) {
2126
+ j++;
2127
+ }
2128
+ APPEND("<li>\n");
2129
+ APPEND(entries[sub_start]->subitem);
2130
+ for (size_t k = sub_start; k < j; k++) {
2131
+ if (!entries[k]->anchor_id) continue;
2132
+ char link_html[256];
2133
+ snprintf(link_html, sizeof(link_html),
2134
+ " <a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a>",
2135
+ entries[k]->anchor_id);
2136
+ APPEND(link_html);
2137
+ }
2138
+ APPEND("</li>\n");
874
2139
  }
875
-
876
- if (entry->subitem) {
877
- APPEND("<ul>\n<li>\n");
878
- APPEND(entry->subitem);
879
- snprintf(link_html, sizeof(link_html), " <a class=\"index-return\" href=\"#%s\"><sup>[go]</sup></a>",
880
- entry->anchor_id ? entry->anchor_id : "");
881
- APPEND(link_html);
882
- APPEND("</li>\n</ul>\n");
2140
+ if (opened_sub) {
2141
+ for (size_t xi = start; xi < i; xi++) {
2142
+ for (apex_index_xref *x = entries[xi]->xrefs; x; x = x->next) {
2143
+ if (x->inbound || !x->also || !x->target) continue;
2144
+ APPEND("<li>\n<em>See also</em> ");
2145
+ APPEND(x->target);
2146
+ APPEND("</li>\n");
2147
+ }
2148
+ }
2149
+ APPEND("</ul>\n");
2150
+ bool have_see = false;
2151
+ for (size_t xi = start; xi < i; xi++) {
2152
+ for (apex_index_xref *x = entries[xi]->xrefs; x; x = x->next) {
2153
+ if (!x->inbound && !x->also && x->target) have_see = true;
2154
+ }
2155
+ }
2156
+ if (have_see) {
2157
+ APPEND(". <em>See</em> ");
2158
+ bool first = true;
2159
+ for (size_t xi = start; xi < i; xi++) {
2160
+ for (apex_index_xref *x = entries[xi]->xrefs; x; x = x->next) {
2161
+ if (x->inbound || x->also || !x->target) continue;
2162
+ if (!first) APPEND("; ");
2163
+ APPEND(x->target);
2164
+ first = false;
2165
+ }
2166
+ }
2167
+ }
2168
+ } else {
2169
+ APPEND_INDEX_XREFS(start, i);
883
2170
  }
884
-
885
2171
  APPEND("</li>\n");
886
2172
  }
887
2173
 
@@ -891,6 +2177,7 @@ char *apex_generate_index_html(apex_index_registry *registry, const apex_options
891
2177
  APPEND("</div>\n");
892
2178
 
893
2179
  #undef APPEND
2180
+ #undef APPEND_INDEX_XREFS
894
2181
 
895
2182
  *write = '\0';
896
2183
  free(entries);