@gmickel/gno 1.46.0 → 2.1.0

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 (235) hide show
  1. package/README.md +17 -5
  2. package/THIRD_PARTY_NOTICES.md +46 -0
  3. package/assets/skill/SKILL.md +7 -6
  4. package/assets/spa-production.json.gz +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/browser-extension/dist/preview.html +1 -1
  10. package/browser-extension/dist/service-worker.js +32 -33
  11. package/bunfig.toml +2 -0
  12. package/package.json +40 -26
  13. package/spec/cli.md +29 -4
  14. package/spec/db/schema.sql +146 -1
  15. package/spec/mcp.md +26 -0
  16. package/src/app/context-runtime-types.ts +3 -0
  17. package/src/app/context-runtime.ts +2 -0
  18. package/src/cli/commands/ask.ts +6 -1
  19. package/src/cli/commands/daemon.ts +21 -8
  20. package/src/cli/commands/embed.ts +77 -41
  21. package/src/cli/detach.ts +3 -2
  22. package/src/config/types.ts +3 -3
  23. package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
  24. package/src/converters/adapters/officeparser/adapter.ts +1 -2
  25. package/src/converters/versions.ts +6 -8
  26. package/src/core/context-evidence.ts +8 -4
  27. package/src/core/job-manager.ts +95 -13
  28. package/src/core/network-boundary-inventory.ts +10 -0
  29. package/src/core/shutdown-budget.ts +45 -0
  30. package/src/embed/backlog.ts +107 -4
  31. package/src/embed/batch.ts +42 -2
  32. package/src/embed/fingerprint.ts +16 -0
  33. package/src/embed/retry.ts +113 -5
  34. package/src/embed/variant-backlog.ts +105 -0
  35. package/src/embed/variant-plan.ts +62 -0
  36. package/src/embed/variant-retry.ts +113 -0
  37. package/src/ingestion/graph-reconciliation.ts +327 -0
  38. package/src/ingestion/sync.ts +9 -272
  39. package/src/llm/http-inference.ts +6 -0
  40. package/src/llm/httpEmbedding.ts +37 -6
  41. package/src/llm/httpGeneration.ts +18 -3
  42. package/src/llm/httpRerank.ts +23 -5
  43. package/src/llm/inference-cancellation.ts +168 -0
  44. package/src/llm/inference-scope.ts +202 -0
  45. package/src/llm/lazy-ports.ts +115 -0
  46. package/src/llm/native-worker/client.ts +541 -0
  47. package/src/llm/native-worker/dispatcher.ts +228 -0
  48. package/src/llm/native-worker/embedding-identity.ts +33 -0
  49. package/src/llm/native-worker/entry.ts +173 -0
  50. package/src/llm/native-worker/errors.ts +32 -0
  51. package/src/llm/native-worker/evaluation.ts +16 -0
  52. package/src/llm/native-worker/owned-exit.ts +108 -0
  53. package/src/llm/native-worker/owner.ts +141 -0
  54. package/src/llm/native-worker/ports.ts +317 -0
  55. package/src/llm/native-worker/protocol.ts +442 -0
  56. package/src/llm/native-worker/runtime-config.ts +92 -0
  57. package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
  58. package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
  59. package/src/llm/nodeLlamaCpp/generation.ts +34 -5
  60. package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
  61. package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
  62. package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
  63. package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
  64. package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
  65. package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
  66. package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
  67. package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
  68. package/src/llm/types.ts +35 -5
  69. package/src/mcp/context.ts +27 -0
  70. package/src/mcp/http-transport.ts +12 -10
  71. package/src/mcp/server.ts +3 -0
  72. package/src/mcp/tool-profile.ts +30 -8
  73. package/src/mcp/tools/context.ts +8 -11
  74. package/src/mcp/tools/embed.ts +1 -1
  75. package/src/mcp/tools/index-cmd.ts +1 -1
  76. package/src/mcp/tools/index.ts +10 -8
  77. package/src/mcp/tools/query.ts +14 -30
  78. package/src/mcp/tools/vsearch.ts +1 -1
  79. package/src/pipeline/answer.ts +23 -3
  80. package/src/pipeline/claim-verifier.ts +6 -0
  81. package/src/pipeline/expansion.ts +43 -40
  82. package/src/pipeline/explain.ts +6 -2
  83. package/src/pipeline/filters.ts +63 -0
  84. package/src/pipeline/fusion.ts +29 -9
  85. package/src/pipeline/graph-retrieval.ts +29 -9
  86. package/src/pipeline/hybrid.ts +198 -55
  87. package/src/pipeline/hydration.ts +161 -0
  88. package/src/pipeline/owner-fusion.ts +87 -0
  89. package/src/pipeline/rerank.ts +35 -11
  90. package/src/pipeline/search.ts +13 -2
  91. package/src/pipeline/types.ts +5 -3
  92. package/src/pipeline/vsearch.ts +87 -7
  93. package/src/sdk/client.ts +47 -3
  94. package/src/sdk/embed.ts +63 -39
  95. package/src/serve/background-runtime.ts +1 -1
  96. package/src/serve/context.ts +41 -56
  97. package/src/serve/embed-scheduler.ts +58 -35
  98. package/src/serve/public/components/IndexingProgress.tsx +46 -60
  99. package/src/serve/public/components/PublishExportDialog.tsx +266 -0
  100. package/src/serve/public/globals.built.css +1 -1
  101. package/src/serve/public/globals.css +35 -0
  102. package/src/serve/public/lib/publish-export.ts +81 -1
  103. package/src/serve/public/lib/shiki-language-ids.ts +14 -0
  104. package/src/serve/public/pages/Collections.tsx +12 -46
  105. package/src/serve/public/pages/DocView.tsx +14 -52
  106. package/src/serve/resident-admission.ts +36 -36
  107. package/src/serve/resident-background-work.ts +20 -2
  108. package/src/serve/resident-request.ts +11 -5
  109. package/src/serve/resident-runtime.ts +97 -61
  110. package/src/serve/resident-shutdown.ts +153 -0
  111. package/src/serve/routes/api.ts +3 -1
  112. package/src/serve/server.ts +47 -26
  113. package/src/store/migrations/028-vector-variants.ts +54 -0
  114. package/src/store/migrations/029-graph-reference-state.ts +77 -0
  115. package/src/store/migrations/index.ts +4 -0
  116. package/src/store/sqlite/adapter.ts +251 -183
  117. package/src/store/sqlite/eligibility.ts +174 -0
  118. package/src/store/sqlite/graph-edge-application.ts +66 -0
  119. package/src/store/sqlite/graph-reference-state.ts +194 -0
  120. package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
  121. package/src/store/types.ts +80 -12
  122. package/src/store/vector/eligibility.ts +36 -0
  123. package/src/store/vector/freshness.ts +33 -6
  124. package/src/store/vector/lazy.ts +81 -0
  125. package/src/store/vector/sqlite-vec.ts +106 -54
  126. package/src/store/vector/stats.ts +14 -3
  127. package/src/store/vector/types.ts +35 -2
  128. package/src/store/vector/variant-search.ts +192 -0
  129. package/src/store/vector/variants.ts +451 -0
  130. package/vendor/converters/markitdown-ts/LICENSE +21 -0
  131. package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
  132. package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
  133. package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
  134. package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
  135. package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
  136. package/vendor/converters/markitdown-ts/package.json +77 -0
  137. package/vendor/converters/officeparser/LICENSE +21 -0
  138. package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
  139. package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
  140. package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
  141. package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
  142. package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
  143. package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
  144. package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
  145. package/vendor/converters/officeparser/dist/cli.js +381 -0
  146. package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
  147. package/vendor/converters/officeparser/dist/defaults.js +218 -0
  148. package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
  149. package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
  150. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
  151. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
  152. package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
  153. package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
  154. package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
  155. package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
  156. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
  157. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
  158. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
  159. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
  160. package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
  161. package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
  162. package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
  163. package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
  164. package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
  165. package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
  166. package/vendor/converters/officeparser/dist/index.d.ts +60 -0
  167. package/vendor/converters/officeparser/dist/index.js +72 -0
  168. package/vendor/converters/officeparser/dist/index.mjs +18 -0
  169. package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
  170. package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
  171. package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
  172. package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
  173. package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
  174. package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
  175. package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
  176. package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
  177. package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
  178. package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
  179. package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
  180. package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
  181. package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
  182. package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
  183. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
  184. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
  185. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
  186. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
  187. package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
  188. package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
  189. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
  190. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
  191. package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
  192. package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
  193. package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
  194. package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
  195. package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
  196. package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
  197. package/vendor/converters/officeparser/dist/types.js +107 -0
  198. package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
  199. package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
  200. package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
  201. package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
  202. package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
  203. package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
  204. package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
  205. package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
  206. package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
  207. package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
  208. package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
  209. package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
  210. package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
  211. package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
  212. package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
  213. package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
  214. package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
  215. package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
  216. package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
  217. package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
  218. package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
  219. package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
  220. package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
  221. package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
  222. package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
  223. package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
  224. package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
  225. package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
  226. package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
  227. package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
  228. package/vendor/converters/officeparser/package.json +147 -0
  229. package/vendor/converters/upstream-manifest.json +124 -0
  230. package/vendor/dependency-fixes/README.md +77 -0
  231. package/vendor/dependency-fixes/vendor-converters.py +83 -0
  232. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip +0 -0
  233. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip.sha256 +0 -1
  234. package/browser-extension/dist/chunk-627emwpj.js +0 -75
  235. /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
