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
@@ -0,0 +1,848 @@
1
+ /*
2
+ * RTF (Rich Text Format) output.
3
+ * Serializes a cmark AST to RTF suitable for files, clipboard tooling, and
4
+ * NSAttributedString(documentType: .rtf).
5
+ */
6
+
7
+ #include "apex/ast_rtf.h"
8
+ #include "apex/apex.h"
9
+ #include "table.h"
10
+ #include "strikethrough.h"
11
+ #include "cmark-gfm-core-extensions.h"
12
+
13
+ #include <ctype.h>
14
+ #include <stdarg.h>
15
+ #include <stdint.h>
16
+ #include <stdio.h>
17
+ #include <stdlib.h>
18
+ #include <string.h>
19
+ #include <stdbool.h>
20
+
21
+ /* ------------------------------------------------------------------------- */
22
+ /* Buffer */
23
+ /* ------------------------------------------------------------------------- */
24
+
25
+ typedef struct {
26
+ char *buf;
27
+ size_t len;
28
+ size_t capacity;
29
+ } rtf_buffer;
30
+
31
+ typedef struct {
32
+ rtf_buffer *buf;
33
+ const apex_options *options;
34
+ int list_depth;
35
+ int quote_depth;
36
+ int footnote_index;
37
+ /* Collected footnote definition nodes for endnotes fallback */
38
+ cmark_node **footnote_defs;
39
+ size_t footnote_def_count;
40
+ size_t footnote_def_cap;
41
+ } rtf_ctx;
42
+
43
+ static void rtf_buf_init(rtf_buffer *b) {
44
+ b->buf = NULL;
45
+ b->len = 0;
46
+ b->capacity = 0;
47
+ }
48
+
49
+ static void rtf_buf_append(rtf_buffer *b, const char *str, size_t n) {
50
+ if (!str || n == 0) return;
51
+ if (b->len + n + 1 > b->capacity) {
52
+ size_t new_cap = b->capacity ? b->capacity * 2 : 1024;
53
+ while (new_cap < b->len + n + 1) new_cap *= 2;
54
+ char *nb = (char *)realloc(b->buf, new_cap);
55
+ if (!nb) return;
56
+ b->buf = nb;
57
+ b->capacity = new_cap;
58
+ }
59
+ memcpy(b->buf + b->len, str, n);
60
+ b->len += n;
61
+ b->buf[b->len] = '\0';
62
+ }
63
+
64
+ static void rtf_buf_append_str(rtf_buffer *b, const char *str) {
65
+ if (str) rtf_buf_append(b, str, strlen(str));
66
+ }
67
+
68
+ static void rtf_buf_append_char(rtf_buffer *b, char c) {
69
+ rtf_buf_append(b, &c, 1);
70
+ }
71
+
72
+ static void rtf_buf_printf(rtf_buffer *b, const char *fmt, ...) {
73
+ char tmp[512];
74
+ va_list ap;
75
+ va_start(ap, fmt);
76
+ int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
77
+ va_end(ap);
78
+ if (n < 0) return;
79
+ if ((size_t)n < sizeof(tmp)) {
80
+ rtf_buf_append(b, tmp, (size_t)n);
81
+ return;
82
+ }
83
+ char *big = (char *)malloc((size_t)n + 1);
84
+ if (!big) return;
85
+ va_start(ap, fmt);
86
+ vsnprintf(big, (size_t)n + 1, fmt, ap);
87
+ va_end(ap);
88
+ rtf_buf_append(b, big, (size_t)n);
89
+ free(big);
90
+ }
91
+
92
+ /* ------------------------------------------------------------------------- */
93
+ /* Escape / Unicode */
94
+ /* ------------------------------------------------------------------------- */
95
+
96
+ static void rtf_append_unicode(rtf_buffer *b, unsigned int cp) {
97
+ if (cp == 0) return;
98
+ if (cp <= 0x7F) {
99
+ char c = (char)cp;
100
+ if (c == '\\' || c == '{' || c == '}') {
101
+ rtf_buf_append_char(b, '\\');
102
+ }
103
+ if (c == '\n') {
104
+ rtf_buf_append_str(b, "\\line ");
105
+ return;
106
+ }
107
+ if (c == '\r') return;
108
+ rtf_buf_append_char(b, c);
109
+ return;
110
+ }
111
+ /* RTF \uN uses signed 16-bit; emit surrogates for > U+FFFF */
112
+ if (cp <= 0xFFFF) {
113
+ int16_t sn = (int16_t)(uint16_t)cp;
114
+ rtf_buf_printf(b, "\\u%d?", (int)sn);
115
+ return;
116
+ }
117
+ cp -= 0x10000;
118
+ unsigned int hi = 0xD800 + (cp >> 10);
119
+ unsigned int lo = 0xDC00 + (cp & 0x3FF);
120
+ rtf_buf_printf(b, "\\u%d?", (int)(int16_t)(uint16_t)hi);
121
+ rtf_buf_printf(b, "\\u%d?", (int)(int16_t)(uint16_t)lo);
122
+ }
123
+
124
+ static void rtf_append_utf8(rtf_buffer *b, const char *str, size_t len) {
125
+ if (!str || len == 0) return;
126
+ size_t i = 0;
127
+ while (i < len) {
128
+ unsigned char c = (unsigned char)str[i];
129
+ if (c < 0x80) {
130
+ rtf_append_unicode(b, c);
131
+ i++;
132
+ } else if ((c & 0xE0) == 0xC0 && i + 1 < len) {
133
+ unsigned int cp = ((c & 0x1F) << 6) | ((unsigned char)str[i + 1] & 0x3F);
134
+ rtf_append_unicode(b, cp);
135
+ i += 2;
136
+ } else if ((c & 0xF0) == 0xE0 && i + 2 < len) {
137
+ unsigned int cp = ((c & 0x0F) << 12) |
138
+ (((unsigned char)str[i + 1] & 0x3F) << 6) |
139
+ ((unsigned char)str[i + 2] & 0x3F);
140
+ rtf_append_unicode(b, cp);
141
+ i += 3;
142
+ } else if ((c & 0xF8) == 0xF0 && i + 3 < len) {
143
+ unsigned int cp = ((c & 0x07) << 18) |
144
+ (((unsigned char)str[i + 1] & 0x3F) << 12) |
145
+ (((unsigned char)str[i + 2] & 0x3F) << 6) |
146
+ ((unsigned char)str[i + 3] & 0x3F);
147
+ rtf_append_unicode(b, cp);
148
+ i += 4;
149
+ } else {
150
+ i++;
151
+ }
152
+ }
153
+ }
154
+
155
+ static void rtf_append_utf8_str(rtf_buffer *b, const char *str) {
156
+ if (str) rtf_append_utf8(b, str, strlen(str));
157
+ }
158
+
159
+ /* Strip HTML tags; decode a few entities; emit text. */
160
+ static void rtf_append_html_stripped(rtf_buffer *b, const char *html, size_t len) {
161
+ if (!html || len == 0) return;
162
+ size_t i = 0;
163
+ bool in_tag = false;
164
+ rtf_buffer text;
165
+ rtf_buf_init(&text);
166
+ while (i < len) {
167
+ char c = html[i];
168
+ if (in_tag) {
169
+ if (c == '>') in_tag = false;
170
+ i++;
171
+ continue;
172
+ }
173
+ if (c == '<') {
174
+ /* Insert space/line for block-ish tags */
175
+ if (i + 2 < len) {
176
+ if ((html[i + 1] == 'b' && html[i + 2] == 'r') ||
177
+ (html[i + 1] == 'p' && (html[i + 2] == '>' || html[i + 2] == ' ')) ||
178
+ (html[i + 1] == '/' && html[i + 2] == 'p')) {
179
+ rtf_buf_append_str(&text, "\n");
180
+ }
181
+ }
182
+ in_tag = true;
183
+ i++;
184
+ continue;
185
+ }
186
+ if (c == '&') {
187
+ if (i + 3 < len && strncmp(html + i, "&lt;", 4) == 0) {
188
+ rtf_buf_append_char(&text, '<'); i += 4; continue;
189
+ }
190
+ if (i + 3 < len && strncmp(html + i, "&gt;", 4) == 0) {
191
+ rtf_buf_append_char(&text, '>'); i += 4; continue;
192
+ }
193
+ if (i + 4 < len && strncmp(html + i, "&amp;", 5) == 0) {
194
+ rtf_buf_append_char(&text, '&'); i += 5; continue;
195
+ }
196
+ if (i + 5 < len && strncmp(html + i, "&quot;", 6) == 0) {
197
+ rtf_buf_append_char(&text, '"'); i += 6; continue;
198
+ }
199
+ if (i + 5 < len && strncmp(html + i, "&nbsp;", 6) == 0) {
200
+ rtf_buf_append_char(&text, ' '); i += 6; continue;
201
+ }
202
+ }
203
+ rtf_buf_append_char(&text, c);
204
+ i++;
205
+ }
206
+ if (text.buf) {
207
+ rtf_append_utf8(b, text.buf, text.len);
208
+ free(text.buf);
209
+ }
210
+ }
211
+
212
+ /* ------------------------------------------------------------------------- */
213
+ /* Document header */
214
+ /* ------------------------------------------------------------------------- */
215
+
216
+ static void rtf_write_header(rtf_buffer *b) {
217
+ rtf_buf_append_str(b,
218
+ "{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033\n"
219
+ "{\\fonttbl"
220
+ "{\\f0\\fswiss\\fcharset0 Helvetica;}"
221
+ "{\\f1\\fmodern\\fcharset0 Courier New;}"
222
+ "{\\f2\\froman\\fcharset0 Times New Roman;}"
223
+ "}\n"
224
+ "{\\colortbl;"
225
+ "\\red0\\green0\\blue0;"
226
+ "\\red0\\green0\\blue255;"
227
+ "\\red128\\green128\\blue128;"
228
+ "\\red245\\green245\\blue245;"
229
+ "\\red0\\green100\\blue0;"
230
+ "\\red180\\green0\\blue0;"
231
+ "}\n"
232
+ "\\viewkind4\\uc1\\pard\\f0\\fs24\n");
233
+ }
234
+
235
+ /* Half-points for heading levels 1-6 */
236
+ static int heading_fs(int level) {
237
+ static const int sizes[] = {0, 36, 32, 28, 26, 24, 22};
238
+ if (level < 1) level = 1;
239
+ if (level > 6) level = 6;
240
+ return sizes[level];
241
+ }
242
+
243
+ static int indent_twips(int depth) {
244
+ if (depth < 0) depth = 0;
245
+ if (depth > 8) depth = 8;
246
+ return depth * 360;
247
+ }
248
+
249
+ /* ------------------------------------------------------------------------- */
250
+ /* Forward decls */
251
+ /* ------------------------------------------------------------------------- */
252
+
253
+ static void render_inline(rtf_ctx *ctx, cmark_node *node);
254
+ static void render_block(rtf_ctx *ctx, cmark_node *node);
255
+ static void render_children_inline(rtf_ctx *ctx, cmark_node *node);
256
+ static void render_children_block(rtf_ctx *ctx, cmark_node *node);
257
+
258
+ static void render_children_inline(rtf_ctx *ctx, cmark_node *node) {
259
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
260
+ render_inline(ctx, c);
261
+ }
262
+
263
+ static void render_children_block(rtf_ctx *ctx, cmark_node *node) {
264
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
265
+ render_block(ctx, c);
266
+ }
267
+
268
+ /* ------------------------------------------------------------------------- */
269
+ /* Images */
270
+ /* ------------------------------------------------------------------------- */
271
+
272
+ static int hex_digit(unsigned char v) {
273
+ return v < 10 ? ('0' + v) : ('a' + (v - 10));
274
+ }
275
+
276
+ /* Local PNG/JPEG only. Remote URLs are never fetched (same policy as HTML embed).
277
+ * Unlike HTML, RTF always attempts embed for local files — no embed_images flag. */
278
+ static bool rtf_try_embed_image(rtf_ctx *ctx, const char *url) {
279
+ if (!url || !*url) return false;
280
+ if (strncmp(url, "http://", 7) == 0 || strncmp(url, "https://", 8) == 0 ||
281
+ strncmp(url, "data:", 5) == 0 || strncmp(url, "//", 2) == 0) {
282
+ return false;
283
+ }
284
+
285
+ const char *path = url;
286
+ char resolved[4096];
287
+ if (ctx->options && ctx->options->base_directory && url[0] != '/') {
288
+ snprintf(resolved, sizeof(resolved), "%s/%s", ctx->options->base_directory, url);
289
+ path = resolved;
290
+ }
291
+
292
+ FILE *fp = fopen(path, "rb");
293
+ if (!fp) return false;
294
+ if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return false; }
295
+ long sz = ftell(fp);
296
+ if (sz <= 0 || sz > 8 * 1024 * 1024) { fclose(fp); return false; }
297
+ if (fseek(fp, 0, SEEK_SET) != 0) { fclose(fp); return false; }
298
+
299
+ unsigned char *data = (unsigned char *)malloc((size_t)sz);
300
+ if (!data) { fclose(fp); return false; }
301
+ if (fread(data, 1, (size_t)sz, fp) != (size_t)sz) {
302
+ free(data);
303
+ fclose(fp);
304
+ return false;
305
+ }
306
+ fclose(fp);
307
+
308
+ const char *blip = NULL;
309
+ if (sz >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47)
310
+ blip = "pngblip";
311
+ else if (sz >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF)
312
+ blip = "jpegblip";
313
+ else {
314
+ free(data);
315
+ return false;
316
+ }
317
+
318
+ rtf_buf_printf(ctx->buf, "{\\pict\\%s\\picwgoal1440\\pichgoal1440 ", blip);
319
+ for (long i = 0; i < sz; i++) {
320
+ char hex[3];
321
+ hex[0] = (char)hex_digit(data[i] >> 4);
322
+ hex[1] = (char)hex_digit(data[i] & 0x0F);
323
+ hex[2] = '\0';
324
+ rtf_buf_append(ctx->buf, hex, 2);
325
+ if ((i & 31) == 31) rtf_buf_append_char(ctx->buf, '\n');
326
+ }
327
+ rtf_buf_append_str(ctx->buf, "}");
328
+ free(data);
329
+ return true;
330
+ }
331
+
332
+ /* ------------------------------------------------------------------------- */
333
+ /* Inlines */
334
+ /* ------------------------------------------------------------------------- */
335
+
336
+ static void rtf_append_hyperlink_url(rtf_buffer *b, const char *url) {
337
+ for (const char *p = url; *p; p++) {
338
+ if (*p == '"' || *p == '\\') rtf_buf_append_char(b, '\\');
339
+ if ((unsigned char)*p < 0x80)
340
+ rtf_buf_append_char(b, *p);
341
+ else
342
+ rtf_buf_append_char(b, *p);
343
+ }
344
+ }
345
+
346
+ static bool rtf_image_has_alt(cmark_node *node) {
347
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c)) {
348
+ cmark_node_type ct = cmark_node_get_type(c);
349
+ if (ct == CMARK_NODE_TEXT) {
350
+ const char *lit = cmark_node_get_literal(c);
351
+ if (lit) {
352
+ while (*lit == ' ' || *lit == '\t') lit++;
353
+ if (*lit) return true;
354
+ }
355
+ } else if (ct != CMARK_NODE_SOFTBREAK && ct != CMARK_NODE_LINEBREAK) {
356
+ return true;
357
+ }
358
+ }
359
+ return false;
360
+ }
361
+
362
+ static void rtf_write_hyperlink_open(rtf_buffer *b, const char *url) {
363
+ rtf_buf_append_str(b, "{\\field{\\*\\fldinst{HYPERLINK \"");
364
+ rtf_append_hyperlink_url(b, url);
365
+ rtf_buf_append_str(b, "\"}}{\\fldrslt{\\ul\\cf2 ");
366
+ }
367
+
368
+ static void rtf_write_hyperlink_close(rtf_buffer *b) {
369
+ rtf_buf_append_str(b, "\\ulnone\\cf0 }}}");
370
+ }
371
+
372
+ static void render_link(rtf_ctx *ctx, cmark_node *node, bool is_image) {
373
+ const char *url = cmark_node_get_url(node);
374
+ if (is_image) {
375
+ if (url && rtf_try_embed_image(ctx, url)) return;
376
+
377
+ /* Fallback: linked alt text (or "image"); never dump the raw URL inline. */
378
+ const char *title = cmark_node_get_title(node);
379
+ bool has_alt = rtf_image_has_alt(node);
380
+ bool has_title = title && *title;
381
+
382
+ if (url && *url) {
383
+ rtf_write_hyperlink_open(ctx->buf, url);
384
+ if (has_alt)
385
+ render_children_inline(ctx, node);
386
+ else if (has_title)
387
+ rtf_append_utf8_str(ctx->buf, title);
388
+ else
389
+ rtf_buf_append_str(ctx->buf, "image");
390
+ rtf_write_hyperlink_close(ctx->buf);
391
+ } else if (has_alt) {
392
+ render_children_inline(ctx, node);
393
+ } else if (has_title) {
394
+ rtf_append_utf8_str(ctx->buf, title);
395
+ }
396
+ return;
397
+ }
398
+
399
+ if (!url || !*url) {
400
+ render_children_inline(ctx, node);
401
+ return;
402
+ }
403
+
404
+ rtf_write_hyperlink_open(ctx->buf, url);
405
+ render_children_inline(ctx, node);
406
+ rtf_write_hyperlink_close(ctx->buf);
407
+ }
408
+
409
+ static void render_inline(rtf_ctx *ctx, cmark_node *node) {
410
+ if (!node) return;
411
+ cmark_node_type t = cmark_node_get_type(node);
412
+
413
+ if (t == CMARK_NODE_STRIKETHROUGH) {
414
+ rtf_buf_append_str(ctx->buf, "{\\strike ");
415
+ render_children_inline(ctx, node);
416
+ rtf_buf_append_str(ctx->buf, "}");
417
+ return;
418
+ }
419
+
420
+ switch (t) {
421
+ case CMARK_NODE_TEXT: {
422
+ const char *lit = cmark_node_get_literal(node);
423
+ if (lit) rtf_append_utf8_str(ctx->buf, lit);
424
+ break;
425
+ }
426
+ case CMARK_NODE_CODE: {
427
+ const char *lit = cmark_node_get_literal(node);
428
+ rtf_buf_append_str(ctx->buf, "{\\f1\\fs20 ");
429
+ if (lit) rtf_append_utf8_str(ctx->buf, lit);
430
+ rtf_buf_append_str(ctx->buf, "\\f0\\fs24}");
431
+ break;
432
+ }
433
+ case CMARK_NODE_SOFTBREAK:
434
+ rtf_buf_append_str(ctx->buf, " ");
435
+ break;
436
+ case CMARK_NODE_LINEBREAK:
437
+ rtf_buf_append_str(ctx->buf, "\\line ");
438
+ break;
439
+ case CMARK_NODE_EMPH:
440
+ rtf_buf_append_str(ctx->buf, "{\\i ");
441
+ render_children_inline(ctx, node);
442
+ rtf_buf_append_str(ctx->buf, "}");
443
+ break;
444
+ case CMARK_NODE_STRONG:
445
+ rtf_buf_append_str(ctx->buf, "{\\b ");
446
+ render_children_inline(ctx, node);
447
+ rtf_buf_append_str(ctx->buf, "}");
448
+ break;
449
+ case CMARK_NODE_LINK:
450
+ render_link(ctx, node, false);
451
+ break;
452
+ case CMARK_NODE_IMAGE:
453
+ render_link(ctx, node, true);
454
+ break;
455
+ case CMARK_NODE_HTML_INLINE: {
456
+ const char *lit = cmark_node_get_literal(node);
457
+ if (!lit) break;
458
+ /* Map common tags */
459
+ if (strncmp(lit, "<sup>", 5) == 0 || strncmp(lit, "<sup ", 5) == 0) {
460
+ rtf_buf_append_str(ctx->buf, "{\\super ");
461
+ } else if (strncmp(lit, "</sup>", 6) == 0) {
462
+ rtf_buf_append_str(ctx->buf, "}");
463
+ } else if (strncmp(lit, "<sub>", 5) == 0 || strncmp(lit, "<sub ", 5) == 0) {
464
+ rtf_buf_append_str(ctx->buf, "{\\sub ");
465
+ } else if (strncmp(lit, "</sub>", 6) == 0) {
466
+ rtf_buf_append_str(ctx->buf, "}");
467
+ } else if (strncmp(lit, "<mark>", 6) == 0 || strstr(lit, "class=\"highlight\"") ||
468
+ strstr(lit, "class='highlight'")) {
469
+ rtf_buf_append_str(ctx->buf, "{\\highlight4 ");
470
+ } else if (strncmp(lit, "</mark>", 7) == 0) {
471
+ rtf_buf_append_str(ctx->buf, "}");
472
+ } else if (lit[0] == '<' && lit[1] != '/') {
473
+ /* opening unknown tag — ignore */
474
+ } else if (lit[0] == '<' && lit[1] == '/') {
475
+ /* closing unknown — ignore */
476
+ } else {
477
+ rtf_append_html_stripped(ctx->buf, lit, strlen(lit));
478
+ }
479
+ break;
480
+ }
481
+ case CMARK_NODE_FOOTNOTE_REFERENCE: {
482
+ const char *lit = cmark_node_get_literal(node);
483
+ ctx->footnote_index++;
484
+ rtf_buf_append_str(ctx->buf, "{\\super\\cf2 ");
485
+ if (lit && *lit)
486
+ rtf_append_utf8_str(ctx->buf, lit);
487
+ else
488
+ rtf_buf_printf(ctx->buf, "%d", ctx->footnote_index);
489
+ rtf_buf_append_str(ctx->buf, "}");
490
+ /* Inline footnote content if definition follows as sibling elsewhere —
491
+ * content is emitted in endnotes pass. */
492
+ break;
493
+ }
494
+ default:
495
+ render_children_inline(ctx, node);
496
+ break;
497
+ }
498
+ }
499
+
500
+ /* ------------------------------------------------------------------------- */
501
+ /* Tables */
502
+ /* ------------------------------------------------------------------------- */
503
+
504
+ static void render_table(rtf_ctx *ctx, cmark_node *table) {
505
+ uint16_t cols = cmark_gfm_extensions_get_table_columns(table);
506
+ if (cols == 0) {
507
+ /* Count from first row */
508
+ for (cmark_node *row = cmark_node_first_child(table); row; row = cmark_node_next(row)) {
509
+ if (cmark_node_get_type(row) != CMARK_NODE_TABLE_ROW) continue;
510
+ for (cmark_node *cell = cmark_node_first_child(row); cell; cell = cmark_node_next(cell)) {
511
+ if (cmark_node_get_type(cell) == CMARK_NODE_TABLE_CELL) cols++;
512
+ }
513
+ break;
514
+ }
515
+ }
516
+ if (cols == 0) cols = 1;
517
+
518
+ int cell_width = 9000 / (int)cols;
519
+ if (cell_width < 720) cell_width = 720;
520
+
521
+ bool first_row = true;
522
+ for (cmark_node *row = cmark_node_first_child(table); row; row = cmark_node_next(row)) {
523
+ if (cmark_node_get_type(row) != CMARK_NODE_TABLE_ROW) continue;
524
+
525
+ rtf_buf_append_str(ctx->buf, "\\trowd\\trgaph108\\trleft0");
526
+ for (int i = 1; i <= (int)cols; i++) {
527
+ rtf_buf_printf(ctx->buf, "\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10"
528
+ "\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10"
529
+ "\\cellx%d", i * cell_width);
530
+ }
531
+ rtf_buf_append_str(ctx->buf, "\n");
532
+
533
+ int col = 0;
534
+ for (cmark_node *cell = cmark_node_first_child(row); cell; cell = cmark_node_next(cell)) {
535
+ if (cmark_node_get_type(cell) != CMARK_NODE_TABLE_CELL) continue;
536
+ col++;
537
+ if (first_row)
538
+ rtf_buf_append_str(ctx->buf, "\\pard\\intbl\\b ");
539
+ else
540
+ rtf_buf_append_str(ctx->buf, "\\pard\\intbl ");
541
+ /* Cell may contain block children (paragraphs) or inlines */
542
+ for (cmark_node *c = cmark_node_first_child(cell); c; c = cmark_node_next(c)) {
543
+ cmark_node_type ct = cmark_node_get_type(c);
544
+ if (ct == CMARK_NODE_PARAGRAPH)
545
+ render_children_inline(ctx, c);
546
+ else if ((ct & CMARK_NODE_TYPE_INLINE) == CMARK_NODE_TYPE_INLINE)
547
+ render_inline(ctx, c);
548
+ else
549
+ render_block(ctx, c);
550
+ }
551
+ rtf_buf_append_str(ctx->buf, "\\cell\n");
552
+ }
553
+ while (col < (int)cols) {
554
+ rtf_buf_append_str(ctx->buf, "\\pard\\intbl\\cell\n");
555
+ col++;
556
+ }
557
+ rtf_buf_append_str(ctx->buf, "\\row\n");
558
+ first_row = false;
559
+ }
560
+ rtf_buf_append_str(ctx->buf, "\\pard\\f0\\fs24\n");
561
+ }
562
+
563
+ /* ------------------------------------------------------------------------- */
564
+ /* HTML blocks: callouts, dl, generic strip */
565
+ /* ------------------------------------------------------------------------- */
566
+
567
+ static bool looks_like_callout(const char *html) {
568
+ return html && (strstr(html, "class=\"callout") || strstr(html, "class='callout") ||
569
+ strstr(html, "callout-") || strstr(html, "div class=\"note") ||
570
+ strstr(html, "markdown-alert"));
571
+ }
572
+
573
+ static void render_html_block(rtf_ctx *ctx, const char *lit, size_t len) {
574
+ if (!lit || len == 0) return;
575
+
576
+ /* Definition list as bold term + indented body */
577
+ if (len >= 4 && lit[0] == '<' && lit[1] == 'd' && lit[2] == 'l') {
578
+ const char *p = lit;
579
+ const char *end = lit + len;
580
+ while (p < end) {
581
+ const char *dt = strstr(p, "<dt");
582
+ if (!dt || dt >= end) break;
583
+ const char *dt_gt = strchr(dt, '>');
584
+ if (!dt_gt || dt_gt >= end) break;
585
+ dt_gt++;
586
+ const char *dt_end = strstr(dt_gt, "</dt>");
587
+ if (!dt_end || dt_end >= end) break;
588
+ rtf_buf_append_str(ctx->buf, "\\pard\\b ");
589
+ rtf_append_html_stripped(ctx->buf, dt_gt, (size_t)(dt_end - dt_gt));
590
+ rtf_buf_append_str(ctx->buf, "\\b0\\par\n");
591
+
592
+ const char *dd = strstr(dt_end, "<dd");
593
+ if (dd && dd < end) {
594
+ const char *dd_gt = strchr(dd, '>');
595
+ const char *dd_end = dd_gt ? strstr(dd_gt, "</dd>") : NULL;
596
+ if (dd_gt && dd_end && dd_end < end) {
597
+ dd_gt++;
598
+ rtf_buf_append_str(ctx->buf, "\\pard\\li360 ");
599
+ rtf_append_html_stripped(ctx->buf, dd_gt, (size_t)(dd_end - dd_gt));
600
+ rtf_buf_append_str(ctx->buf, "\\par\n");
601
+ p = dd_end + 5;
602
+ continue;
603
+ }
604
+ }
605
+ p = dt_end + 5;
606
+ }
607
+ rtf_buf_append_str(ctx->buf, "\\pard\\f0\\fs24\n");
608
+ return;
609
+ }
610
+
611
+ if (looks_like_callout(lit)) {
612
+ rtf_buf_append_str(ctx->buf, "\\pard\\li360\\ri360\\box\\brdrs\\brdrw15\\brdrcf3\\cbpat4\\f0\\fs22\\i ");
613
+ rtf_append_html_stripped(ctx->buf, lit, len);
614
+ rtf_buf_append_str(ctx->buf, "\\i0\\par\\pard\\f0\\fs24\n");
615
+ return;
616
+ }
617
+
618
+ /* Generic HTML: strip tags to text paragraphs */
619
+ rtf_buf_append_str(ctx->buf, "\\pard ");
620
+ rtf_append_html_stripped(ctx->buf, lit, len);
621
+ rtf_buf_append_str(ctx->buf, "\\par\n");
622
+ }
623
+
624
+ /* ------------------------------------------------------------------------- */
625
+ /* Blocks */
626
+ /* ------------------------------------------------------------------------- */
627
+
628
+ static void render_list_item(rtf_ctx *ctx, cmark_node *item, int index, bool ordered) {
629
+ int li = indent_twips(ctx->list_depth);
630
+ int fi = -360;
631
+ rtf_buf_printf(ctx->buf, "\\pard\\li%d\\fi%d ", li, fi);
632
+
633
+ bool checked = false;
634
+ bool is_task = false;
635
+ {
636
+ const char *ts = cmark_node_get_type_string(item);
637
+ if (ts && strcmp(ts, "tasklist") == 0) {
638
+ is_task = true;
639
+ checked = cmark_gfm_extensions_get_tasklist_item_checked(item);
640
+ }
641
+ }
642
+
643
+ if (is_task) {
644
+ rtf_append_utf8_str(ctx->buf, checked ? "\\u9745? " : "\\u9744? ");
645
+ } else if (ordered) {
646
+ rtf_buf_printf(ctx->buf, "%d. ", index);
647
+ } else {
648
+ rtf_buf_append_str(ctx->buf, "\\bullet ");
649
+ }
650
+
651
+ /* First paragraph's inlines on same line; subsequent blocks as paras */
652
+ bool first = true;
653
+ for (cmark_node *c = cmark_node_first_child(item); c; c = cmark_node_next(c)) {
654
+ if (cmark_node_get_type(c) == CMARK_NODE_PARAGRAPH) {
655
+ if (!first) {
656
+ rtf_buf_printf(ctx->buf, "\\par\\pard\\li%d ", li);
657
+ }
658
+ render_children_inline(ctx, c);
659
+ first = false;
660
+ } else if (cmark_node_get_type(c) == CMARK_NODE_LIST) {
661
+ rtf_buf_append_str(ctx->buf, "\\par\n");
662
+ render_block(ctx, c);
663
+ first = false;
664
+ } else {
665
+ if (!first) rtf_buf_append_str(ctx->buf, "\\par\n");
666
+ render_block(ctx, c);
667
+ first = false;
668
+ }
669
+ }
670
+ rtf_buf_append_str(ctx->buf, "\\par\n");
671
+ }
672
+
673
+ static void render_block(rtf_ctx *ctx, cmark_node *node) {
674
+ if (!node) return;
675
+ cmark_node_type t = cmark_node_get_type(node);
676
+
677
+ if (t == CMARK_NODE_TABLE) {
678
+ render_table(ctx, node);
679
+ return;
680
+ }
681
+
682
+ switch (t) {
683
+ case CMARK_NODE_DOCUMENT:
684
+ render_children_block(ctx, node);
685
+ break;
686
+
687
+ case CMARK_NODE_HEADING: {
688
+ int level = cmark_node_get_heading_level(node);
689
+ int fs = heading_fs(level);
690
+ rtf_buf_printf(ctx->buf, "\\pard\\sb240\\sa120\\b\\f0\\fs%d ", fs);
691
+ render_children_inline(ctx, node);
692
+ rtf_buf_append_str(ctx->buf, "\\b0\\fs24\\par\n");
693
+ break;
694
+ }
695
+
696
+ case CMARK_NODE_PARAGRAPH: {
697
+ cmark_node *parent = cmark_node_parent(node);
698
+ if (parent && cmark_node_get_type(parent) == CMARK_NODE_ITEM)
699
+ break; /* handled by list item */
700
+ int li = indent_twips(ctx->quote_depth);
701
+ if (li > 0)
702
+ rtf_buf_printf(ctx->buf, "\\pard\\li%d\\sb60\\sa60 ", li);
703
+ else
704
+ rtf_buf_append_str(ctx->buf, "\\pard\\sb60\\sa60 ");
705
+ render_children_inline(ctx, node);
706
+ rtf_buf_append_str(ctx->buf, "\\par\n");
707
+ break;
708
+ }
709
+
710
+ case CMARK_NODE_BLOCK_QUOTE: {
711
+ ctx->quote_depth++;
712
+ rtf_buf_append_str(ctx->buf, "\\pard\\li360\\ri360\\i\\cf3 ");
713
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c)) {
714
+ if (cmark_node_get_type(c) == CMARK_NODE_PARAGRAPH) {
715
+ render_children_inline(ctx, c);
716
+ rtf_buf_append_str(ctx->buf, "\\par\n");
717
+ } else {
718
+ render_block(ctx, c);
719
+ }
720
+ }
721
+ rtf_buf_append_str(ctx->buf, "\\i0\\cf0\\pard\n");
722
+ ctx->quote_depth--;
723
+ break;
724
+ }
725
+
726
+ case CMARK_NODE_LIST: {
727
+ bool ordered = (cmark_node_get_list_type(node) == CMARK_ORDERED_LIST);
728
+ int start = (int)cmark_node_get_list_start(node);
729
+ if (start < 1) start = 1;
730
+ ctx->list_depth++;
731
+ int idx = start;
732
+ for (cmark_node *item = cmark_node_first_child(node); item; item = cmark_node_next(item)) {
733
+ if (cmark_node_get_type(item) != CMARK_NODE_ITEM) continue;
734
+ int item_idx = cmark_node_get_item_index(item);
735
+ if (item_idx > 0) idx = item_idx;
736
+ render_list_item(ctx, item, idx, ordered);
737
+ idx++;
738
+ }
739
+ ctx->list_depth--;
740
+ rtf_buf_append_str(ctx->buf, "\\pard\\f0\\fs24\n");
741
+ break;
742
+ }
743
+
744
+ case CMARK_NODE_ITEM:
745
+ /* Handled by list */
746
+ break;
747
+
748
+ case CMARK_NODE_CODE_BLOCK: {
749
+ const char *lit = cmark_node_get_literal(node);
750
+ rtf_buf_append_str(ctx->buf, "\\pard\\li360\\cbpat4\\f1\\fs18 ");
751
+ if (lit) {
752
+ /* Emit line by line with \\par */
753
+ const char *p = lit;
754
+ while (*p) {
755
+ const char *nl = strchr(p, '\n');
756
+ if (!nl) {
757
+ rtf_append_utf8_str(ctx->buf, p);
758
+ break;
759
+ }
760
+ rtf_append_utf8(ctx->buf, p, (size_t)(nl - p));
761
+ rtf_buf_append_str(ctx->buf, "\\par\n");
762
+ p = nl + 1;
763
+ }
764
+ }
765
+ rtf_buf_append_str(ctx->buf, "\\par\\pard\\f0\\fs24\\cbpat0\n");
766
+ break;
767
+ }
768
+
769
+ case CMARK_NODE_THEMATIC_BREAK:
770
+ rtf_buf_append_str(ctx->buf, "\\pard\\qc\\sb200\\sa200 ");
771
+ rtf_append_utf8_str(ctx->buf, "────────────");
772
+ rtf_buf_append_str(ctx->buf, "\\par\\pard\n");
773
+ break;
774
+
775
+ case CMARK_NODE_HTML_BLOCK: {
776
+ const char *lit = cmark_node_get_literal(node);
777
+ if (lit) render_html_block(ctx, lit, strlen(lit));
778
+ break;
779
+ }
780
+
781
+ case CMARK_NODE_FOOTNOTE_DEFINITION: {
782
+ /* Defer to endnotes — collect pointer */
783
+ if (ctx->footnote_def_count + 1 > ctx->footnote_def_cap) {
784
+ size_t nc = ctx->footnote_def_cap ? ctx->footnote_def_cap * 2 : 8;
785
+ cmark_node **n = (cmark_node **)realloc(ctx->footnote_defs, nc * sizeof(cmark_node *));
786
+ if (!n) break;
787
+ ctx->footnote_defs = n;
788
+ ctx->footnote_def_cap = nc;
789
+ }
790
+ ctx->footnote_defs[ctx->footnote_def_count++] = node;
791
+ break;
792
+ }
793
+
794
+ default:
795
+ render_children_block(ctx, node);
796
+ break;
797
+ }
798
+ }
799
+
800
+ static void render_endnotes(rtf_ctx *ctx) {
801
+ if (ctx->footnote_def_count == 0) return;
802
+ rtf_buf_append_str(ctx->buf, "\\pard\\sb400\\sa200\\b\\fs28 Footnotes\\b0\\fs24\\par\n");
803
+ for (size_t i = 0; i < ctx->footnote_def_count; i++) {
804
+ cmark_node *def = ctx->footnote_defs[i];
805
+ const char *lab = cmark_node_get_literal(def);
806
+ rtf_buf_append_str(ctx->buf, "\\pard\\li360\\fi-360\\super ");
807
+ if (lab && *lab)
808
+ rtf_append_utf8_str(ctx->buf, lab);
809
+ else
810
+ rtf_buf_printf(ctx->buf, "%zu", i + 1);
811
+ rtf_buf_append_str(ctx->buf, "\\nosupersub ");
812
+ for (cmark_node *c = cmark_node_first_child(def); c; c = cmark_node_next(c)) {
813
+ if (cmark_node_get_type(c) == CMARK_NODE_PARAGRAPH)
814
+ render_children_inline(ctx, c);
815
+ else
816
+ render_block(ctx, c);
817
+ }
818
+ rtf_buf_append_str(ctx->buf, "\\par\n");
819
+ }
820
+ }
821
+
822
+ /* ------------------------------------------------------------------------- */
823
+ /* Public API */
824
+ /* ------------------------------------------------------------------------- */
825
+
826
+ char *apex_cmark_to_rtf(cmark_node *document, const struct apex_options *options) {
827
+ if (!document) return NULL;
828
+
829
+ rtf_buffer buf;
830
+ rtf_buf_init(&buf);
831
+ rtf_ctx ctx;
832
+ memset(&ctx, 0, sizeof(ctx));
833
+ ctx.buf = &buf;
834
+ ctx.options = options;
835
+
836
+ rtf_write_header(&buf);
837
+ render_block(&ctx, document);
838
+ render_endnotes(&ctx);
839
+ rtf_buf_append_str(&buf, "}\n");
840
+
841
+ free(ctx.footnote_defs);
842
+
843
+ if (!buf.buf) {
844
+ char *empty = strdup("{\\rtf1\\ansi\\deff0\\pard\\par}\n");
845
+ return empty;
846
+ }
847
+ return buf.buf;
848
+ }