@@ -0,0 +1,1175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MarkdownGenerator = void 0;
4
+ const sanitize_js_1 = require("../utils/sanitize.js");
5
+ const BaseGenerator_js_1 = require("./BaseGenerator.js");
6
+ const errorUtils_js_1 = require("../utils/errorUtils.js");
7
+ /**
8
+ * Values accepted for an attribute-list `align=`. Matches what `MarkdownParser`'s own
9
+ * `parseAttributeList` allowlists on import (plus `justify`, which HTML sources can supply),
10
+ * so this is lossless for anything the parser produced.
11
+ */
12
+ const MD_ALIGN_VALUES = new Set(['left', 'center', 'right', 'justify']);
13
+ /** A CSS length or percentage - the only shape `width=` legitimately carries. */
14
+ const MD_LENGTH_PATTERN = /^\d+(?:\.\d+)?(?:px|%|em|rem|pt|pc|in|cm|mm|ex|ch|vw|vh)?$/;
15
+ /** Admonition kinds, mirroring the union declared on `AdmonitionMetadata` in types.ts. */
16
+ const MD_ADMONITION_TYPES = new Set(['note', 'tip', 'important', 'warning', 'caution']);
17
+ /**
18
+ * Folds line breaks to spaces.
19
+ *
20
+ * Used on values that sit inside a single-line construct (an abbreviation definition, an
21
+ * admonition's bold title). A raw newline there does not merely look wrong: it terminates the
22
+ * construct and exposes whatever follows as document-level Markdown.
23
+ */
24
+ const foldLines = (value) => String(value ?? '').replace(/[\r\n]+/g, ' ');
25
+ /**
26
+ * Named Markdown dialect presets. `extended` reproduces this library's historical output
27
+ * exactly (every feature on, GitHub-style admonitions) - the backward-compatibility anchor.
28
+ */
29
+ const MARKDOWN_DIALECT_PRESETS = {
30
+ extended: { admonitions: 'blockquote', definitionLists: 'colon', footnotes: 'caret', citations: 'at', wikilinks: 'double-bracket', math: 'dollar', attributeLists: 'brace', strikethrough: 'tilde', highlight: 'equals', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
31
+ github: { admonitions: 'blockquote', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'none', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
32
+ gitlab: { admonitions: 'fence', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'none', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
33
+ obsidian: { admonitions: 'blockquote', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'double-bracket', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'equals', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
34
+ pandoc: { admonitions: 'fence-attribute', definitionLists: 'colon', footnotes: 'caret', citations: 'at', wikilinks: 'none', math: 'dollar', attributeLists: 'brace', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
35
+ commonmark: { admonitions: 'none', definitionLists: 'none', footnotes: 'none', citations: 'none', wikilinks: 'none', math: 'none', attributeLists: 'none', strikethrough: 'none', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'html' },
36
+ };
37
+ /**
38
+ * Normalizes `MdGeneratorConfig.dialect` into a fully-resolved preset. A string names a preset
39
+ * directly; an object's `extends` field (default `'extended'`) names the base preset that any
40
+ * omitted field falls back to - NOT "whatever preset was ambient before", since config merging
41
+ * replaces the whole `dialect` field rather than layering an object on top of a prior string.
42
+ */
43
+ /**
44
+ * Coerces a per-capability field to its canonical syntax variant. An omitted value inherits `base`;
45
+ * a deprecated boolean maps `true` -> `onValue` and `false` -> `'none'` (the two are the only legacy
46
+ * inputs, dropped next major); an explicit syntax string passes through unchanged.
47
+ */
48
+ function resolveToggle(value, onValue, base) {
49
+ if (value === undefined)
50
+ return base;
51
+ if (value === true)
52
+ return onValue;
53
+ if (value === false)
54
+ return 'none';
55
+ return value;
56
+ }
57
+ /** Maps the deprecated admonition flavor aliases to their syntax names; passes syntax names through. */
58
+ function resolveAdmonitions(value, base) {
59
+ switch (value) {
60
+ case undefined: return base;
61
+ case 'github': return 'blockquote';
62
+ case 'gitlab': return 'fence';
63
+ case 'pandoc': return 'fence-attribute';
64
+ default: return value;
65
+ }
66
+ }
67
+ function resolveDialect(dialect) {
68
+ if (dialect === undefined)
69
+ return MARKDOWN_DIALECT_PRESETS.extended;
70
+ if (typeof dialect === 'string')
71
+ return MARKDOWN_DIALECT_PRESETS[dialect] ?? MARKDOWN_DIALECT_PRESETS.extended;
72
+ const base = MARKDOWN_DIALECT_PRESETS[dialect.extends ?? 'extended'] ?? MARKDOWN_DIALECT_PRESETS.extended;
73
+ return {
74
+ admonitions: resolveAdmonitions(dialect.admonitions, base.admonitions),
75
+ definitionLists: resolveToggle(dialect.definitionLists, 'colon', base.definitionLists),
76
+ footnotes: resolveToggle(dialect.footnotes, 'caret', base.footnotes),
77
+ citations: resolveToggle(dialect.citations, 'at', base.citations),
78
+ wikilinks: resolveToggle(dialect.wikilinks, 'double-bracket', base.wikilinks),
79
+ math: dialect.math ?? base.math,
80
+ attributeLists: resolveToggle(dialect.attributeLists, 'brace', base.attributeLists),
81
+ strikethrough: resolveToggle(dialect.strikethrough, 'tilde', base.strikethrough),
82
+ highlight: dialect.highlight ?? base.highlight,
83
+ bulletListMarker: dialect.bulletListMarker ?? base.bulletListMarker,
84
+ orderedListMarker: dialect.orderedListMarker ?? base.orderedListMarker,
85
+ emphasisMarker: dialect.emphasisMarker ?? base.emphasisMarker,
86
+ tables: dialect.tables ?? base.tables,
87
+ };
88
+ }
89
+ /**
90
+ * Normalizes `MdGeneratorConfig.fallbackToHtml` into a fully resolved object, mirroring
91
+ * `HtmlGenerator`'s `resolveStandalone()` pattern: `true`/undefined turns every part on; `false`
92
+ * turns every part off; an object's omitted fields default to on.
93
+ */
94
+ function resolveFallbackToHtml(fallbackToHtml) {
95
+ const uniform = (on) => ({
96
+ // inlineFormatting is opt-in only: it is never enabled by the boolean form, since it changes
97
+ // default output. Every other field follows the boolean.
98
+ textFormatting: on, alignment: on, anchors: on, tables: on, embeds: on, cellLineBreaks: on,
99
+ itemLineBreaks: on,
100
+ inlineFormatting: false,
101
+ });
102
+ if (fallbackToHtml === undefined || typeof fallbackToHtml === 'boolean')
103
+ return uniform(fallbackToHtml ?? true);
104
+ const on = uniform(true);
105
+ return {
106
+ textFormatting: fallbackToHtml.textFormatting ?? on.textFormatting,
107
+ alignment: fallbackToHtml.alignment ?? on.alignment,
108
+ anchors: fallbackToHtml.anchors ?? on.anchors,
109
+ tables: fallbackToHtml.tables ?? on.tables,
110
+ embeds: fallbackToHtml.embeds ?? on.embeds,
111
+ cellLineBreaks: fallbackToHtml.cellLineBreaks ?? on.cellLineBreaks,
112
+ itemLineBreaks: fallbackToHtml.itemLineBreaks ?? on.itemLineBreaks,
113
+ inlineFormatting: fallbackToHtml.inlineFormatting ?? on.inlineFormatting,
114
+ };
115
+ }
116
+ /**
117
+ * Generates Markdown from an AST.
118
+ *
119
+ * DESIGN PRINCIPLES:
120
+ * 1. **Strict Native Preference**: Always utilize native Markdown syntax for features that
121
+ * are natively supported (headings, lists, bold/italic, etc.). HTML tags should NEVER
122
+ * be used for these features.
123
+ *
124
+ * 2. **Fidelity vs. Purity (The `fallbackToHtml` Principle)**:
125
+ * - When a given `fallbackToHtml` part is TRUE: The generator prioritizes high-fidelity
126
+ * document conversion for that part. It will use HTML tags for features that Markdown
127
+ * cannot natively represent (e.g., `<u>` for underline, `<div>` for alignment, `<table>`
128
+ * for nested structures or merged cells).
129
+ * - When FALSE: The generator prioritizes "pure" Markdown for that part.
130
+ * Unsupported features are either:
131
+ * - **Skipped**: Non-essential formatting like underline, subscript, superscript,
132
+ * or text alignment is omitted.
133
+ * - **Simplified/Hoisted**: Complex structures like nested tables are hoisted out
134
+ * of their parent cells and rendered as separate sequential tables to maintain
135
+ * valid Markdown syntax.
136
+ *
137
+ * 3. **Consistency**: All similar structural or formatting ideological problems must be
138
+ * resolved using these same rules to ensure predictable output.
139
+ *
140
+ * 4. **Dialect (`MdGeneratorConfig.dialect`)**: A second, independent axis from `fallbackToHtml` -
141
+ * which *native* Markdown syntax to emit for constructs with more than one real-world
142
+ * convention (admonitions, definition lists, footnotes, citations, wikilinks, math, list/
143
+ * emphasis markers, tables). See `resolveDialect()` and `MARKDOWN_DIALECT_PRESETS` above.
144
+ */
145
+ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
146
+ isInsideTable = false;
147
+ /**
148
+ * Set while rendering the children of a heading, or the cells of a table's header row.
149
+ *
150
+ * Markdown already conveys "this is a heading" with `#` and "this is a header row" with the
151
+ * separator line, so a run inside one that also carries bold - the normal case for ODF, whose
152
+ * heading and header-row paragraph styles are bold and are now inherited by their runs - would
153
+ * render as `# **Heading**` and `| **ITEM** |`. That is redundant rather than wrong, but it
154
+ * also round-trips back into bold text nodes nested inside a heading, so the noise compounds
155
+ * on every parse/generate cycle. Emphasis the node type already implies is dropped; every
156
+ * other formatting flag still comes through.
157
+ */
158
+ inImplicitBold = false;
159
+ hoistedContent = [];
160
+ collectedAbbreviations = new Map();
161
+ resolvedDialect;
162
+ resolvedFallbackToHtml;
163
+ resolvedEmbeds;
164
+ constructor(ast, config) {
165
+ super('md', ast, config);
166
+ this.resolvedDialect = resolveDialect(this.config.mdConfig.dialect);
167
+ this.resolvedFallbackToHtml = resolveFallbackToHtml(this.config.mdConfig.fallbackToHtml);
168
+ // `dialect.embeds` is the authority for embed form. It lives in a different config object
169
+ // than the deprecated `fallbackToHtml.embeds` boolean, and an explicit boolean `false` must
170
+ // still win over the preset default, so it is resolved here rather than in `resolveDialect`:
171
+ // an embeds value set on the dialect OBJECT wins; otherwise the boolean maps (`true`/unset ->
172
+ // `'html'`, `false` -> `'link'`); otherwise the default `'html'`.
173
+ const dialectCfg = this.config.mdConfig.dialect;
174
+ const explicitEmbeds = (dialectCfg && typeof dialectCfg === 'object') ? dialectCfg.embeds : undefined;
175
+ this.resolvedEmbeds = explicitEmbeds ?? (this.resolvedFallbackToHtml.embeds ? 'html' : 'link');
176
+ }
177
+ /**
178
+ * Renders anchor tags if HTML fallback is allowed.
179
+ */
180
+ renderAnchors(metadata) {
181
+ if (!this.resolvedFallbackToHtml.anchors || this.config.ignoreInternalLinks)
182
+ return '';
183
+ const ids = metadata?.anchorIds || [];
184
+ return ids.map((aid) => `<a id="${this.slugify(aid)}"></a>`).join('');
185
+ }
186
+ /**
187
+ * Serializes a frontmatter array as a YAML flow sequence (e.g. `[a, b]`), matching
188
+ * MarkdownParser's frontmatter array handling. Plain strings are left bare; anything
189
+ * that would break flow-array syntax (or isn't a string) falls back to JSON encoding.
190
+ */
191
+ serializeFrontmatterArray(arr) {
192
+ const items = arr.map(item => (typeof item === 'string' && item.trim() === item && !/[,[\]]/.test(item))
193
+ ? item
194
+ : JSON.stringify(item));
195
+ return `[${items.join(', ')}]`;
196
+ }
197
+ /**
198
+ * Renders a Pandoc-style attribute list (e.g. `{width=50% align=left}`) from
199
+ * ImageMetadata/TableMetadata's width/align fields - the canonical form is always
200
+ * `key=value`, matching MarkdownParser's own vocabulary (MARKDOWN_DIALECT.md §15).
201
+ */
202
+ renderAttributeList(meta, options = {}) {
203
+ if (this.resolvedDialect.attributeLists === 'none')
204
+ return '';
205
+ if (!meta)
206
+ return '';
207
+ const align = options.skipAlign ? undefined : meta.align;
208
+ if (!meta.width && !align)
209
+ return '';
210
+ const parts = [];
211
+ // Allowlist, not escape. These land in `metadata.width`/`align` on reparse, which the
212
+ // parser does NOT entity-decode, so encoding here would not round-trip - and stripping
213
+ // alone is not enough: the previous `[{}\s]+` guard removed whitespace, which stops
214
+ // `<img src=x onerror=…>` but not the slash-separated `<img/src=x/onerror=…>`.
215
+ // Both values have a small, fully-known shape, so matching that shape is both safer and
216
+ // lossless for anything a parser can produce.
217
+ //
218
+ // (`isValidContainerWidth` in utils/configUtils.ts is a near-identical regex, but it is a
219
+ // config validator that also accepts 'auto' and numbers; importing configUtils here for
220
+ // one pattern would be a worse coupling than this local constant.)
221
+ if (meta.width && MD_LENGTH_PATTERN.test(String(meta.width).trim())) {
222
+ parts.push(`width=${String(meta.width).trim()}`);
223
+ }
224
+ if (align && MD_ALIGN_VALUES.has(String(align).trim().toLowerCase())) {
225
+ parts.push(`align=${String(align).trim().toLowerCase()}`);
226
+ }
227
+ if (parts.length === 0)
228
+ return '';
229
+ return `{${parts.join(' ')}}`;
230
+ }
231
+ /** Converts a document-supplied date to an ISO string, or '' if invalid
232
+ * (a malformed date would otherwise throw a RangeError and abort generation). */
233
+ toIsoDate(value) {
234
+ if (value === undefined || value === null || value === '')
235
+ return '';
236
+ const d = new Date(value);
237
+ return isNaN(d.getTime()) ? '' : d.toISOString();
238
+ }
239
+ /**
240
+ * Generates Markdown string from the provided AST.
241
+ *
242
+ * @returns A Markdown string
243
+ */
244
+ async generate() {
245
+ let output = '';
246
+ // Add Metadata (YAML Front Matter)
247
+ const meta = this.effectiveMetadata;
248
+ if (meta) {
249
+ // Build the field lines first. JSON-encode scalar values so a title/author/description
250
+ // containing a quote or newline can't break out of the YAML string and inject arbitrary
251
+ // front-matter keys. (JSON.stringify of a benign value yields the same `"..."` form as
252
+ // before, so normal output is unchanged.)
253
+ let fields = '';
254
+ if (meta.title)
255
+ fields += `title: ${JSON.stringify(meta.title)}\n`;
256
+ if (meta.author)
257
+ fields += `author: ${JSON.stringify(meta.author)}\n`;
258
+ const createdIso = this.toIsoDate(meta.created);
259
+ if (createdIso)
260
+ fields += `created: ${createdIso}\n`;
261
+ const modifiedIso = this.toIsoDate(meta.modified);
262
+ if (modifiedIso)
263
+ fields += `modified: ${modifiedIso}\n`;
264
+ if (meta.description)
265
+ fields += `description: ${JSON.stringify(meta.description)}\n`;
266
+ if (meta.subject)
267
+ fields += `subject: ${JSON.stringify(meta.subject)}\n`;
268
+ if (meta.keywords)
269
+ fields += `keywords: ${JSON.stringify(meta.keywords)}\n`;
270
+ if (meta.customProperties) {
271
+ for (const [key, val] of Object.entries(meta.customProperties)) {
272
+ // Strip newlines/colons from the key so it can't inject a new mapping.
273
+ const safeKey = String(key).replace(/[\r\n:]+/g, ' ').trim();
274
+ fields += `${safeKey}: ${Array.isArray(val) ? this.serializeFrontmatterArray(val) : JSON.stringify(val)}\n`;
275
+ }
276
+ }
277
+ // Only emit the frontmatter fence when at least one field is present. Empty metadata
278
+ // (a bare Tiptap/HTML fragment with no <head>) would otherwise emit `---\n---`, which
279
+ // reparses as a setext `## ---` heading and corrupts the document on every save/reload.
280
+ if (fields) {
281
+ output += `---\n${fields}---\n\n`;
282
+ }
283
+ }
284
+ const processor = async (node, childrenOutput) => {
285
+ // Handle Style Mapping for Markdown using the semantic mapping helper
286
+ const mapping = this.getSemanticMapping(node);
287
+ if (mapping) {
288
+ // Map common HTML tags to Markdown equivalents
289
+ if (mapping.tag === 'blockquote')
290
+ return `> ${childrenOutput}\n\n`;
291
+ if (mapping.tag === 'code')
292
+ return `\`${childrenOutput}\` `;
293
+ if (mapping.tag === 'pre')
294
+ return `\`\`\`\n${childrenOutput}\n\`\`\`\n\n`;
295
+ const hMatch = mapping.tag.match(/^h([1-6])$/);
296
+ if (hMatch) {
297
+ const level = parseInt(hMatch[1]);
298
+ return `${'#'.repeat(level)} ${childrenOutput}\n\n`;
299
+ }
300
+ }
301
+ switch (node.type) {
302
+ case 'text': {
303
+ // Entity-encode angle brackets so document text can't inject a raw
304
+ // HTML tag (e.g. <script>) when the Markdown is rendered to HTML.
305
+ let text = (0, sanitize_js_1.markdownEscapeText)(node.text || '');
306
+ if (this.config.includeFormatting && node.formatting) {
307
+ // Inline code: re-wrap the RAW text in backticks. The content is literal
308
+ // inside a code span, so the entity-escaped form above must not show through.
309
+ // The fence is one backtick longer than the longest embedded run so an inner
310
+ // backtick can't close the span early, padded when the content touches a
311
+ // backtick. Done before emphasis so bold/italic wrap the span (`**`code`**`).
312
+ // Previously a monospace text node emitted its bare text, dropping the code.
313
+ if (node.formatting.font === 'monospace') {
314
+ const raw = node.text || '';
315
+ const longestRun = Math.max(0, ...(raw.match(/`+/g) || []).map(s => s.length));
316
+ const fence = '`'.repeat(longestRun + 1);
317
+ const pad = (raw.startsWith('`') || raw.endsWith('`')) ? ' ' : '';
318
+ text = `${fence}${pad}${raw}${pad}${fence}`;
319
+ }
320
+ const emphasisAsterisk = this.resolvedDialect.emphasisMarker === 'asterisk';
321
+ if (node.formatting.bold && !this.inImplicitBold)
322
+ text = emphasisAsterisk ? `**${text}**` : `__${text}__`;
323
+ if (node.formatting.italic)
324
+ text = emphasisAsterisk ? `*${text}*` : `_${text}_`;
325
+ if (node.formatting.strikethrough && this.resolvedDialect.strikethrough !== 'none')
326
+ text = `~~${text}~~`;
327
+ // `==text==` highlight, in dialects that define it (Obsidian/extended). A plain
328
+ // highlight (the default yellow) always becomes `==text==`; a highlight carrying
329
+ // a SPECIFIC colour stays a background-color <span> when `inlineFormatting` is on,
330
+ // so its exact colour survives. With `inlineFormatting` off (no span to hold it)
331
+ // even a coloured highlight degrades to `==` rather than being dropped. In
332
+ // GFM/CommonMark `==` is literal, so a highlight falls through to the <span> path.
333
+ const isDefaultHighlight = node.formatting.backgroundColor === '#ffff00';
334
+ const emitHighlightMark = !!node.formatting.backgroundColor && this.resolvedDialect.highlight !== 'none'
335
+ && (isDefaultHighlight || !this.resolvedFallbackToHtml.inlineFormatting);
336
+ if (emitHighlightMark)
337
+ text = `==${text}==`;
338
+ // Use HTML tags for formatting not natively supported by standard Markdown
339
+ if (this.resolvedFallbackToHtml.textFormatting) {
340
+ if (node.formatting.underline)
341
+ text = `<u>${text}</u>`;
342
+ if (node.formatting.subscript)
343
+ text = `<sub>${text}</sub>`;
344
+ if (node.formatting.superscript)
345
+ text = `<sup>${text}</sup>`;
346
+ }
347
+ // Inline color / highlight / font size have no Markdown syntax; emit a styled
348
+ // <span> (outermost, so the inner Markdown markers survive) only when opted in,
349
+ // so default output is unchanged. Values are CSS-sanitized against injection.
350
+ if (this.resolvedFallbackToHtml.inlineFormatting) {
351
+ const styles = [];
352
+ const pushStyle = (prop, val) => {
353
+ if (!val)
354
+ return;
355
+ const safe = (0, sanitize_js_1.sanitizeCssValue)(val); // drops url()/expression()/<>/quotes
356
+ if (safe)
357
+ styles.push(`${prop}: ${safe}`);
358
+ };
359
+ pushStyle('color', node.formatting.color);
360
+ // Skip the background-color only when it was already emitted as `==text==`
361
+ // above; a specific-colour highlight in a highlight dialect still keeps its
362
+ // exact colour here.
363
+ if (!emitHighlightMark)
364
+ pushStyle('background-color', node.formatting.backgroundColor);
365
+ pushStyle('font-size', node.formatting.size);
366
+ if (styles.length)
367
+ text = `<span style="${styles.join('; ')}">${text}</span>`;
368
+ }
369
+ }
370
+ const meta = node.metadata;
371
+ if (meta?.wikilink && this.resolvedDialect.wikilinks !== 'none') {
372
+ // Obsidian syntax: bare page name, or page|alias when the display
373
+ // text differs from the page name. Strip the `[]|`/newline chars
374
+ // that would break out of the `[[...]]` wrapper.
375
+ // The alias must be built from the ESCAPED text, not from raw node.text.
376
+ // Rebuilding from the raw value here discarded the markdownEscapeText()
377
+ // applied above, so a wikilink was the one place document text reached
378
+ // the output unescaped. Escaping is lossless for the alias specifically,
379
+ // because it lands back in a text node, which the parser entity-decodes.
380
+ const alias = (0, sanitize_js_1.markdownEscapeText)(node.text || '').replace(/[[\]|\r\n]+/g, '');
381
+ // `page` lands in metadata.link, which is NOT entity-decoded on reparse,
382
+ // so it gets `<` dropped rather than encoded - a page name is an
383
+ // identifier, and `<` carries no meaning in one.
384
+ const page = (meta.link || '').replace(/[[\]|<\r\n]+/g, '');
385
+ text = (node.text && node.text !== (meta.link || '')) ? `[[${page}|${alias}]]` : `[[${page}]]`;
386
+ }
387
+ else if (meta?.link) {
388
+ const isInternal = meta.linkType !== 'external';
389
+ if (!this.config.ignoreInternalLinks || !isInternal) {
390
+ let link = meta.link;
391
+ // Slugify internal link targets to match heading IDs if generating IDs
392
+ if (isInternal && link.startsWith('#') && (this.config.generateIds || this.resolvedFallbackToHtml.anchors)) {
393
+ const target = link.substring(1);
394
+ link = '#' + this.slugify(target);
395
+ }
396
+ // Reject javascript:/data: schemes and encode `()`/whitespace so the
397
+ // URL can't break out of `](...)` or inject a script link. An advisory
398
+ // title follows as `"title"` (quotes inside it escaped), matching what
399
+ // the parser reads back.
400
+ const linkTitle = meta.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
401
+ text = `[${text}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(link)}${linkTitle})`;
402
+ }
403
+ }
404
+ if (meta?.abbreviationTitle) {
405
+ // Markdown Extra's abbreviation syntax has no inline marker - the bare
406
+ // word round-trips as-is, with its expansion collected at the document
407
+ // end via `*[abbr]: title`.
408
+ this.collectedAbbreviations.set(node.text || '', meta.abbreviationTitle);
409
+ }
410
+ if (meta?.citationKey) {
411
+ // Allowlist to exactly the character class MarkdownParser's own citation
412
+ // recognizer accepts, so this is provably lossless for anything it
413
+ // produced - while fully neutralizing a key arriving from HtmlParser's
414
+ // `data-citation-key`, which accepts any string. Like the wikilink above,
415
+ // this branch also replaces `text` wholesale, so a strip that left `<`
416
+ // behind discarded the escaping applied earlier.
417
+ const key = String(meta.citationKey).replace(/[^a-zA-Z0-9_:.-]/g, '');
418
+ text = this.resolvedDialect.citations !== 'none' ? `[@${key}]` : `[${key}]`;
419
+ }
420
+ return text;
421
+ }
422
+ case 'heading': {
423
+ const meta = node.metadata;
424
+ const level = Math.min(Math.max(meta?.level || 1, 1), 6);
425
+ const prefix = '#'.repeat(level) + ' ';
426
+ let id = '';
427
+ let remainingAnchors = [];
428
+ if (!this.config.ignoreInternalLinks && meta?.anchorIds && meta.anchorIds.length > 0) {
429
+ const ids = [...meta.anchorIds];
430
+ const lastId = ids.pop();
431
+ // Slugify the explicit ID to ensure it's a valid Markdown identifier
432
+ id = ` {#${this.slugify(lastId)}}`;
433
+ remainingAnchors = ids;
434
+ }
435
+ else if (this.config.generateIds) {
436
+ id = ` {#${this.slugify(this.getNodeText(node))}}`;
437
+ }
438
+ const anchors = this.resolvedFallbackToHtml.anchors
439
+ ? remainingAnchors.map(aid => `<a name="${this.slugify(aid)}"></a>`).join('')
440
+ : '';
441
+ let content = `${prefix}${childrenOutput}${id}`;
442
+ // Alignment fallback via HTML div/p
443
+ if (this.resolvedFallbackToHtml.alignment && meta?.alignment && meta.alignment !== 'left') {
444
+ // Use extra newlines to ensure Markdown inside the div is parsed
445
+ content = `<div style="text-align: ${(0, sanitize_js_1.sanitizeCssValue)(meta.alignment)}">\n\n${content}\n\n</div>`;
446
+ }
447
+ return `${anchors}${anchors ? '\n' : ''}${content}\n\n`;
448
+ }
449
+ case 'paragraph': {
450
+ const meta = node.metadata;
451
+ const anchors = this.renderAnchors(meta);
452
+ let content = childrenOutput;
453
+ // Alignment fallback via HTML div/p
454
+ if (this.resolvedFallbackToHtml.alignment && meta?.alignment && meta.alignment !== 'left') {
455
+ content = `<div style="text-align: ${(0, sanitize_js_1.sanitizeCssValue)(meta.alignment)}">${content}</div>`;
456
+ }
457
+ return childrenOutput ? `${anchors}${content}\n\n` : '';
458
+ }
459
+ case 'list': {
460
+ const meta = node.metadata;
461
+ const indentSpaces = ' '.repeat(4);
462
+ const indent = indentSpaces.repeat(meta?.indentation || 0);
463
+ const bullet = `${this.resolvedDialect.bulletListMarker} `;
464
+ const marker = meta?.isTask
465
+ ? (meta.checked ? `${bullet}[x] ` : `${bullet}[ ] `)
466
+ : (meta?.listType === 'ordered' ? `${(meta.itemIndex ?? 0) + 1}${this.resolvedDialect.orderedListMarker} ` : bullet);
467
+ const anchors = this.renderAnchors(meta);
468
+ // A list item is a single Markdown line. HTML-origin items carry `paragraph`
469
+ // children (e.g. `<li><p>a</p><ul>...`), whose renderer appends `\n\n`; dumped
470
+ // verbatim that produces `- a\n\n\n - a1`, whose blank line splits the list
471
+ // apart and whose indent is then stripped on reparse, flattening the nesting.
472
+ // Collapse the item's internal breaks the same way table cells do (see the
473
+ // `cellLineBreaks` handling in renderMarkdownTable): join with `<br>` when the
474
+ // fallback is on, a space when off. Block children (code fences, tables) inside
475
+ // an item degrade under this join, exactly as they do inside a cell.
476
+ const br = this.resolvedFallbackToHtml.itemLineBreaks ? '<br>' : ' ';
477
+ const content = childrenOutput.trim().replace(/[ \t]*\n+/g, br);
478
+ return `${indent}${marker}${anchors}${content}\n`;
479
+ }
480
+ case 'image': {
481
+ if (!this.config.includeImages)
482
+ return '';
483
+ const meta = node.metadata;
484
+ const alt = meta?.altText || 'image';
485
+ let src = meta?.url || meta?.attachmentName || '';
486
+ // Resolve attachment to data URI if no external URL is provided
487
+ if (!meta?.url && meta?.attachmentName && this.ast) {
488
+ const attachment = this.ast.attachments.find(a => a.name === meta.attachmentName);
489
+ if (attachment) {
490
+ src = `data:${attachment.mimeType || 'image/png'};base64,${attachment.data}`;
491
+ }
492
+ }
493
+ const anchors = this.renderAnchors(meta);
494
+ // Strip `[]` from alt (would close the `![...]`) and neutralize the URL scheme.
495
+ const safeAlt = (0, sanitize_js_1.markdownEscapeText)(alt).replace(/[[\]]/g, '');
496
+ const safeSrc = (0, sanitize_js_1.sanitizeMarkdownUrl)(src, { allowDataImage: true });
497
+ const imgTitle = meta?.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
498
+ return `${anchors}${anchors ? '\n' : ''}![${safeAlt}](${safeSrc}${imgTitle})${this.renderAttributeList(meta)}`;
499
+ }
500
+ case 'table': {
501
+ const anchors = this.renderAnchors(node.metadata);
502
+ const tableOutput = await this.renderMarkdownTable(node, processor);
503
+ // The HTML-fallback path (merged cells/nested tables, or a dialect that forces
504
+ // HTML tables outright) already carries data-align on the <table> tag directly -
505
+ // only the plain pipe-table form needs the attribute-list syntax for alignment.
506
+ const usedHtmlFallback = this.resolvedDialect.tables === 'html' ||
507
+ (this.resolvedFallbackToHtml.tables && (this.hasNestedTable(node) || this.hasColspanOrRowspan(node)));
508
+ const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata, { skipAlign: true });
509
+ if (attrList) {
510
+ // Must glue directly below the last row with no blank line, or
511
+ // MarkdownParser's block splitter won't see it as part of the same block.
512
+ return `${anchors}${anchors ? '\n' : ''}${tableOutput.replace(/\n+$/, '\n')}${attrList}\n`;
513
+ }
514
+ return `${anchors}${anchors ? '\n' : ''}${tableOutput}`;
515
+ }
516
+ case 'row':
517
+ case 'cell': {
518
+ // These are handled manually in the 'table' case above
519
+ return childrenOutput;
520
+ }
521
+ case 'break': {
522
+ // A hard line break (CommonMark: two trailing spaces before the newline)
523
+ // round-trips back to a distinct 'break' node on reparse. A thematic break
524
+ // emits `---` as its own block (the top-level loop supplies the surrounding
525
+ // blank lines), so a Markdown `---` / HTML `<hr>` survives a save instead of
526
+ // collapsing to whitespace. Every other breakType - notably 'page', which
527
+ // Markdown has no syntax for - keeps emitting a bare newline, unchanged.
528
+ const meta = node.metadata;
529
+ if (meta?.breakType === 'carriageReturn')
530
+ return ' \n';
531
+ if (meta?.breakType === 'thematic')
532
+ return '---';
533
+ return '\n';
534
+ }
535
+ case 'code': {
536
+ const meta = node.metadata;
537
+ // Math content reached the output completely raw, which mattered most under
538
+ // `math: 'none'` (the commonmark preset), where there is no `$` wrapper at
539
+ // all and the text lands directly in the document body.
540
+ //
541
+ // Encode rather than drop: `$a < b$` is ordinary LaTeX, and dropping `<`
542
+ // would silently corrupt real formulae. markdownEscapeText only touches `<`
543
+ // followed by a letter/`/`/`!`/`?`, which is not idiomatic math, and it is
544
+ // idempotent - so output is stable across repeated round-trips even though
545
+ // the first cycle shifts an anomalous `<img` to `&lt;img`. (Fully lossless
546
+ // would mean teaching MarkdownParser.decodeHtmlEntities to cover math `code`
547
+ // nodes; that is a parser behaviour change with its own baseline
548
+ // consequences and must not gate a security fix.)
549
+ if (meta?.math === 'block') {
550
+ // A content line of exactly `$$` would close the block early.
551
+ const mathBlock = (0, sanitize_js_1.markdownEscapeText)(node.text || '')
552
+ .split('\n').map(l => (l.trim() === '$$' ? ` ${l}` : l)).join('\n');
553
+ return this.resolvedDialect.math === 'dollar' ? `\n$$\n${mathBlock}\n$$\n\n` : `\n${mathBlock}\n\n`;
554
+ }
555
+ if (meta?.math === 'inline') {
556
+ // Dropping `$` and newlines is lossless here: the parser's own inline-math
557
+ // recognizer is `\$(?!\s)([^$\n]+?)(?<!\s)\$`, which can never capture either.
558
+ const mathInline = (0, sanitize_js_1.markdownEscapeText)(node.text || '').replace(/[$\r\n]+/g, '');
559
+ return this.resolvedDialect.math === 'dollar' ? `$${mathInline}$` : mathInline;
560
+ }
561
+ const lang = (meta?.language || '').replace(/[\r\n`]+/g, '');
562
+ // A `code` node is always block-level: genuinely inline code is a monospace
563
+ // text node, never a `code` node. So emit a fenced block whenever the node
564
+ // carries a language OR spans multiple lines. Previously the decision keyed only
565
+ // off a line break, so a single-line code node with a language - `const x = 1;`
566
+ // tagged `js`, or a one-line `mermaid` diagram - collapsed to an inline span,
567
+ // silently dropping both its language and its block-ness. (Testing `[\r\n]`, not
568
+ // just `\n`, still routes a CR-only body to the fenced branch, where a renderer
569
+ // that normalizes `\r` to a line ending would otherwise kill an inline span.)
570
+ if (lang || (node.text && /[\r\n]/.test(node.text))) {
571
+ // Fence with one more backtick than the longest run inside the content
572
+ // so an embedded ``` can't close the block early and inject markup.
573
+ const longestRun = Math.max(0, ...((node.text || '').match(/`+/g) || []).map(s => s.length));
574
+ const fence = '`'.repeat(Math.max(3, longestRun + 1));
575
+ return `\n${fence}${lang}\n${node.text || ''}\n${fence}\n\n`;
576
+ }
577
+ else {
578
+ const t = node.text || '';
579
+ const longestRun = Math.max(0, ...(t.match(/`+/g) || []).map(s => s.length));
580
+ const fence = '`'.repeat(Math.max(1, longestRun + 1));
581
+ const pad = (t.startsWith('`') || t.endsWith('`')) ? ' ' : '';
582
+ return `${fence}${pad}${t}${pad}${fence} `;
583
+ }
584
+ }
585
+ case 'sheet': {
586
+ const anchors = this.renderAnchors(node.metadata);
587
+ const tableOutput = await this.renderMarkdownTable(node, processor);
588
+ return `\n---\n\n${anchors}${anchors ? '\n' : ''}${tableOutput}\n\n`;
589
+ }
590
+ case 'slide': {
591
+ const anchors = this.renderAnchors(node.metadata);
592
+ return `\n---\n\n${anchors}${anchors ? '\n' : ''}${childrenOutput}\n\n`;
593
+ }
594
+ case 'page': {
595
+ const anchors = this.renderAnchors(node.metadata);
596
+ return `\n---\n\n${anchors}${anchors ? '\n' : ''}${childrenOutput}\n\n`;
597
+ }
598
+ case 'note': {
599
+ const meta = node.metadata;
600
+ if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
601
+ if (this.resolvedDialect.footnotes === 'none') {
602
+ // Dialect has no footnote syntax - the caller inlines this bare body
603
+ // as a parenthetical at the reference point instead of collecting it
604
+ // into an end-of-document "### Notes" section under a [^id] marker.
605
+ return childrenOutput.trim();
606
+ }
607
+ // Indent continuation lines one level so a multi-line body re-parses as a
608
+ // single definition (a bare newline would end it). Single-line bodies, the
609
+ // common case, are unaffected.
610
+ return `[^${this.getFootnoteKey(node)}]: ${childrenOutput.trim().replace(/\n/g, '\n ')}\n\n`;
611
+ }
612
+ return `> **Note:** ${childrenOutput.trim()}\n\n`;
613
+ }
614
+ case 'embed': {
615
+ // Markdown has no native embed syntax. `this.resolvedEmbeds` (from
616
+ // `dialect.embeds`, honoring the deprecated `fallbackToHtml.embeds` boolean)
617
+ // selects the form: 'html' (the single-line block this library has always
618
+ // emitted and re-recognises), 'directive' (a remark-directive leaf), 'link'
619
+ // (a plain link), 'thumbnail' (YouTube-only clickable preview).
620
+ const meta = node.metadata;
621
+ const mode = this.resolvedEmbeds;
622
+ // A directive label sits inside `::name[...]`; strip the `[]`/newline chars that
623
+ // would break out of it. An attribute value sits inside `{...}`; percent-encode
624
+ // the space/brace chars that would break out (widths/aligns/ids never contain
625
+ // them, but a src can).
626
+ const dirLabel = (meta?.label || '').replace(/[[\]\r\n]+/g, ' ').trim();
627
+ const dirUrl = (u) => (0, sanitize_js_1.sanitizeMarkdownUrl)(u).replace(/[{}\s]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
628
+ const attrList = (pairs) => {
629
+ const kv = pairs.filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`);
630
+ return kv.length ? `{${kv.join(' ')}}` : '';
631
+ };
632
+ if (meta?.embedType === 'iframe') {
633
+ const rawUrl = meta?.url || '';
634
+ if (mode === 'directive') {
635
+ const src = dirUrl(rawUrl);
636
+ if (!src)
637
+ return '';
638
+ const lbl = dirLabel ? `[${dirLabel}]` : '';
639
+ return `::embed${lbl}${attrList([['src', src], ['width', meta?.width], ['height', meta?.height], ['align', meta?.align]])}\n\n`;
640
+ }
641
+ if (mode === 'html') {
642
+ // sanitizeUrl scheme-checks and HTML-escapes the src (hostile schemes drop
643
+ // the node). The single-line <iframe> is what MarkdownParser recognises on
644
+ // reimport, gated there on preserveIframes.
645
+ const safe = (0, sanitize_js_1.sanitizeUrl)(rawUrl);
646
+ if (!safe)
647
+ return '';
648
+ const w = meta?.width ? ` width="${(0, sanitize_js_1.escapeHtml)(meta.width)}"` : '';
649
+ const h = meta?.height ? ` height="${(0, sanitize_js_1.escapeHtml)(meta.height)}"` : '';
650
+ return `\n<iframe src="${safe}"${w}${h}></iframe>\n\n`;
651
+ }
652
+ // 'link' and 'thumbnail' (thumbnail is YouTube-only, so a generic iframe
653
+ // degrades to a link) both emit a plain link.
654
+ const safe = (0, sanitize_js_1.sanitizeMarkdownUrl)(rawUrl);
655
+ return safe ? `[${meta?.label || 'Embed'}](${safe})\n\n` : '';
656
+ }
657
+ const id = meta?.videoId || '';
658
+ if (mode === 'directive') {
659
+ const lbl = dirLabel ? `[${dirLabel}]` : '';
660
+ return `::youtube${lbl}${attrList([['id', id], ['width', meta?.width], ['align', meta?.align]])}\n\n`;
661
+ }
662
+ if (mode === 'html') {
663
+ const width = meta?.width ? ` data-width="${(0, sanitize_js_1.escapeHtml)(meta.width)}"` : '';
664
+ const align = meta?.align ? ` data-align="${(0, sanitize_js_1.escapeHtml)(meta.align)}"` : '';
665
+ const lbl = meta?.label ? ` data-embed-label="${(0, sanitize_js_1.escapeHtml)(meta.label)}"` : '';
666
+ return `\n<div data-youtube-video="${(0, sanitize_js_1.escapeHtml)(id)}"${width}${align}${lbl}></div>\n\n`;
667
+ }
668
+ if (mode === 'thumbnail' && id) {
669
+ const watch = (0, sanitize_js_1.sanitizeMarkdownUrl)(`https://www.youtube.com/watch?v=${id}`);
670
+ const thumb = (0, sanitize_js_1.sanitizeMarkdownUrl)(`https://img.youtube.com/vi/${id}/hqdefault.jpg`);
671
+ return `[![${meta?.label || 'YouTube'}](${thumb})](${watch})\n\n`;
672
+ }
673
+ // 'link' (and 'thumbnail' with no id): a plain link.
674
+ const url = meta?.url || (id ? `https://youtu.be/${id}` : '');
675
+ return url ? `[${meta?.label || 'YouTube'}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(url)})\n\n` : '';
676
+ }
677
+ case 'admonition': {
678
+ const meta = node.metadata;
679
+ // `admonitionType` is a closed union in types.ts and both parsers already
680
+ // allowlist on import, so enforcing it here is a no-op for any conforming
681
+ // AST - it closes the gap for a programmatically-built one, where the type is
682
+ // interpolated straight into `:::TYPE` / `::: {.TYPE}` / `[!TYPE]`.
683
+ const rawType = String(meta?.admonitionType || 'note').toLowerCase();
684
+ const type = MD_ADMONITION_TYPES.has(rawType) ? rawType : 'note';
685
+ const label = type.toUpperCase();
686
+ // A newline in the title would close the `**...**` and, in the fenced-div
687
+ // branches, could emit a stray `:::` line. `title` is never parser-set, so
688
+ // there is no round-trip to preserve and escaping is free.
689
+ const title = meta?.title ? (0, sanitize_js_1.markdownEscapeText)(foldLines(meta.title)) : '';
690
+ const body = childrenOutput.trim();
691
+ switch (this.resolvedDialect.admonitions) {
692
+ case 'fence':
693
+ // GLFM fenced-div: no dedicated title syntax, so a custom title (if
694
+ // any) is folded into the body as a bold first line.
695
+ return `:::${type}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
696
+ case 'fence-attribute':
697
+ // Pandoc's own fenced-div-with-class syntax; same title handling as fence.
698
+ return `::: {.${type}}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
699
+ case 'none': {
700
+ // Degrade to a plain bold-labeled blockquote, no special marker.
701
+ const quotedLines = body.split('\n').map(l => l.length > 0 ? `> ${l}` : '>').join('\n');
702
+ const heading = title || label.charAt(0) + label.slice(1).toLowerCase();
703
+ return `> **${heading}:**\n${quotedLines}\n\n`;
704
+ }
705
+ case 'blockquote':
706
+ default: {
707
+ // Canonical GitHub blockquote form. No dedicated title syntax either
708
+ // (matches this library's historical output).
709
+ const quotedLines = body.split('\n').map(l => l.length > 0 ? `> ${l}` : '>').join('\n');
710
+ return `> [!${label}]\n${quotedLines}\n\n`;
711
+ }
712
+ }
713
+ }
714
+ case 'definitionList':
715
+ if (this.resolvedDialect.definitionLists === 'none')
716
+ return `${childrenOutput}\n`;
717
+ return `${childrenOutput}\n`;
718
+ case 'definitionTerm':
719
+ if (this.resolvedDialect.definitionLists === 'none')
720
+ return `**${childrenOutput}**\n\n`;
721
+ return `${childrenOutput}\n`;
722
+ case 'definitionDescription':
723
+ if (this.resolvedDialect.definitionLists === 'none')
724
+ return `${childrenOutput}\n\n`;
725
+ return `: ${childrenOutput}\n`;
726
+ case 'chart':
727
+ case 'drawing':
728
+ case 'comment':
729
+ case 'header':
730
+ case 'footer':
731
+ case 'slideMaster':
732
+ return childrenOutput;
733
+ }
734
+ };
735
+ const optimizedContent = this.optimizeNodes(this.ast.content);
736
+ for (let i = 0; i < optimizedContent.length; i++) {
737
+ const node = optimizedContent[i];
738
+ const nextNode = optimizedContent[i + 1];
739
+ // A top-level footnote/endnote note is an orphan definition (unreferenced `[^id]: ...`
740
+ // the MarkdownParser recovered). Collect it so it's emitted with the other definitions
741
+ // at the document end rather than inline before them.
742
+ const orphanNoteType = node.metadata?.noteType;
743
+ if (node.type === 'note' && (orphanNoteType === 'footnote' || orphanNoteType === 'endnote')) {
744
+ this.collectedNotes.push(node);
745
+ continue;
746
+ }
747
+ let result = await this.processNodeRecursive(node, processor);
748
+ // Ensure lists and other block elements are separated from non-similar content by a blank line
749
+ if (nextNode) {
750
+ const isBothLists = node.type === 'list' && nextNode.type === 'list';
751
+ if (!isBothLists) {
752
+ if (!result.endsWith('\n\n')) {
753
+ if (result.endsWith('\n'))
754
+ result += '\n';
755
+ else
756
+ result += '\n\n';
757
+ }
758
+ }
759
+ }
760
+ output += result;
761
+ }
762
+ if (this.collectedNotes.length > 0) {
763
+ // No decorative `---\n\n### Notes` preamble: `[^id]:` definitions are valid on their own
764
+ // (GitHub/Pandoc render the footnotes section and its rule automatically), and the
765
+ // literal heading round-tripped as a real `###` node - so every save/reload re-emitted
766
+ // the parsed heading AND a fresh one, growing the document unbounded. Emitting the bare
767
+ // definitions makes the cycle byte-stable. Behaviour change, noted in the changelog.
768
+ // Collapse the preceding block's trailing blank lines so exactly one blank line separates
769
+ // the body from the definitions (rather than the doubled `\n\n\n\n` the concatenation
770
+ // would otherwise leave).
771
+ output = output.replace(/\n+$/, '');
772
+ let notesMd = '\n\n';
773
+ // De-duplicate by node identity: a footnote referenced more than once shares a single
774
+ // note object (see MarkdownParser), pushed here once per reference. Emit its definition
775
+ // just once. Distinct notes - even two office notes that happen to share a numeric id -
776
+ // are separate objects and are all kept.
777
+ for (const note of [...new Set(this.collectedNotes)]) {
778
+ notesMd += await this.processNodeRecursive(note, processor);
779
+ }
780
+ output += notesMd;
781
+ }
782
+ if (this.collectedAbbreviations.size > 0) {
783
+ output += '\n\n';
784
+ for (const [abbr, title] of this.collectedAbbreviations) {
785
+ output += `*[${(0, sanitize_js_1.markdownEscapeText)(String(abbr).replace(/[[\]\r\n]+/g, ''))}]: ${(0, sanitize_js_1.markdownEscapeText)(foldLines(title))}\n`;
786
+ }
787
+ }
788
+ // Only a run of literal "\n" at either end is ever a generator artifact here: block
789
+ // separators, the notes/abbreviations sections, the unconditional '\n\n' before
790
+ // hoistedContent (added even when hoistedContent is empty), and renderMarkdownTable's
791
+ // HTML-fallback branches, which unconditionally wrap in a leading+trailing '\n' as
792
+ // separators from whatever precedes/follows (in practice this rarely surfaces at the very
793
+ // start of `output` today since frontmatter's own "---" almost always precedes real
794
+ // content first - see the type doc on `ast.metadata` - but the strip is correct regardless
795
+ // of what precedes it). Nothing else at either end is a generator artifact: not leading
796
+ // whitespace, and not any other kind of trailing whitespace, both of which would be real
797
+ // document content. See the identical reasoning in TextGenerator.generate().
798
+ return {
799
+ value: (output + '\n\n' + this.hoistedContent.join('\n\n')).replace(/^\n+|\n+$/g, ''),
800
+ messages: this.messages
801
+ };
802
+ }
803
+ /**
804
+ * Recursively processes nodes and builds output.
805
+ * Overridden to provide AST optimization (merging adjacent text nodes).
806
+ */
807
+ async processNodeRecursive(node, processor) {
808
+ // Mirrors the check in BaseGenerator.processNodeRecursive. This override replaces that
809
+ // method entirely, so without repeating the check here the signal would be silently
810
+ // inert for this generator - which is exactly how it was missed.
811
+ (0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
812
+ // Allow user to completely override rendering or skip via onNode
813
+ const override = await this.handleOnNode(node);
814
+ if (override === false) {
815
+ return '';
816
+ }
817
+ if (typeof override === 'string') {
818
+ return override;
819
+ }
820
+ const walkedByProcessor = node.type === 'table' || node.type === 'sheet';
821
+ const wasInImplicitBold = this.inImplicitBold;
822
+ if (node.type === 'heading' && this.hasUniformFormatting(node, f => f?.bold === true))
823
+ this.inImplicitBold = true;
824
+ let childrenOutput = '';
825
+ if (!walkedByProcessor && node.children && node.children.length > 0) {
826
+ // Optimization: Merge adjacent text nodes with identical formatting
827
+ const optimizedChildren = this.optimizeNodes(node.children);
828
+ for (const child of optimizedChildren) {
829
+ childrenOutput += await this.processNodeRecursive(child, processor);
830
+ }
831
+ }
832
+ this.inImplicitBold = wasInImplicitBold;
833
+ // When the dialect has no footnote syntax, a footnote/endnote is inlined right at its
834
+ // reference point instead (see below) - so it must not also be collected into the
835
+ // end-of-document "### Notes" section, or its content would be duplicated.
836
+ const isInlinedFootnote = (note) => {
837
+ const meta = note.metadata;
838
+ return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
839
+ };
840
+ this.collectNotesFrom(node);
841
+ let result = await processor(node, childrenOutput);
842
+ if (node.type === 'slide' && node.notes && node.notes.length > 0) {
843
+ for (const note of node.notes) {
844
+ result += await this.processNodeRecursive(note, processor);
845
+ }
846
+ }
847
+ else if (node.notes && node.notes.length > 0) {
848
+ for (const note of node.notes) {
849
+ const meta = note.metadata;
850
+ if (meta?.noteType !== 'footnote' && meta?.noteType !== 'endnote')
851
+ continue;
852
+ if (isInlinedFootnote(note)) {
853
+ // Markdown-specific degrade (not RTF/plain-text's "drop the marker, just
854
+ // append at the end" convention): inline the note's rendered body as a
855
+ // parenthetical right where it's referenced, since Markdown readers benefit
856
+ // from an inline association those simpler formats don't need in the same way.
857
+ const body = await this.processNodeRecursive(note, processor);
858
+ result += ` (Note: ${body})`;
859
+ }
860
+ else {
861
+ // Emit the [^id] reference marker at the point of reference. Without this,
862
+ // a footnote/endnote would only ever show up in the collected ### Notes
863
+ // section at the end, with no indication of where it was originally cited.
864
+ result += `[^${this.getFootnoteKey(note)}]`;
865
+ }
866
+ }
867
+ }
868
+ return result;
869
+ }
870
+ /**
871
+ * Merges adjacent text nodes with identical formatting and metadata.
872
+ */
873
+ optimizeNodes(nodes) {
874
+ if (nodes.length <= 1)
875
+ return nodes;
876
+ const result = [];
877
+ let current = null;
878
+ for (const node of nodes) {
879
+ if (node.type === 'text' && current && current.type === 'text' &&
880
+ // A note anchors to the end of its text run and its `[^id]` marker is emitted there;
881
+ // merging a following run onto a note-carrying run would slide the marker past it
882
+ // (`Body[^1].` -> `Body.[^1]`). Keep such runs separate so the marker stays put and
883
+ // matches where HtmlGenerator emits it.
884
+ (!current.notes || current.notes.length === 0) &&
885
+ (!node.notes || node.notes.length === 0) &&
886
+ this.areFormattingEqual(node.formatting, current.formatting) &&
887
+ JSON.stringify(node.metadata) === JSON.stringify(current.metadata)) {
888
+ current.text = (current.text || '') + (node.text || '');
889
+ if (current.rawContent && node.rawContent)
890
+ current.rawContent += node.rawContent;
891
+ if (node.notes && node.notes.length > 0) {
892
+ if (!current.notes)
893
+ current.notes = [];
894
+ current.notes.push(...node.notes);
895
+ }
896
+ }
897
+ else {
898
+ current = { ...node }; // Clone
899
+ if (node.notes) {
900
+ current.notes = [...node.notes];
901
+ }
902
+ result.push(current);
903
+ }
904
+ }
905
+ return result;
906
+ }
907
+ areFormattingEqual(f1, f2) {
908
+ if (f1 === f2)
909
+ return true;
910
+ if (!f1 || !f2)
911
+ return false;
912
+ const keys1 = Object.keys(f1);
913
+ const keys2 = Object.keys(f2);
914
+ if (keys1.length !== keys2.length)
915
+ return false;
916
+ return keys1.every(key => f1[key] === f2[key]);
917
+ }
918
+ async renderMarkdownTable(node, processor) {
919
+ if (!node.children || node.children.length === 0)
920
+ return '';
921
+ // A dialect that has no native table syntax at all (e.g. strict CommonMark) always
922
+ // renders as HTML, regardless of complexity - this is a separate axis from the
923
+ // nested/merged-cell HTML fallback below, which only applies to otherwise-native tables.
924
+ if (this.resolvedDialect.tables === 'html') {
925
+ return '\n' + await this.renderTableAsHtml(node) + '\n';
926
+ }
927
+ // If table is complex, nested, or uses merges, fallback to HTML for high fidelity if allowed
928
+ const isComplex = this.hasNestedTable(node) || this.hasColspanOrRowspan(node);
929
+ if (this.resolvedFallbackToHtml.tables && isComplex) {
930
+ return '\n' + await this.renderTableAsHtml(node) + '\n';
931
+ }
932
+ // Handle nested tables in pure Markdown by hoisting them out
933
+ if (this.isInsideTable && !this.resolvedFallbackToHtml.tables) {
934
+ const wasInside = this.isInsideTable;
935
+ this.isInsideTable = false; // Reset to allow rendering the hoisted table correctly
936
+ const hoistedId = this.hoistedContent.length + 1;
937
+ const tableOutput = await this.renderMarkdownTableInternal(node, processor);
938
+ this.hoistedContent.push(`**Table ${hoistedId} (Hoisted from cell content):**\n${tableOutput}`);
939
+ this.isInsideTable = wasInside;
940
+ return `*(See Table ${hoistedId} below)*`;
941
+ }
942
+ this.isInsideTable = true;
943
+ const result = await this.renderMarkdownTableInternal(node, processor);
944
+ this.isInsideTable = false;
945
+ return result;
946
+ }
947
+ collectNotesFrom(node) {
948
+ if (!node.notes || node.notes.length === 0)
949
+ return;
950
+ if (node.type === 'slide')
951
+ return;
952
+ const isInlinedFootnote = (note) => {
953
+ const meta = note.metadata;
954
+ return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
955
+ };
956
+ this.collectedNotes.push(...node.notes.filter(note => !isInlinedFootnote(note)));
957
+ }
958
+ async renderMarkdownTableInternal(node, processor) {
959
+ let tableOutput = '';
960
+ let maxCols = 0;
961
+ // First pass: Process rows and determine max columns (accounting for colspans)
962
+ const processedRows = [];
963
+ for (const rowNode of (node.children ?? [])) {
964
+ // The first row becomes the header row - the `| --- |` separator emitted below marks
965
+ // it as such - so bold inside it is already implied.
966
+ const wasInImplicitBold = this.inImplicitBold;
967
+ if (processedRows.length === 0 && this.hasUniformFormatting(rowNode, f => f?.bold === true))
968
+ this.inImplicitBold = true;
969
+ try {
970
+ const override = await this.handleOnNode(rowNode);
971
+ if (override === false)
972
+ continue;
973
+ if (typeof override === 'string') {
974
+ processedRows.push([override]);
975
+ continue;
976
+ }
977
+ // After the override checks, not before: a row the caller skipped via `onNode` must not
978
+ // still contribute its footnote to the end-of-document Notes section, where it would
979
+ // appear with no `[^id]` marker anywhere in the document pointing at it.
980
+ // `renderTableAsHtml` gets this right by returning early, so collecting here keeps the
981
+ // pipe and HTML paths agreeing on what a skipped row means.
982
+ this.collectNotesFrom(rowNode);
983
+ const rowCells = [];
984
+ let lastCol = -1;
985
+ if (rowNode.children) {
986
+ const cellNodes = rowNode.children.filter(c => c.type === 'cell');
987
+ for (const cellNode of cellNodes) {
988
+ const currentCol = cellNode.metadata?.col ?? (lastCol + 1);
989
+ // Fill gaps with empty cells
990
+ while (lastCol < currentCol - 1) {
991
+ rowCells.push(' ');
992
+ lastCol++;
993
+ }
994
+ // Process cell content
995
+ let cellContent = await this.processNodeRecursive(cellNode, processor);
996
+ // Use <br> fallback only if allowed, otherwise space
997
+ const br = this.resolvedFallbackToHtml.cellLineBreaks ? '<br>' : ' ';
998
+ // Consume any trailing spaces before the newline(s) too, so a hard-break's
999
+ // ` \n` collapses to a single `<br>` instead of leaving ` <br>` in the cell.
1000
+ cellContent = cellContent.trim().replace(/[ \t]*\n+/g, br).replace(/\|/g, '\\|');
1001
+ rowCells.push(cellContent);
1002
+ // Handle colspan by adding empty cells
1003
+ const colSpan = cellNode.metadata?.colSpan || 1;
1004
+ for (let i = 1; i < colSpan; i++) {
1005
+ rowCells.push(' ');
1006
+ }
1007
+ lastCol = currentCol + colSpan - 1;
1008
+ }
1009
+ }
1010
+ processedRows.push(rowCells);
1011
+ maxCols = Math.max(maxCols, rowCells.length);
1012
+ }
1013
+ finally {
1014
+ this.inImplicitBold = wasInImplicitBold;
1015
+ }
1016
+ }
1017
+ // Second pass: Build table string with separator. The separator carries standard GFM
1018
+ // per-column alignment (`:---`/`:---:`/`---:`) from columnAlignments, or the single table-level
1019
+ // align applied to every column, rather than a non-standard trailing `{align}` attribute list.
1020
+ const tableMeta = node.metadata;
1021
+ // Column alignment lives on each cell (CellMetadata.align); read it off the header row.
1022
+ // Fall back to the single table-level align (an editor's data-align) for every column.
1023
+ const headerCells = (node.children?.[0]?.children || []).filter(c => c.type === 'cell');
1024
+ const alignMarker = (i) => {
1025
+ const a = headerCells[i]?.metadata?.align ?? tableMeta?.align;
1026
+ return a === 'center' ? ':---:' : a === 'left' ? ':---' : a === 'right' ? '---:' : '---';
1027
+ };
1028
+ for (let i = 0; i < processedRows.length; i++) {
1029
+ const row = processedRows[i];
1030
+ // Pad row with empty cells if it has fewer than maxCols
1031
+ while (row.length < maxCols)
1032
+ row.push(' ');
1033
+ tableOutput += `| ${row.join(' | ')} |\n`;
1034
+ if (i === 0) {
1035
+ // Header separator
1036
+ tableOutput += `| ${Array.from({ length: maxCols }, (_, i) => alignMarker(i)).join(' | ')} |\n`;
1037
+ }
1038
+ }
1039
+ return `\n${tableOutput}\n`;
1040
+ }
1041
+ hasNestedTable(node) {
1042
+ if (!node.children)
1043
+ return false;
1044
+ for (const child of node.children) {
1045
+ if (child.type === 'table')
1046
+ return true;
1047
+ if (this.hasNestedTable(child))
1048
+ return true;
1049
+ }
1050
+ return false;
1051
+ }
1052
+ hasColspanOrRowspan(node) {
1053
+ if (!node.children)
1054
+ return false;
1055
+ for (const row of node.children) {
1056
+ if (row.type === 'row' && row.children) {
1057
+ for (const cell of row.children) {
1058
+ if (cell.type === 'cell') {
1059
+ const meta = cell.metadata;
1060
+ if ((meta?.colSpan && meta.colSpan > 1) || (meta?.rowSpan && meta.rowSpan > 1)) {
1061
+ return true;
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ }
1067
+ return false;
1068
+ }
1069
+ /**
1070
+ * Renders a complex table as HTML since Markdown doesn't support nested tables or rowspans.
1071
+ */
1072
+ async renderTableAsHtml(node, override) {
1073
+ if (override === false)
1074
+ return '';
1075
+ if (typeof override === 'string') {
1076
+ if (node.type === 'row')
1077
+ return ` <tr><td colspan="100%">${override}</td></tr>\n`;
1078
+ if (node.type === 'cell')
1079
+ return `<td>${override}</td>`;
1080
+ return override;
1081
+ }
1082
+ if (node.type === 'table') {
1083
+ let rows = '';
1084
+ if (node.children) {
1085
+ for (const row of node.children) {
1086
+ rows += await this.renderTableAsHtml(row, await this.handleOnNode(row));
1087
+ }
1088
+ }
1089
+ // Carry table-layout alignment through the HTML fallback so it isn't lost
1090
+ // just because the table also needed HTML for merged cells.
1091
+ const tableMeta = node.metadata;
1092
+ const alignAttr = tableMeta?.align ? ` data-align="${(0, sanitize_js_1.escapeHtml)(tableMeta.align)}"` : '';
1093
+ return `<table${alignAttr}>\n${rows}</table>\n`;
1094
+ }
1095
+ else if (node.type === 'row') {
1096
+ this.collectNotesFrom(node);
1097
+ let cells = '';
1098
+ if (node.children) {
1099
+ for (const cell of node.children) {
1100
+ cells += await this.renderTableAsHtml(cell, await this.handleOnNode(cell));
1101
+ }
1102
+ }
1103
+ return ` <tr>\n${cells} </tr>\n`;
1104
+ }
1105
+ else if (node.type === 'cell') {
1106
+ this.collectNotesFrom(node);
1107
+ const meta = node.metadata;
1108
+ const rs = meta?.rowSpan > 1 ? ` rowspan="${meta.rowSpan}"` : '';
1109
+ const cs = meta?.colSpan > 1 ? ` colspan="${meta.colSpan}"` : '';
1110
+ let content = '';
1111
+ if (node.children) {
1112
+ // Use a simplified HTML processor for cell content
1113
+ for (const child of this.optimizeNodes(node.children)) {
1114
+ content += await this.processNodeRecursive(child, async (n, co) => {
1115
+ switch (n.type) {
1116
+ case 'text': {
1117
+ if (n.metadata) {
1118
+ const m = n.metadata;
1119
+ if (m.abbreviationTitle) {
1120
+ this.collectedAbbreviations.set(n.text || '', m.abbreviationTitle);
1121
+ }
1122
+ }
1123
+ // Inside HTML table cells, entity-encode angle brackets so cell
1124
+ // text can't inject a raw tag (e.g. </td><script>).
1125
+ let text = (0, sanitize_js_1.markdownEscapeText)(n.text || '');
1126
+ if (n.formatting?.bold && !this.inImplicitBold)
1127
+ text = `<b>${text}</b>`;
1128
+ if (n.formatting?.italic)
1129
+ text = `<i>${text}</i>`;
1130
+ if (n.formatting?.underline)
1131
+ text = `<u>${text}</u>`;
1132
+ if (n.formatting?.subscript)
1133
+ text = `<sub>${text}</sub>`;
1134
+ if (n.formatting?.superscript)
1135
+ text = `<sup>${text}</sup>`;
1136
+ return text;
1137
+ }
1138
+ case 'paragraph': return `<p>${co}</p>`;
1139
+ case 'heading': {
1140
+ const level = Math.min(Math.max(Number(n.metadata?.level) || 1, 1), 6);
1141
+ return `<h${level}>${co}</h${level}>`;
1142
+ }
1143
+ case 'table': return await this.renderTableAsHtml(n);
1144
+ case 'list':
1145
+ case 'image':
1146
+ case 'chart':
1147
+ case 'drawing':
1148
+ case 'slide':
1149
+ case 'note':
1150
+ case 'sheet':
1151
+ case 'row':
1152
+ case 'cell':
1153
+ case 'page':
1154
+ case 'break':
1155
+ case 'code':
1156
+ case 'comment':
1157
+ case 'header':
1158
+ case 'footer':
1159
+ case 'slideMaster':
1160
+ case 'embed':
1161
+ case 'admonition':
1162
+ case 'definitionList':
1163
+ case 'definitionTerm':
1164
+ case 'definitionDescription':
1165
+ return co;
1166
+ }
1167
+ });
1168
+ }
1169
+ }
1170
+ return ` <td${rs}${cs}>${content}</td>\n`;
1171
+ }
1172
+ return '';
1173
+ }
1174
+ }
1175
+ exports.MarkdownGenerator = MarkdownGenerator;