@gmickel/gno 1.46.0 → 2.0.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 (230) hide show
  1. package/README.md +1 -1
  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.0.0.zip +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v2.0.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 +21 -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/globals.built.css +1 -1
  100. package/src/serve/public/lib/shiki-language-ids.ts +14 -0
  101. package/src/serve/resident-admission.ts +36 -36
  102. package/src/serve/resident-background-work.ts +20 -2
  103. package/src/serve/resident-request.ts +11 -5
  104. package/src/serve/resident-runtime.ts +97 -61
  105. package/src/serve/resident-shutdown.ts +153 -0
  106. package/src/serve/routes/api.ts +3 -1
  107. package/src/serve/server.ts +47 -26
  108. package/src/store/migrations/028-vector-variants.ts +54 -0
  109. package/src/store/migrations/029-graph-reference-state.ts +77 -0
  110. package/src/store/migrations/index.ts +4 -0
  111. package/src/store/sqlite/adapter.ts +251 -183
  112. package/src/store/sqlite/eligibility.ts +174 -0
  113. package/src/store/sqlite/graph-edge-application.ts +66 -0
  114. package/src/store/sqlite/graph-reference-state.ts +194 -0
  115. package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
  116. package/src/store/types.ts +80 -12
  117. package/src/store/vector/eligibility.ts +36 -0
  118. package/src/store/vector/freshness.ts +33 -6
  119. package/src/store/vector/lazy.ts +81 -0
  120. package/src/store/vector/sqlite-vec.ts +106 -54
  121. package/src/store/vector/stats.ts +14 -3
  122. package/src/store/vector/types.ts +35 -2
  123. package/src/store/vector/variant-search.ts +192 -0
  124. package/src/store/vector/variants.ts +451 -0
  125. package/vendor/converters/markitdown-ts/LICENSE +21 -0
  126. package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
  127. package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
  128. package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
  129. package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
  130. package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
  131. package/vendor/converters/markitdown-ts/package.json +77 -0
  132. package/vendor/converters/officeparser/LICENSE +21 -0
  133. package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
  134. package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
  135. package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
  136. package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
  137. package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
  138. package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
  139. package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
  140. package/vendor/converters/officeparser/dist/cli.js +381 -0
  141. package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
  142. package/vendor/converters/officeparser/dist/defaults.js +218 -0
  143. package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
  144. package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
  145. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
  146. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
  147. package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
  148. package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
  149. package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
  150. package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
  151. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
  152. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
  153. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
  154. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
  155. package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
  156. package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
  157. package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
  158. package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
  159. package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
  160. package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
  161. package/vendor/converters/officeparser/dist/index.d.ts +60 -0
  162. package/vendor/converters/officeparser/dist/index.js +72 -0
  163. package/vendor/converters/officeparser/dist/index.mjs +18 -0
  164. package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
  165. package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
  166. package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
  167. package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
  168. package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
  169. package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
  170. package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
  171. package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
  172. package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
  173. package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
  174. package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
  175. package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
  176. package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
  177. package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
  178. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
  179. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
  180. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
  181. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
  182. package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
  183. package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
  184. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
  185. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
  186. package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
  187. package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
  188. package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
  189. package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
  190. package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
  191. package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
  192. package/vendor/converters/officeparser/dist/types.js +107 -0
  193. package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
  194. package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
  195. package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
  196. package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
  197. package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
  198. package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
  199. package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
  200. package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
  201. package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
  202. package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
  203. package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
  204. package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
  205. package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
  206. package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
  207. package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
  208. package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
  209. package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
  210. package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
  211. package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
  212. package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
  213. package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
  214. package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
  215. package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
  216. package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
  217. package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
  218. package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
  219. package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
  220. package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
  221. package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
  222. package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
  223. package/vendor/converters/officeparser/package.json +147 -0
  224. package/vendor/converters/upstream-manifest.json +124 -0
  225. package/vendor/dependency-fixes/README.md +77 -0
  226. package/vendor/dependency-fixes/vendor-converters.py +83 -0
  227. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip +0 -0
  228. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip.sha256 +0 -1
  229. package/browser-extension/dist/chunk-627emwpj.js +0 -75
  230. /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
@@ -0,0 +1,1942 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HtmlGenerator = void 0;
4
+ const types_js_1 = require("../types.js");
5
+ const BaseGenerator_js_1 = require("./BaseGenerator.js");
6
+ const errorUtils_js_1 = require("../utils/errorUtils.js");
7
+ const sanitize_js_1 = require("../utils/sanitize.js");
8
+ /**
9
+ * Attributes that carry a URL and therefore must go through `sanitizeUrl` rather than plain
10
+ * escaping - an escaped `javascript:` payload is still a `javascript:` payload.
11
+ */
12
+ const URL_BEARING_ATTRS = new Set([
13
+ 'href', 'src', 'srcset', 'action', 'formaction', 'poster', 'cite', 'data', 'background', 'ping',
14
+ ]);
15
+ /**
16
+ * Renders `node.htmlAttributes` (see `BaseContentNode.htmlAttributes`) as an attribute string.
17
+ *
18
+ * This re-applies the parser's filtering rather than trusting it, because an AST can be built
19
+ * programmatically and handed straight to the generator - the parse-side pass is defence in depth,
20
+ * not the only gate. `class` is returned separately so the caller can merge it into the class
21
+ * attribute it already composes: emitting a second `class=` would be invalid HTML and, worse, a
22
+ * *fatal* XML well-formedness error once EpubGenerator converts the output to XHTML.
23
+ */
24
+ function renderHtmlAttributeBag(node, alreadyEmitted = []) {
25
+ const bag = node.htmlAttributes;
26
+ if (!bag)
27
+ return { attrs: '' };
28
+ const taken = new Set([...alreadyEmitted].map(k => k.toLowerCase()));
29
+ let attrs = '';
30
+ let className;
31
+ for (const [rawKey, rawValue] of Object.entries(bag)) {
32
+ const key = rawKey.toLowerCase();
33
+ // Same policy as the parser, restated here because this path is independently reachable.
34
+ if (/^on/i.test(key))
35
+ continue;
36
+ if (key === 'srcdoc' || key === 'style' || key === 'id')
37
+ continue;
38
+ if (!(0, sanitize_js_1.isSafeHtmlAttributeName)(key))
39
+ continue;
40
+ if (taken.has(key))
41
+ continue;
42
+ if (key === 'class') {
43
+ className = String(rawValue);
44
+ continue;
45
+ }
46
+ if (URL_BEARING_ATTRS.has(key)) {
47
+ const safe = (0, sanitize_js_1.sanitizeUrl)(String(rawValue));
48
+ if (!safe)
49
+ continue;
50
+ attrs += ` ${key}="${safe}"`;
51
+ continue;
52
+ }
53
+ attrs += ` ${key}="${(0, sanitize_js_1.escapeHtml)(String(rawValue))}"`;
54
+ }
55
+ return { attrs, className };
56
+ }
57
+ /**
58
+ * Normalizes `HtmlGeneratorConfig.standalone` (`boolean | StandaloneConfig`) into a fully
59
+ * resolved object. `true`/undefined turns every part on (a complete standalone document);
60
+ * `false` turns every part off (a bare content fragment). When an object is passed, any field
61
+ * left unspecified defaults to its "on" value, matching the boolean-shorthand semantics.
62
+ */
63
+ function resolveStandalone(standalone) {
64
+ const uniform = (on) => ({
65
+ document: on,
66
+ metaTags: on,
67
+ styles: on ? 'full' : 'none',
68
+ scripts: on,
69
+ headInjections: on,
70
+ bodyInjections: on,
71
+ });
72
+ if (standalone === undefined || typeof standalone === 'boolean') {
73
+ return uniform(standalone ?? true);
74
+ }
75
+ const on = uniform(true);
76
+ return {
77
+ document: standalone.document ?? on.document,
78
+ metaTags: standalone.metaTags ?? on.metaTags,
79
+ styles: standalone.styles ?? on.styles,
80
+ scripts: standalone.scripts ?? on.scripts,
81
+ headInjections: standalone.headInjections ?? on.headInjections,
82
+ bodyInjections: standalone.bodyInjections ?? on.bodyInjections,
83
+ };
84
+ }
85
+ /**
86
+ * Generates semantic, high-fidelity HTML from an AST.
87
+ */
88
+ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
89
+ chartCounter = 0;
90
+ isSpreadsheetMode = false;
91
+ /**
92
+ * Set while rendering a heading's children, so `formatText` can drop the run-level bold and
93
+ * font-size the `<hN>` already establishes. See the note there, and the identical flag in
94
+ * `RtfGenerator`, where the same inherited size actively shrinks the heading.
95
+ */
96
+ inHeading = false;
97
+ /** As `inHeading`, but for the inherited font size - see `hasUniformFormatting`. */
98
+ headingUniformSize = false;
99
+ constructor(ast, config) {
100
+ super('html', ast, config);
101
+ }
102
+ /**
103
+ * Generates HTML string from the provided AST.
104
+ *
105
+ * @returns An HTML string
106
+ */
107
+ async generate() {
108
+ this.isSpreadsheetMode = this.ast.content.some(n => n.type === 'sheet');
109
+ const isPresentation = this.ast.content.some(n => n.type === 'slide');
110
+ const isPdf = this.ast.content.some(n => n.type === 'page');
111
+ let containerClass = 'container';
112
+ if (this.isSpreadsheetMode)
113
+ containerClass = 'spreadsheet-container';
114
+ else if (isPresentation)
115
+ containerClass = 'presentation-container';
116
+ else if (isPdf)
117
+ containerClass = 'pdf-container';
118
+ let bodyContent = await this.processNodeArray(this.ast.content);
119
+ if (this.collectedNotes.length > 0) {
120
+ // De-duplicate by node identity first. A table row with sparse column metadata
121
+ // re-processes its cells in `case 'row'` after they were already processed for
122
+ // `childrenOutput`, so a footnote referenced inside a cell gets pushed here twice -
123
+ // the same object reference both times, which a Set collapses back to one. Genuinely
124
+ // distinct notes (even two references to the same id) are different objects and stay.
125
+ const collectedNotes = [...new Set(this.collectedNotes)];
126
+ // Footnotes/endnotes get their own <section data-footnotes> (the agreed
127
+ // contract with attribute-driven editors' footnote nodes); other note types (e.g.
128
+ // slide speaker notes) keep the existing generic notes wrapper.
129
+ const footnotes = collectedNotes.filter(n => {
130
+ const t = n.metadata?.noteType;
131
+ return t === 'footnote' || t === 'endnote';
132
+ });
133
+ const otherNotes = collectedNotes.filter(n => !footnotes.includes(n));
134
+ if (footnotes.length > 0) {
135
+ let footnotesHtml = '';
136
+ for (const note of footnotes) {
137
+ footnotesHtml += await this.processNodeRecursive(note, this.nodeProcessor.bind(this));
138
+ }
139
+ // data-footnotes carries an explicit empty value (not a bare attribute) so
140
+ // the markup is valid XHTML too - EpubGenerator embeds this verbatim, and
141
+ // XML rejects valueless attributes. HtmlParser only checks for presence.
142
+ bodyContent += `\n<section data-footnotes="">\n${footnotesHtml}\n</section>\n`;
143
+ }
144
+ if (otherNotes.length > 0) {
145
+ let notesHtml = '';
146
+ for (const note of otherNotes) {
147
+ notesHtml += await this.processNodeRecursive(note, this.nodeProcessor.bind(this));
148
+ }
149
+ bodyContent += `\n<div class="document-notes-section">\n<hr class="page-break">\n${notesHtml}\n</div>\n`;
150
+ }
151
+ }
152
+ const metadataBlock = this.config.renderMetadata ? this.renderMetadataSummary() : '';
153
+ let title = 'Document';
154
+ let metaTags = '';
155
+ let spreadsheetTabs = '';
156
+ let spreadsheetScript = '';
157
+ if (this.isSpreadsheetMode) {
158
+ const sheets = [];
159
+ for (const node of this.ast.content) {
160
+ if (node.type === 'sheet') {
161
+ const override = await this.handleOnNode(node);
162
+ if (override !== false) {
163
+ sheets.push(node);
164
+ }
165
+ }
166
+ }
167
+ const tabs = sheets.map((n, i) => {
168
+ const sheetName = n.metadata?.sheetName || `Sheet ${i + 1}`;
169
+ return `<a href="#sheet-${i}" class="spreadsheet-tab">${this.escape(sheetName)}</a>`;
170
+ }).join('');
171
+ spreadsheetTabs = `<div class="spreadsheet-tabs">${tabs}</div>`;
172
+ spreadsheetScript = `
173
+ <script>
174
+ function initSpreadsheetResizing() {
175
+ document.querySelectorAll('.excel-grid').forEach(table => {
176
+ if (table.dataset.resizingInitialized) return;
177
+ if (table.offsetWidth === 0) return; // skip hidden tables
178
+ table.dataset.resizingInitialized = 'true';
179
+
180
+ const sheetId = table.parentElement.id || 'sheet';
181
+ const docId = window.location.pathname;
182
+
183
+ // Freeze initial auto-layout widths of columns and set table layout to fixed
184
+ const colHeaders = table.querySelectorAll('.excel-col-header');
185
+ colHeaders.forEach((header, index) => {
186
+ let currentWidth = header.offsetWidth;
187
+ const savedWidth = localStorage.getItem(docId + '_' + sheetId + '_col_' + index);
188
+ if (savedWidth) {
189
+ currentWidth = parseInt(savedWidth, 10);
190
+ }
191
+ header.style.width = currentWidth + 'px';
192
+ header.style.minWidth = currentWidth + 'px';
193
+ });
194
+ table.style.width = table.offsetWidth + 'px';
195
+ table.style.tableLayout = 'fixed';
196
+
197
+ // Freeze initial auto-layout heights of rows
198
+ table.querySelectorAll('tr').forEach((row, index) => {
199
+ let currentHeight = row.offsetHeight;
200
+ const savedHeight = localStorage.getItem(docId + '_' + sheetId + '_row_' + index);
201
+ if (savedHeight) {
202
+ currentHeight = parseInt(savedHeight, 10);
203
+ }
204
+ row.style.height = currentHeight + 'px';
205
+ });
206
+
207
+ colHeaders.forEach((header, index) => {
208
+ if (header.querySelector('.col-resizer')) return;
209
+ const resizer = document.createElement('div');
210
+ resizer.className = 'col-resizer';
211
+ header.appendChild(resizer);
212
+
213
+ let startX = 0;
214
+ let startWidth = 0;
215
+ let startTableWidth = 0;
216
+
217
+ const onMouseMove = (e) => {
218
+ const width = startWidth + (e.clientX - startX);
219
+ if (width > 40) {
220
+ header.style.width = width + 'px';
221
+ header.style.minWidth = width + 'px';
222
+ table.style.width = (startTableWidth + (width - startWidth)) + 'px';
223
+ }
224
+ };
225
+
226
+ const onMouseUp = (e) => {
227
+ resizer.classList.remove('resizing');
228
+ document.removeEventListener('mousemove', onMouseMove);
229
+ document.removeEventListener('mouseup', onMouseUp);
230
+ const finalWidth = startWidth + (e.clientX - startX);
231
+ if (finalWidth > 40) {
232
+ localStorage.setItem(docId + '_' + sheetId + '_col_' + index, finalWidth);
233
+ }
234
+ };
235
+
236
+ resizer.addEventListener('mousedown', (e) => {
237
+ e.preventDefault();
238
+ e.stopPropagation();
239
+ startX = e.clientX;
240
+ startWidth = header.offsetWidth;
241
+ startTableWidth = table.offsetWidth;
242
+ resizer.classList.add('resizing');
243
+ document.addEventListener('mousemove', onMouseMove);
244
+ document.addEventListener('mouseup', onMouseUp);
245
+ });
246
+ });
247
+
248
+ table.querySelectorAll('.excel-row-num').forEach((rowHeader, index) => {
249
+ if (rowHeader.querySelector('.row-resizer')) return;
250
+ const resizer = document.createElement('div');
251
+ resizer.className = 'row-resizer';
252
+ rowHeader.appendChild(resizer);
253
+
254
+ const row = rowHeader.parentElement;
255
+ let startY = 0;
256
+ let startHeight = 0;
257
+
258
+ const onMouseMove = (e) => {
259
+ const height = startHeight + (e.clientY - startY);
260
+ if (height > 20) {
261
+ row.style.height = height + 'px';
262
+ }
263
+ };
264
+
265
+ const onMouseUp = (e) => {
266
+ resizer.classList.remove('resizing');
267
+ document.removeEventListener('mousemove', onMouseMove);
268
+ document.removeEventListener('mouseup', onMouseUp);
269
+ const finalHeight = startHeight + (e.clientY - startY);
270
+ if (finalHeight > 20) {
271
+ localStorage.setItem(docId + '_' + sheetId + '_row_' + index, finalHeight);
272
+ }
273
+ };
274
+
275
+ resizer.addEventListener('mousedown', (e) => {
276
+ e.preventDefault();
277
+ e.stopPropagation();
278
+ startY = e.clientY;
279
+ startHeight = row.offsetHeight;
280
+ resizer.classList.add('resizing');
281
+ document.addEventListener('mousemove', onMouseMove);
282
+ document.addEventListener('mouseup', onMouseUp);
283
+ });
284
+ });
285
+ });
286
+ }
287
+
288
+ function switchSheet() {
289
+ try {
290
+ let hash = window.location.hash;
291
+ if (!hash || hash === '#' || !hash.startsWith('#sheet-')) hash = '#sheet-0';
292
+
293
+ const sheets = document.querySelectorAll('.spreadsheet-sheet');
294
+ const tabs = document.querySelectorAll('.spreadsheet-tab');
295
+
296
+ if (sheets.length === 0) return;
297
+
298
+ sheets.forEach(s => s.classList.remove('active'));
299
+ tabs.forEach(t => t.classList.remove('active'));
300
+
301
+ const activeSheet = document.querySelector(hash) || sheets[0];
302
+ activeSheet.classList.add('active');
303
+
304
+ const activeTab = document.querySelector('a[href="' + hash + '"]') || tabs[0];
305
+ if (activeTab) activeTab.classList.add('active');
306
+
307
+ // Trigger chart re-render/resize when sheet becomes visible
308
+ window.dispatchEvent(new Event('resize'));
309
+ if (window.Chart) {
310
+ Object.values(window.Chart.instances || {}).forEach(chart => {
311
+ if (activeSheet.contains(chart.canvas)) {
312
+ chart.resize();
313
+ chart.update();
314
+ }
315
+ });
316
+ }
317
+
318
+ initSpreadsheetResizing();
319
+ } catch (e) {
320
+ console.error('Sheet switch failed:', e);
321
+ const firstSheet = document.querySelector('.spreadsheet-sheet');
322
+ if (firstSheet) firstSheet.classList.add('active');
323
+ }
324
+ }
325
+
326
+ window.addEventListener('hashchange', switchSheet);
327
+ if (document.readyState === 'complete') switchSheet();
328
+ else window.addEventListener('load', switchSheet);
329
+ </script>`;
330
+ }
331
+ const sa = resolveStandalone(this.config.htmlConfig.standalone);
332
+ if (sa.document && sa.metaTags) {
333
+ title = this.effectiveMetadata.title || 'Document';
334
+ metaTags = this.renderMetaTags();
335
+ }
336
+ const styleBlock = sa.styles === 'none' ? '' : `<style>${sa.styles === 'scoped'
337
+ ? this.getScopedPremiumStyles(this.isSpreadsheetMode, isPresentation, isPdf)
338
+ : this.getPremiumStyles(this.isSpreadsheetMode, isPresentation, isPdf)}</style>`;
339
+ // 'scoped' styles are anchored to this wrapper via CSS @scope, so custom properties and
340
+ // base body-level styling attach here instead of leaking onto a host page's real <html>/
341
+ // <body> when the output is embedded as a fragment.
342
+ const scopeOpen = sa.styles === 'scoped' ? '<div class="op-html-scope">' : '';
343
+ const scopeClose = sa.styles === 'scoped' ? '</div>' : '';
344
+ const inj = this.config.htmlConfig.injections;
345
+ const headInjectionsOn = sa.document && sa.headInjections;
346
+ const chartScriptTag = (sa.scripts && this.config.includeCharts) ? `<script src="${this.config.htmlConfig.chartJsSrc}"></script>` : '';
347
+ const spreadsheetScriptOut = sa.scripts ? spreadsheetScript : '';
348
+ const value = sa.document ? `<!DOCTYPE html>
349
+ <html lang="en">
350
+ <head>
351
+ ${headInjectionsOn ? inj.headStart : ''}
352
+ <meta charset="UTF-8">
353
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
354
+ <title>${this.escape(title)}</title>
355
+ ${metaTags}
356
+ ${chartScriptTag}
357
+ ${styleBlock}
358
+ ${headInjectionsOn ? inj.headEnd : ''}
359
+ </head>
360
+ <body>
361
+ ${sa.bodyInjections ? inj.bodyStart : ''}
362
+ ${scopeOpen}
363
+ <div class="${containerClass}">
364
+ <article>
365
+ ${metadataBlock}
366
+ ${bodyContent}
367
+ </article>
368
+ ${spreadsheetTabs}
369
+ </div>
370
+ ${scopeClose}
371
+ ${spreadsheetScriptOut}
372
+ ${sa.bodyInjections ? inj.bodyEnd : ''}
373
+ </body>
374
+ </html>` : `${styleBlock}${sa.bodyInjections ? inj.bodyStart : ''}${scopeOpen}<div class="${containerClass}">${metadataBlock}${bodyContent}${spreadsheetTabs}</div>${scopeClose}${spreadsheetScriptOut}${sa.bodyInjections ? inj.bodyEnd : ''}`;
375
+ return {
376
+ value,
377
+ messages: this.messages
378
+ };
379
+ }
380
+ renderMetaTags() {
381
+ if (!this.ast?.metadata)
382
+ return '';
383
+ const m = this.effectiveMetadata;
384
+ const tags = [];
385
+ if (m.author)
386
+ tags.push(`<meta name="author" content="${this.escape(m.author)}">`);
387
+ if (m.description)
388
+ tags.push(`<meta name="description" content="${this.escape(m.description)}">`);
389
+ const created = this.toIsoDate(m.created);
390
+ const modified = this.toIsoDate(m.modified);
391
+ if (created)
392
+ tags.push(`<meta name="dcterms.created" content="${created}">`);
393
+ if (modified)
394
+ tags.push(`<meta name="dcterms.modified" content="${modified}">`);
395
+ if (m.lastModifiedBy)
396
+ tags.push(`<meta name="lastModifiedBy" content="${this.escape(m.lastModifiedBy)}">`);
397
+ // Both are parsed from every OOXML/ODF core-properties block but had no output sink at
398
+ // all, so they were silently dropped on every conversion. `keywords` is the standard HTML
399
+ // meta name; `subject` has no standard equivalent, so it uses its Dublin Core name.
400
+ if (m.keywords)
401
+ tags.push(`<meta name="keywords" content="${this.escape(m.keywords)}">`);
402
+ if (m.subject)
403
+ tags.push(`<meta name="DC.subject" content="${this.escape(m.subject)}">`);
404
+ if (m.customProperties) {
405
+ for (const [key, val] of Object.entries(m.customProperties)) {
406
+ tags.push(`<meta name="custom:${this.escape(key)}" content="${this.escape(String(val))}">`);
407
+ }
408
+ }
409
+ return tags.join('\n ');
410
+ }
411
+ renderMetadataSummary() {
412
+ if (!this.ast?.metadata)
413
+ return '';
414
+ const m = this.effectiveMetadata;
415
+ let customPropsHtml = '';
416
+ if (m.customProperties && Object.keys(m.customProperties).length > 0) {
417
+ const items = Object.entries(m.customProperties)
418
+ .map(([k, v]) => `<div class="meta-tag"><strong>${this.escape(k)}:</strong> ${this.escape(String(v))}</div>`)
419
+ .join('');
420
+ customPropsHtml = `<div class="meta-custom-section">
421
+ <div class="meta-section-title">🏷️ Custom Properties</div>
422
+ <div class="meta-tags-grid">${items}</div>
423
+ </div>`;
424
+ }
425
+ const addField = (label, val) => {
426
+ if (!val)
427
+ return '';
428
+ const display = val instanceof Date ? val.toLocaleString() : val;
429
+ return `<div class="meta-item">
430
+ <div class="meta-label">${label}</div>
431
+ <div class="meta-value">${this.escape(display)}</div>
432
+ </div>`;
433
+ };
434
+ return `<div class="metadata-summary">
435
+ <div class="meta-grid">
436
+ ${addField('Title', m.title)}
437
+ ${addField('Author', m.author)}
438
+ ${addField('Created', m.created)}
439
+ ${addField('Modified', m.modified)}
440
+ </div>
441
+ ${customPropsHtml}
442
+ </div>`;
443
+ }
444
+ /**
445
+ * Processes an array of nodes, handling list grouping and nesting.
446
+ */
447
+ async processNodeArray(nodes) {
448
+ let html = '';
449
+ // Stack to track active lists. `liClose` is the currently-open item's deferred closing
450
+ // suffix (`</li>`, or `</div></li>` for a task item): a list item is rendered WITHOUT its
451
+ // close so a deeper list can land inside it (spec-valid `<li>a<ul>...</ul></li>` rather
452
+ // than the invalid `<li>a</li><ul>...</ul>` sibling shape). The close is emitted when a
453
+ // same-level sibling arrives, when the level is popped, or at the end.
454
+ const listStack = [];
455
+ const openListTag = (type, isTask) => {
456
+ if (isTask)
457
+ return '<ul data-type="taskList">';
458
+ return type === 'ordered' ? '<ol>' : '<ul>';
459
+ };
460
+ const closeListTag = (type) => type === 'ordered' ? '</ol>' : '</ul>';
461
+ const closeListsToLevel = (level) => {
462
+ while (listStack.length > 0 && listStack[listStack.length - 1].indentation > level) {
463
+ const list = listStack.pop();
464
+ html += list.liClose + closeListTag(list.type) + '\n\n';
465
+ }
466
+ };
467
+ for (const node of nodes) {
468
+ // Check if node should be filtered out or overridden
469
+ const override = await this.handleOnNode(node);
470
+ if (override === false) {
471
+ continue;
472
+ }
473
+ // A top-level footnote/endnote note is an orphan definition (the MarkdownParser
474
+ // recovers unreferenced `[^id]: ...` defs as trailing note nodes). Route it into the
475
+ // collected footnotes so it renders inside `<section data-footnotes>` - where HtmlParser
476
+ // reads it back on import - instead of inline outside the section with a dead back-link.
477
+ const orphanMeta = node.metadata;
478
+ const orphanNoteType = orphanMeta?.noteType;
479
+ if (node.type === 'note' && orphanMeta?.unreferenced && (orphanNoteType === 'footnote' || orphanNoteType === 'endnote')) {
480
+ // Only an unreferenced (orphan) definition is hoisted into <section data-footnotes>.
481
+ // The `unreferenced` guard also keeps the generators in agreement at depth: this
482
+ // routing runs in every processNodeArray call, so without it a `note` sitting as a
483
+ // CHILD of a container (a consumer-built AST; no shipped parser emits this) would be
484
+ // hoisted here and then given an `<a href="#footnote-ref-N">` back-link with no
485
+ // anchor - the exact dangling link the orphan handling removes. MarkdownGenerator's
486
+ // equivalent routing is top-level only, so gating on the flag matches it.
487
+ this.collectedNotes.push(node);
488
+ continue;
489
+ }
490
+ if (node.type === 'list') {
491
+ const meta = node.metadata;
492
+ const type = meta?.listType === 'ordered' ? 'ordered' : 'unordered';
493
+ const isTask = !!meta?.isTask;
494
+ const indentation = meta?.indentation || 0;
495
+ // Close deeper lists
496
+ closeListsToLevel(indentation);
497
+ // Handle current level
498
+ if (listStack.length > 0 && listStack[listStack.length - 1].indentation === indentation) {
499
+ const top = listStack[listStack.length - 1];
500
+ if (top.type !== type || top.isTask !== isTask) {
501
+ // Kind changed at the same level: close the open item and the old list,
502
+ // then open the replacement list.
503
+ const last = listStack.pop();
504
+ html += last.liClose + closeListTag(last.type) + '\n';
505
+ html += openListTag(type, isTask) + '\n';
506
+ listStack.push({ indentation, type, isTask, liClose: '' });
507
+ }
508
+ else {
509
+ // Sibling at the same level: close the previous item before this one opens.
510
+ html += top.liClose;
511
+ }
512
+ }
513
+ else {
514
+ // Deeper level (or the first list): open a nested list INSIDE the currently
515
+ // open item, leaving the parent <li>'s close pending on its stack frame.
516
+ html += openListTag(type, isTask) + '\n';
517
+ listStack.push({ indentation, type, isTask, liClose: '' });
518
+ }
519
+ html += await this.processNodeRecursive(node, this.nodeProcessor.bind(this), override);
520
+ // Defer this item's close so a nested list can land inside it. A string override is
521
+ // a complete replacement item that already carries its own close, so add none.
522
+ listStack[listStack.length - 1].liClose = (typeof override === 'string')
523
+ ? ''
524
+ : (isTask ? '</div></li>' : '</li>');
525
+ }
526
+ else {
527
+ // Non-list node closes all active lists
528
+ closeListsToLevel(-1);
529
+ let result = await this.processNodeRecursive(node, this.nodeProcessor.bind(this), override);
530
+ // Add a blank line after BLOCK nodes for readable HTML source. Inline nodes (a
531
+ // paragraph's text/link runs) must concatenate with no separator: adding `\n\n`
532
+ // around an inline <a> put a blank line inside the <p>, which reparsed as a stray
533
+ // space before the following punctuation (`[video](url) .`).
534
+ if (node.type !== 'text' && !result.endsWith('\n\n')) {
535
+ if (result.endsWith('\n'))
536
+ result += '\n';
537
+ else
538
+ result += '\n\n';
539
+ }
540
+ html += result;
541
+ }
542
+ }
543
+ closeListsToLevel(-1);
544
+ return html;
545
+ }
546
+ /**
547
+ * Overridden to handle children using processNodeArray for list grouping.
548
+ */
549
+ tableNestingLevel = 0;
550
+ async processNodeRecursive(node, processor, override) {
551
+ // Mirrors the check in BaseGenerator.processNodeRecursive. This override replaces that
552
+ // method entirely, so without repeating the check here the signal would be silently
553
+ // inert for this generator - which is exactly how it was missed.
554
+ (0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
555
+ const wasInHeading = this.inHeading;
556
+ const wasHeadingSize = this.headingUniformSize;
557
+ if (node.type === 'heading') {
558
+ this.inHeading = this.hasUniformFormatting(node, f => f?.bold === true);
559
+ this.headingUniformSize = this.hasUniformFormatting(node, f => !!f?.size);
560
+ }
561
+ try {
562
+ return await this.processNodeRecursiveInner(node, processor, override);
563
+ }
564
+ finally {
565
+ this.inHeading = wasInHeading;
566
+ this.headingUniformSize = wasHeadingSize;
567
+ }
568
+ }
569
+ async processNodeRecursiveInner(node, processor, override) {
570
+ // Use pre-evaluated override if provided, otherwise call handleOnNode
571
+ const actualOverride = override !== undefined ? override : await this.handleOnNode(node);
572
+ // Returning false skips the node and its children
573
+ if (actualOverride === false) {
574
+ return '';
575
+ }
576
+ // Returning a string overrides default rendering and recursion
577
+ if (typeof actualOverride === 'string') {
578
+ return actualOverride;
579
+ }
580
+ const isTable = node.type === 'table' || node.type === 'sheet';
581
+ if (isTable)
582
+ this.tableNestingLevel++;
583
+ let childrenOutput = '';
584
+ if (node.children && node.children.length > 0) {
585
+ childrenOutput = await this.processNodeArray(node.children);
586
+ }
587
+ else if (node.text && node.type !== 'text') {
588
+ // Fallback for nodes that have text property but no children (e.g. simple paragraphs)
589
+ childrenOutput = this.escape(node.text);
590
+ }
591
+ if (node.notes && node.notes.length > 0) {
592
+ if (node.type !== 'slide') {
593
+ this.collectedNotes.push(...node.notes);
594
+ }
595
+ }
596
+ let result = await processor(node, childrenOutput);
597
+ if (isTable)
598
+ this.tableNestingLevel--;
599
+ if (node.type === 'slide' && node.notes && node.notes.length > 0) {
600
+ for (const note of node.notes) {
601
+ result += await this.processNodeRecursive(note, processor);
602
+ }
603
+ }
604
+ else if (node.notes && node.notes.length > 0) {
605
+ // Emit the reference marker at the point of citation. Without this, a
606
+ // footnote/endnote would only ever show up in the collected footnotes
607
+ // section, with no indication of where it was originally cited.
608
+ for (const note of node.notes) {
609
+ const meta = note.metadata;
610
+ if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
611
+ const key = this.escape(this.getFootnoteKey(note));
612
+ result += `<sup data-footnote-ref="${key}" id="footnote-ref-${key}"><a href="#footnote-${key}">${key}</a></sup>`;
613
+ }
614
+ }
615
+ }
616
+ return result;
617
+ }
618
+ /**
619
+ * Internal processor for individual nodes.
620
+ */
621
+ async nodeProcessor(node, childrenOutput) {
622
+ // Handle Style Mapping using the semantic mapping helper
623
+ const mapping = this.getSemanticMapping(node);
624
+ // A styleMap's `output.tag` was previously written here and then shadowed by a `const tag`
625
+ // in every switch branch, so HtmlGenerator silently ignored it - while MarkdownGenerator
626
+ // and RtfGenerator both honoured it and the README documented it as working. Honoured now,
627
+ // but only through the allowlist: the tag is interpolated into both `<TAG>` and `</TAG>`,
628
+ // and the shadowing bug was the sole reason a hostile value could not inject. A rejected
629
+ // tag falls back to the default rather than being emitted.
630
+ const mappedTag = (0, sanitize_js_1.isSafeStyleMapTag)(mapping?.tag) ? mapping.tag.toLowerCase() : undefined;
631
+ if (mapping?.tag && !mappedTag) {
632
+ this.warn(types_js_1.OfficeWarningType.INVALID_STYLE_MAP_TAG, mapping.tag, node);
633
+ }
634
+ // Handle Attributes from mapping
635
+ let mappedAttrs = '';
636
+ if (mapping?.attributes) {
637
+ for (const [key, val] of Object.entries(mapping.attributes)) {
638
+ // The value is escaped, but the NAME needs validating too: a key containing a
639
+ // quote closes the attribute and opens another, so `x" onmouseover="alert(1)" z`
640
+ // yields a live event handler however carefully the value is escaped. The
641
+ // attribute bag and the parser already apply this policy; this path did not.
642
+ if (!(0, sanitize_js_1.isSafeHtmlAttributeName)(key))
643
+ continue;
644
+ mappedAttrs += ` ${key}="${this.escape(val)}"`;
645
+ }
646
+ }
647
+ // Preserved source attributes (opt-in; absent unless htmlParserConfig.preserveAttributes).
648
+ // Dedupe against whatever the mapping already emitted so a typed field always wins, and
649
+ // take the bag's `class` back as a value to merge below rather than a second attribute.
650
+ const bag = renderHtmlAttributeBag(node, Object.keys(mapping?.attributes || {}));
651
+ // Fold into mappedAttrs rather than threading a separate fragment through all ~20 emission
652
+ // sites: every site already interpolates mappedAttrs, so this cannot miss one (a miss would
653
+ // silently drop preserved attributes), and an empty bag contributes nothing, so output for
654
+ // nodes without one stays byte-identical.
655
+ mappedAttrs += bag.attrs;
656
+ // Combine classes from mapping, defaults, and any preserved source class. Merging here is
657
+ // what keeps `<p class="lead">` from either losing "lead" or emitting a duplicate `class`.
658
+ const classes = mapping?.classes ? [...mapping.classes] : [];
659
+ if (bag.className) {
660
+ for (const c of bag.className.split(/\s+/).filter(Boolean)) {
661
+ if (!classes.includes(c))
662
+ classes.push(c);
663
+ }
664
+ }
665
+ const className = classes.length > 0 ? ` class="${this.escape(classes.join(' '))}"` : '';
666
+ // Handle ID and Anchors
667
+ let idAttr = '';
668
+ let extraAnchors = '';
669
+ const anchorIds = this.config.ignoreInternalLinks ? [] : [...(node.metadata?.anchorIds || [])];
670
+ if (this.config.generateIds) {
671
+ if (node.type === 'heading') {
672
+ const slug = this.slugify(node.text || '');
673
+ if (!anchorIds.includes(slug))
674
+ anchorIds.push(slug);
675
+ }
676
+ else if (node.type === 'sheet') {
677
+ const sheetIndex = this.ast?.content.filter(n => n.type === 'sheet').indexOf(node) ?? 0;
678
+ const sheetId = `sheet-${sheetIndex}`;
679
+ if (!anchorIds.includes(sheetId))
680
+ anchorIds.push(sheetId);
681
+ }
682
+ }
683
+ if (anchorIds.length > 0) {
684
+ idAttr = ` id="${this.escape(anchorIds[0])}"`;
685
+ if (anchorIds.length > 1) {
686
+ extraAnchors = anchorIds.slice(1).map(aid => `<a id="${this.escape(aid)}" name="${this.escape(aid)}"></a>`).join('');
687
+ }
688
+ }
689
+ // Inline Styles for structural nodes
690
+ let styleAttr = '';
691
+ if (this.config.includeFormatting && node.type !== 'text') {
692
+ const styles = this.getInlineStyles(node);
693
+ if (styles)
694
+ styleAttr = ` style="${styles}"`;
695
+ }
696
+ switch (node.type) {
697
+ case 'text':
698
+ return this.formatText(node, node.text || '');
699
+ case 'image': {
700
+ if (!this.config.includeImages)
701
+ return '';
702
+ const meta = node.metadata;
703
+ const attachmentName = meta?.attachmentName;
704
+ let src = meta?.url || attachmentName || '';
705
+ if (!meta?.url && attachmentName && this.ast) {
706
+ const attachment = this.ast.attachments.find(a => a.name === attachmentName);
707
+ if (attachment) {
708
+ src = `data:${attachment.mimeType || 'image/png'};base64,${attachment.data}`;
709
+ }
710
+ }
711
+ // Match CustomImage's exact data-width/data-align + style contract so a loaded
712
+ // image re-hydrates the editor node without losing size/alignment.
713
+ let imgDataAttrs = '';
714
+ const imgStyleParts = [];
715
+ const baseImgStyle = this.getInlineStyles(node);
716
+ if (baseImgStyle)
717
+ imgStyleParts.push(baseImgStyle);
718
+ if (meta?.width) {
719
+ imgDataAttrs += ` data-width="${this.escape(meta.width)}"`;
720
+ // Sanitize before it enters the style="" attribute: an unescaped width
721
+ // (e.g. `1px" onerror="alert(1)`) would otherwise break out and inject an
722
+ // event handler, and a CSS `url(...)` would fetch a remote resource.
723
+ const safeWidth = (0, sanitize_js_1.sanitizeCssValue)(meta.width);
724
+ if (safeWidth)
725
+ imgStyleParts.push(`width: ${safeWidth}`);
726
+ }
727
+ if (meta?.align) {
728
+ imgDataAttrs += ` data-align="${this.escape(meta.align)}"`;
729
+ const ml = meta.align === 'left' ? '0' : 'auto';
730
+ const mr = meta.align === 'right' ? '0' : 'auto';
731
+ imgStyleParts.push('display: block', `margin-left: ${ml}`, `margin-right: ${mr}`);
732
+ }
733
+ const imgStyleAttr = imgStyleParts.length > 0 ? ` style="${imgStyleParts.join('; ')}"` : '';
734
+ const imgTitle = meta?.title ? ` title="${this.escape(meta.title)}"` : '';
735
+ const img = `<img src="${(0, sanitize_js_1.sanitizeImageUrl)(src)}" alt="${this.escape(node.text || meta?.altText || '')}"${imgTitle}${className}${mappedAttrs}${imgDataAttrs}${imgStyleAttr}>`;
736
+ const content = this.config.includeFormatting ? `<div class="image-container">${img}<div class="caption">${this.escape(attachmentName || '')}</div></div>` : img;
737
+ return `${extraAnchors}<div${idAttr}>${content}</div>`;
738
+ }
739
+ case 'chart': {
740
+ if (!this.config.includeCharts)
741
+ return '';
742
+ const meta = node.metadata; // ChartMetadata
743
+ this.chartCounter++;
744
+ const chartId = `chart-${this.chartCounter}`;
745
+ const chartAttName = meta?.attachmentName;
746
+ const chartAttachment = this.ast?.attachments.find(a => a.name === chartAttName);
747
+ if (chartAttachment && chartAttachment.chartData) {
748
+ const chartData = chartAttachment.chartData;
749
+ const canvas = `<div class="chart-container"><canvas id="${chartId}"></canvas></div>`;
750
+ const script = `
751
+ <script>
752
+ (function() {
753
+ const initChart = () => {
754
+ const ctx = document.getElementById('${chartId}').getContext('2d');
755
+ const chartData = ${(0, sanitize_js_1.serializeForInlineScript)(chartData)};
756
+ const getRandomColor = (index, alpha) => {
757
+ const colors = [
758
+ 'rgba(255, 99, 132, ' + alpha + ')',
759
+ 'rgba(54, 162, 235, ' + alpha + ')',
760
+ 'rgba(255, 206, 86, ' + alpha + ')',
761
+ 'rgba(75, 192, 192, ' + alpha + ')',
762
+ 'rgba(153, 102, 255, ' + alpha + ')',
763
+ 'rgba(255, 159, 64, ' + alpha + ')'
764
+ ];
765
+ return colors[index % colors.length];
766
+ };
767
+ if (typeof Chart === 'undefined') return;
768
+ try {
769
+ const canvas = document.getElementById('${chartId}');
770
+ if (!canvas) return;
771
+ const datasets = chartData.dataSets.map((ds, index) => ({
772
+ label: ds.name || 'Series ' + (index + 1),
773
+ data: ds.values.map(Number),
774
+ backgroundColor: getRandomColor(index, 0.5),
775
+ borderColor: getRandomColor(index, 1),
776
+ borderWidth: 1
777
+ }));
778
+ new Chart(canvas, {
779
+ type: 'bar',
780
+ data: {
781
+ labels: chartData.labels,
782
+ datasets: datasets
783
+ },
784
+ options: {
785
+ responsive: true,
786
+ maintainAspectRatio: false,
787
+ plugins: {
788
+ title: {
789
+ display: !!chartData.title,
790
+ text: chartData.title
791
+ }
792
+ }
793
+ }
794
+ });
795
+ } catch (e) {
796
+ console.error('Failed to initialize chart ${chartId}:', e);
797
+ }
798
+ };
799
+
800
+ let retries = 0;
801
+ const tryInit = () => {
802
+ if (typeof Chart !== 'undefined') {
803
+ initChart();
804
+ } else if (retries < 10) {
805
+ retries++;
806
+ setTimeout(tryInit, 500);
807
+ }
808
+ };
809
+
810
+ if (document.readyState === 'complete') tryInit();
811
+ else window.addEventListener('load', tryInit);
812
+ })();
813
+ </script>`;
814
+ return `${extraAnchors}${canvas}${script}`;
815
+ }
816
+ return '';
817
+ }
818
+ case 'break': {
819
+ const breakType = node.metadata?.breakType;
820
+ if (breakType === 'page')
821
+ return '<hr class="page-break">';
822
+ // A thematic break is a plain rule; the parser reads a bare <hr> back as one.
823
+ if (breakType === 'thematic')
824
+ return '<hr>';
825
+ return '<br>';
826
+ }
827
+ case 'code': {
828
+ const meta = node.metadata;
829
+ if (meta?.math) {
830
+ const tag = meta.math === 'block' ? 'div' : 'span';
831
+ if (this.config.htmlConfig.sourceAttributes) {
832
+ // Attribute-driven emission: the raw (undelimited) LaTeX lives in both
833
+ // data-math and the text content, and the class token carries the mode.
834
+ // The widened HtmlParser reads this back (a data-math value other than
835
+ // inline/block is treated as LaTeX).
836
+ const latex = node.text || '';
837
+ return `${extraAnchors}<${tag} class="math math-${this.escape(meta.math)}" data-math="${this.escape(latex)}"${idAttr}${mappedAttrs}${styleAttr}>${this.escape(latex)}</${tag}>`;
838
+ }
839
+ // Default emission: data-math names the display mode, and the visible text
840
+ // keeps its $ delimiters so the raw LaTeX degrades gracefully without a
841
+ // KaTeX renderer.
842
+ const delimited = meta.math === 'block' ? `$$${node.text || ''}$$` : `$${node.text || ''}$`;
843
+ return `${extraAnchors}<${tag} class="math math-${this.escape(meta.math)}" data-math="${this.escape(meta.math)}"${idAttr}${mappedAttrs}${styleAttr}>${this.escape(delimited)}</${tag}>`;
844
+ }
845
+ if (this.config.htmlConfig.sourceAttributes && meta?.language === 'mermaid') {
846
+ // Attribute-driven emission: a <div class="mermaid" data-mermaid> the widened
847
+ // parser maps back to a mermaid code node. escape() encodes '>' so diagram
848
+ // arrows (-->), plus newlines and quotes, stay inside the tag and attribute.
849
+ const code = node.text || '';
850
+ return `${extraAnchors}<div class="mermaid" data-mermaid="${this.escape(code)}"${idAttr}${mappedAttrs}${styleAttr}>${this.escape(code)}</div>`;
851
+ }
852
+ const lang = meta?.language ? ` class="language-${this.escape(meta.language)}"` : '';
853
+ const codeHtml = `<code${lang}>${this.escape(node.text || '')}</code>`;
854
+ // A `code` node is always block-level (inline code is a monospace text run, emitted
855
+ // as <code> by formatText). Wrap in <pre> whenever it carries a language or spans
856
+ // multiple lines; only a bare single-line, language-less code node stays a <span>.
857
+ // Previously a single-line block (e.g. a one-line ```js) emitted <span><code>, which
858
+ // re-imports as inline code and which strict CodeBlock parsers (only <pre><code>) miss.
859
+ if (meta?.language || (node.text && node.text.includes('\n'))) {
860
+ return `${extraAnchors}<pre${idAttr}${className}${mappedAttrs}${styleAttr}>${codeHtml}</pre>`;
861
+ }
862
+ else {
863
+ return `${extraAnchors}<span${idAttr}${className}${mappedAttrs}${styleAttr}>${codeHtml}</span>`;
864
+ }
865
+ }
866
+ case 'list': {
867
+ // The closing suffix (`</div></li>` for a task item, `</li>` otherwise) is emitted
868
+ // by processNodeArray's list stack, not here, so a nested list can be placed inside
869
+ // this item before it closes. See `listStack`/`liClose` there.
870
+ const meta = node.metadata;
871
+ if (meta?.isTask) {
872
+ const checkedAttr = ` data-checked="${meta.checked ? 'true' : 'false'}"`;
873
+ const checkedBool = meta.checked ? ' checked' : '';
874
+ return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}`;
875
+ }
876
+ const value = (meta?.listType === 'ordered' && typeof meta.itemIndex === 'number')
877
+ ? ` value="${meta.itemIndex + 1}"`
878
+ : '';
879
+ return `${extraAnchors}<li${value}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}`;
880
+ }
881
+ case 'table': {
882
+ // Smart Table Header Detection
883
+ let finalChildren = childrenOutput;
884
+ const rows = node.children || [];
885
+ if (rows.length > 0 && rows[0].type === 'row') {
886
+ const firstRow = rows[0];
887
+ const firstRowCells = firstRow.children || [];
888
+ // Heuristic: Is the first row a header?
889
+ // 1. Explicitly marked via style containing "Header"
890
+ // 2. All cells have bold formatting
891
+ // 3. All cells have a background color different from the second row (if exists)
892
+ const isHeaderStyle = firstRow.metadata?.style?.toLowerCase().includes('header');
893
+ const allBold = firstRowCells.length > 0 && firstRowCells.every(c => c.children?.every(child => child.formatting?.bold === true));
894
+ if (isHeaderStyle || allBold) {
895
+ // Re-process the first row as header cells, wrapped in a <tr>. Without the
896
+ // <tr>, the header cells sit directly under <thead> (`<thead><th>…`), which
897
+ // is invalid HTML that HtmlParser does not read back as a table row - so a
898
+ // md -> HTML -> md round trip lost the header content. `<thead><tr><th>…` is
899
+ // valid and self-idempotent.
900
+ const headOutput = await this.processNodeRecursive(firstRow, async (n, children) => {
901
+ return `<tr>${children.replace(/<td/g, '<th').replace(/<\/td>/g, '</th>')}</tr>`;
902
+ });
903
+ const bodyRows = rows.slice(1);
904
+ const bodyOutput = await this.processNodeArray(bodyRows);
905
+ finalChildren = `<thead>${headOutput}</thead><tbody>${bodyOutput}</tbody>`;
906
+ }
907
+ }
908
+ // Match CustomTable's exact data-align + margin style contract so a loaded
909
+ // table re-hydrates the editor node without losing its layout alignment.
910
+ const tableMeta = node.metadata;
911
+ let tableDataAttrs = '';
912
+ let tableStyleAttr = styleAttr;
913
+ if (tableMeta?.align) {
914
+ tableDataAttrs = ` data-align="${this.escape(tableMeta.align)}"`;
915
+ const ml = tableMeta.align === 'left' ? '0' : 'auto';
916
+ const mr = tableMeta.align === 'right' ? '0' : 'auto';
917
+ const marginStyle = `margin-left: ${ml}; margin-right: ${mr}`;
918
+ tableStyleAttr = styleAttr
919
+ ? ` style="${styleAttr.replace(/^ style="|"$/g, '')}; ${marginStyle}"`
920
+ : ` style="${marginStyle}"`;
921
+ }
922
+ const tableHtml = `<table${idAttr}${className}${mappedAttrs}${tableDataAttrs}${tableStyleAttr}>${finalChildren}</table>`;
923
+ const result = this.tableNestingLevel > 1 ? tableHtml : `<div class="table-container">${tableHtml}</div>`;
924
+ return `${extraAnchors}${result}`;
925
+ }
926
+ case 'row': {
927
+ if (node.children) {
928
+ let sparseChildren = '';
929
+ let lastCol = -1;
930
+ const cellNodes = node.children.filter(c => c.type === 'cell');
931
+ if (cellNodes.length > 0 && cellNodes.some(c => c.metadata?.col !== undefined)) {
932
+ for (const cell of cellNodes) {
933
+ const currentCol = cell.metadata?.col ?? (lastCol + 1);
934
+ // Fill gaps with empty cells
935
+ while (lastCol < currentCol - 1) {
936
+ sparseChildren += '<td></td>';
937
+ lastCol++;
938
+ }
939
+ sparseChildren += await this.processNodeRecursive(cell, this.nodeProcessor.bind(this));
940
+ const colSpan = cell.metadata?.colSpan || 1;
941
+ lastCol = currentCol + colSpan - 1;
942
+ }
943
+ return `<tr${idAttr}${className}${mappedAttrs}${styleAttr}>${sparseChildren}</tr>`;
944
+ }
945
+ }
946
+ return `<tr${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</tr>`;
947
+ }
948
+ case 'cell': {
949
+ const meta = node.metadata;
950
+ const rowSpan = (meta?.rowSpan && meta.rowSpan > 1) ? ` rowspan="${meta.rowSpan}"` : '';
951
+ const colSpan = (meta?.colSpan && meta.colSpan > 1) ? ` colspan="${meta.colSpan}"` : '';
952
+ return `<td${rowSpan}${colSpan}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</td>`;
953
+ }
954
+ case 'sheet': {
955
+ const rows = node.children || [];
956
+ // Find grid bounds
957
+ let maxRow = -1;
958
+ let maxCol = -1;
959
+ for (const child of rows) {
960
+ if (child.type === 'row') {
961
+ for (const cell of child.children || []) {
962
+ if (cell.type === 'cell') {
963
+ const meta = cell.metadata;
964
+ if (meta) {
965
+ const r = meta.row;
966
+ const c = meta.col;
967
+ const rSpan = meta.rowSpan || 1;
968
+ const cSpan = meta.colSpan || 1;
969
+ if (r + rSpan - 1 > maxRow)
970
+ maxRow = r + rSpan - 1;
971
+ if (c + cSpan - 1 > maxCol)
972
+ maxCol = c + cSpan - 1;
973
+ }
974
+ }
975
+ }
976
+ }
977
+ }
978
+ let tableHtml = '';
979
+ if (maxRow >= 0 && maxCol >= 0) {
980
+ // Populate cell grid and track merged cells
981
+ const grid = Array.from({ length: maxRow + 1 }, () => Array(maxCol + 1).fill(null));
982
+ const mergedCovered = Array.from({ length: maxRow + 1 }, () => Array(maxCol + 1).fill(false));
983
+ const rowNodeMap = new Map();
984
+ for (const child of rows) {
985
+ if (child.type === 'row') {
986
+ const cellsInRow = child.children?.filter(c => c.type === 'cell') || [];
987
+ if (cellsInRow.length > 0) {
988
+ const r = cellsInRow[0].metadata.row;
989
+ rowNodeMap.set(r, child);
990
+ for (const cell of cellsInRow) {
991
+ const meta = cell.metadata;
992
+ if (meta) {
993
+ const c = meta.col;
994
+ if (r >= 0 && r <= maxRow && c >= 0 && c <= maxCol) {
995
+ grid[r][c] = cell;
996
+ const rSpan = meta.rowSpan || 1;
997
+ const cSpan = meta.colSpan || 1;
998
+ if (rSpan > 1 || cSpan > 1) {
999
+ for (let rOffset = 0; rOffset < rSpan; rOffset++) {
1000
+ for (let cOffset = 0; cOffset < cSpan; cOffset++) {
1001
+ if (rOffset === 0 && cOffset === 0)
1002
+ continue;
1003
+ const targetR = r + rOffset;
1004
+ const targetC = c + cOffset;
1005
+ if (targetR <= maxRow && targetC <= maxCol) {
1006
+ mergedCovered[targetR][targetC] = true;
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+ }
1017
+ // Build column headers (A, B, C...)
1018
+ let ths = '<th class="excel-row-num-header"></th>';
1019
+ for (let c = 0; c <= maxCol; c++) {
1020
+ ths += `<th class="excel-col-header">${this.getColumnLetter(c)}</th>`;
1021
+ }
1022
+ const thead = `<thead><tr>${ths}</tr></thead>`;
1023
+ // Build rows
1024
+ let tbodyRows = '';
1025
+ for (let r = 0; r <= maxRow; r++) {
1026
+ const rowNode = rowNodeMap.get(r);
1027
+ let trAttrs = '';
1028
+ if (rowNode) {
1029
+ const mapping = this.getSemanticMapping(rowNode);
1030
+ const rClasses = ['excel-row'];
1031
+ if (mapping?.classes)
1032
+ rClasses.push(...mapping.classes);
1033
+ // Escaped like the `className` built for every other node type. This
1034
+ // path rebuilds the class attribute from the raw mapping array rather
1035
+ // than reusing that value, and was the only place it went out unescaped.
1036
+ trAttrs += ` class="${this.escape(rClasses.join(' '))}"`;
1037
+ if (mapping?.attributes) {
1038
+ for (const [key, val] of Object.entries(mapping.attributes)) {
1039
+ if (!(0, sanitize_js_1.isSafeHtmlAttributeName)(key))
1040
+ continue;
1041
+ trAttrs += ` ${key}="${this.escape(val)}"`;
1042
+ }
1043
+ }
1044
+ const rAnchorIds = this.config.ignoreInternalLinks ? [] : [...(rowNode.metadata?.anchorIds || [])];
1045
+ if (rAnchorIds.length > 0) {
1046
+ trAttrs += ` id="${this.escape(rAnchorIds[0])}"`;
1047
+ }
1048
+ if (this.config.includeFormatting) {
1049
+ const styles = this.getInlineStyles(rowNode);
1050
+ if (styles)
1051
+ trAttrs += ` style="${styles}"`;
1052
+ }
1053
+ }
1054
+ else {
1055
+ trAttrs = ' class="excel-row"';
1056
+ }
1057
+ let rowCellsHtml = `<td class="excel-row-num">${r + 1}</td>`;
1058
+ for (let c = 0; c <= maxCol; c++) {
1059
+ if (mergedCovered[r][c]) {
1060
+ continue;
1061
+ }
1062
+ const cell = grid[r][c];
1063
+ if (cell) {
1064
+ const cellHtml = await this.processNodeRecursive(cell, this.nodeProcessor.bind(this));
1065
+ rowCellsHtml += cellHtml;
1066
+ }
1067
+ else {
1068
+ rowCellsHtml += '<td class="excel-cell-empty"></td>';
1069
+ }
1070
+ }
1071
+ tbodyRows += `<tr${trAttrs}>${rowCellsHtml}</tr>\n`;
1072
+ }
1073
+ const tbody = `<tbody>${tbodyRows}</tbody>`;
1074
+ tableHtml = `<table class="spreadsheet-table excel-grid">${thead}${tbody}</table>`;
1075
+ }
1076
+ // Process non-row elements (images, charts, etc.)
1077
+ const nonRowNodes = rows.filter(c => c.type !== 'row');
1078
+ let nonRowHtml = '';
1079
+ if (nonRowNodes.length > 0) {
1080
+ nonRowHtml = await this.processNodeArray(nonRowNodes);
1081
+ }
1082
+ const isFirstSheet = this.ast.content.filter(n => n.type === 'sheet')[0] === node;
1083
+ const isActive = isFirstSheet;
1084
+ const sheetIndex = this.ast.content.filter(n => n.type === 'sheet').indexOf(node);
1085
+ const sheetId = `sheet-${sheetIndex}`;
1086
+ // Merge classes correctly to avoid duplicate class attributes
1087
+ const mergedClasses = ['spreadsheet-sheet'];
1088
+ if (isActive)
1089
+ mergedClasses.push('active');
1090
+ if (classes.length > 0)
1091
+ mergedClasses.push(...classes);
1092
+ // Escaped for the same reason as the row above; `classes` here also carries the
1093
+ // attribute bag's raw className, so escaping at the join covers both sources.
1094
+ const classAttr = ` class="${this.escape(mergedClasses.join(' '))}"`;
1095
+ // Ensure we don't have duplicate IDs
1096
+ const finalIdAttr = ` id="${sheetId}"`;
1097
+ return `${extraAnchors}<div${finalIdAttr}${classAttr}${mappedAttrs}${styleAttr}>${tableHtml}${nonRowHtml}</div>`;
1098
+ }
1099
+ case 'paragraph':
1100
+ case 'heading': {
1101
+ // The styleMap tag wins over the structural default; that is the whole point of
1102
+ // mapping "Heading 1"/"Intense Quote" onto a semantic element.
1103
+ const tag = mappedTag ?? (node.type === 'heading' ? `h${node.metadata?.level || 1}` : 'p');
1104
+ // Normalize empty paragraphs so DOCX and PPTX empty cells render with consistent height
1105
+ // Strip tags to check if it's purely empty or just contains non-breaking spaces (like PPTX)
1106
+ const textOnly = childrenOutput.replace(/<[^>]+>/g, '').trim();
1107
+ if (!textOnly && !node.children?.some(c => c.type === 'image' || c.type === 'chart')) {
1108
+ const extraClass = className ? ` class="${className.replace('class="', '').replace('"', '')} empty-paragraph"` : ' class="empty-paragraph"';
1109
+ return `${extraAnchors}<${tag}${idAttr}${extraClass}${mappedAttrs}${styleAttr}><br></${tag}>`;
1110
+ }
1111
+ return `${extraAnchors}<${tag}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</${tag}>`;
1112
+ }
1113
+ case 'slide': {
1114
+ const meta = node.metadata;
1115
+ const slideNum = this.escape(String(meta?.slideNumber || ''));
1116
+ return `${extraAnchors}<section class="slide" data-slide-num="${slideNum}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</section>`;
1117
+ }
1118
+ case 'page': {
1119
+ const meta = node.metadata;
1120
+ const pageNum = this.escape(String(meta?.pageNumber || ''));
1121
+ return `${extraAnchors}<section class="page" data-page-num="${pageNum}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</section>`;
1122
+ }
1123
+ case 'note': {
1124
+ const meta = node.metadata;
1125
+ if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
1126
+ const key = this.escape(this.getFootnoteKey(node));
1127
+ // A <div> wrapper, not <p>: childrenOutput is block content, so a <p> wrapper
1128
+ // nests <p> inside <p> (every DOM parser splits it, leaving the wrapper empty),
1129
+ // and attribute-driven editors match `div[data-footnote-id]`. This changes the
1130
+ // default footnote-definition markup, which was broken-by-construction before.
1131
+ // An unreferenced (orphan) note has no citation anchor, so the back-link would
1132
+ // dangle - omit it.
1133
+ const backLink = meta?.unreferenced ? '' : ` <a href="#footnote-ref-${key}">↩</a>`;
1134
+ return `<div id="footnote-${key}" data-footnote-id="${key}">${childrenOutput}${backLink}</div>`;
1135
+ }
1136
+ const noteClass = meta?.noteType ? ` note-${this.escape(meta.noteType)}` : '';
1137
+ return `${extraAnchors}<div class="slide-note${noteClass}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</div>`;
1138
+ }
1139
+ case 'embed': {
1140
+ const meta = node.metadata;
1141
+ if (meta?.embedType === 'iframe') {
1142
+ // Generic preserved iframe. sanitizeUrl scheme-checks the src (only http/https
1143
+ // and the other non-executing schemes survive), so a javascript:/data: src is
1144
+ // dropped even with preservation on. The node only exists via opt-in parsing or
1145
+ // a programmatic AST, so this guard is unconditional.
1146
+ const src = (0, sanitize_js_1.sanitizeUrl)(meta?.url || '');
1147
+ if (!src)
1148
+ return '';
1149
+ const w = meta?.width ? ` width="${this.escape(meta.width)}"` : '';
1150
+ const h = meta?.height ? ` height="${this.escape(meta.height)}"` : '';
1151
+ if (this.config.htmlConfig.gatedEmbeds) {
1152
+ // Inert, never-auto-loading placeholder: the editor renders click-to-load from
1153
+ // it, and HtmlParser reads it back to the same embed node. src already sanitized.
1154
+ const a = meta?.align ? ` data-embed-align="${this.escape(meta.align)}"` : '';
1155
+ const l = meta?.label ? ` data-embed-label="${this.escape(meta.label)}"` : '';
1156
+ const dw = meta?.width ? ` data-embed-width="${this.escape(meta.width)}"` : '';
1157
+ const dh = meta?.height ? ` data-embed-height="${this.escape(meta.height)}"` : '';
1158
+ return `${extraAnchors}<div data-embed-gated data-embed-src="${src}"${dw}${dh}${a}${l}${idAttr}${mappedAttrs}${styleAttr}></div>`;
1159
+ }
1160
+ return `${extraAnchors}<iframe src="${src}"${w}${h}${idAttr}${mappedAttrs}${styleAttr}></iframe>`;
1161
+ }
1162
+ // Match the attribute-driven Youtube wrapper shape so a loaded embed re-hydrates
1163
+ // an editor's Youtube node.
1164
+ const id = meta?.videoId || '';
1165
+ const width = meta?.width || '100%';
1166
+ const align = meta?.align || 'center';
1167
+ const ml = align === 'left' ? '0' : 'auto';
1168
+ const mr = align === 'right' ? '0' : 'auto';
1169
+ const iframe = id
1170
+ ? `<iframe src="https://www.youtube.com/embed/${this.escape(id)}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`
1171
+ : '';
1172
+ // Carry the human label so it survives AST -> editor-HTML -> AST, at parity with the
1173
+ // generic gated path's data-embed-label. Only emitted when a label exists (from a
1174
+ // `::youtube[Label]` directive), so unlabeled youtube embeds are byte-identical.
1175
+ const ytLabel = meta?.label ? ` data-embed-label="${this.escape(meta.label)}"` : '';
1176
+ return `${extraAnchors}<div data-youtube-video="${this.escape(id)}" data-width="${this.escape(width)}" data-align="${this.escape(align)}"${ytLabel} class="youtube-embed"${idAttr}${mappedAttrs} style="width: ${(0, sanitize_js_1.sanitizeCssValue)(width)}; margin-left: ${ml}; margin-right: ${mr};">${iframe}</div>`;
1177
+ }
1178
+ case 'admonition': {
1179
+ // Match the attribute-driven admonition wrapper so a loaded admonition
1180
+ // reaches an editor as that node instead of a plain blockquote.
1181
+ const meta = node.metadata;
1182
+ const admonitionType = this.escape(meta?.admonitionType || 'note');
1183
+ return `${extraAnchors}<div class="admonition admonition-${admonitionType}" data-type="${admonitionType}"${idAttr}${mappedAttrs}${styleAttr}>${childrenOutput}</div>`;
1184
+ }
1185
+ case 'definitionList':
1186
+ return `${extraAnchors}<dl${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dl>`;
1187
+ case 'definitionTerm':
1188
+ return `${extraAnchors}<dt${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dt>`;
1189
+ case 'definitionDescription':
1190
+ return `${extraAnchors}<dd${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dd>`;
1191
+ default:
1192
+ return childrenOutput;
1193
+ }
1194
+ }
1195
+ getDefaultTag(node) {
1196
+ switch (node.type) {
1197
+ case 'paragraph': return 'p';
1198
+ case 'heading': {
1199
+ const level = node.metadata?.level || 1;
1200
+ return `h${Math.min(Math.max(level, 1), 6)}`;
1201
+ }
1202
+ case 'list': return 'li';
1203
+ default: return 'div';
1204
+ }
1205
+ }
1206
+ formatText(node, text) {
1207
+ let result = this.escape(text);
1208
+ const f = node.formatting;
1209
+ if (this.config.includeFormatting && f) {
1210
+ // Inline code: a monospace run becomes `<code>`, not a `font-family: monospace` span, so
1211
+ // an editor keying on <code> sees it and it re-imports as inline code (HtmlParser maps
1212
+ // <code> back to a monospace run). Innermost, so bold/italic wrap it (`<b><code>…`).
1213
+ if (f.font === 'monospace')
1214
+ result = `<code>${result}</code>`;
1215
+ // Inside an `<hN>`, the heading's own styling is authoritative. A run that also carries
1216
+ // bold and a font size - the normal case for ODF, where a heading's paragraph style is
1217
+ // inherited by its runs - would wrap the text in `<b>` the heading already implies and,
1218
+ // worse, in a `<span style="font-size: 14pt">` that *shrinks* the heading to the size
1219
+ // its paragraph style happened to name. See the same suppression in RtfGenerator.
1220
+ if (f.bold && !this.inHeading)
1221
+ result = `<b>${result}</b>`;
1222
+ if (f.italic)
1223
+ result = `<i>${result}</i>`;
1224
+ if (f.underline)
1225
+ result = `<u>${result}</u>`;
1226
+ if (f.strikethrough)
1227
+ result = `<strike>${result}</strike>`;
1228
+ if (f.subscript)
1229
+ result = `<sub>${result}</sub>`;
1230
+ if (f.superscript)
1231
+ result = `<sup>${result}</sup>`;
1232
+ const styles = this.headingUniformSize
1233
+ ? this.getInlineStyles(node, { skipFontSize: true, skipBackgroundColor: true })
1234
+ : this.getInlineStyles(node, { skipBackgroundColor: true });
1235
+ if (styles) {
1236
+ result = `<span style="${styles}">${result}</span>`;
1237
+ }
1238
+ // Highlight -> <mark>, not a <span style="background-color">. Tiptap's Highlight
1239
+ // extension parseHTML matches exactly `mark`, so an editor round trip only rehydrates
1240
+ // the highlight from a <mark>; a bare span comes back as unhighlighted text. Kept
1241
+ // outside the colour/size span above so a run carrying both still rehydrates both. The
1242
+ // widened HtmlParser reads this shape back (style wins over data-color). Behaviour
1243
+ // change (was a span), noted in the changelog.
1244
+ if (f.backgroundColor) {
1245
+ const safeBg = (0, sanitize_js_1.sanitizeCssValue)(f.backgroundColor);
1246
+ if (safeBg) {
1247
+ result = `<mark data-color="${this.escape(safeBg)}" style="background-color: ${safeBg}">${result}</mark>`;
1248
+ }
1249
+ }
1250
+ }
1251
+ const meta = node.metadata;
1252
+ if (meta?.wikilink) {
1253
+ // data-wikilink-page preserves the exact page name separately from the display
1254
+ // text/alias and from href (which a host resolver may rewrite to a real URL).
1255
+ if (!this.config.ignoreInternalLinks) {
1256
+ // Attribute-driven emission adds data-wikilink/data-target (and data-alias when the
1257
+ // display text differs from the page) alongside the default attributes. It is a
1258
+ // superset - the widened parser still resolves via data-wikilink-page first.
1259
+ const extra = this.config.htmlConfig.sourceAttributes
1260
+ ? ` data-wikilink="true" data-target="${this.escape(meta.link || '')}"`
1261
+ + (node.text && node.text !== meta.link ? ` data-alias="${this.escape(node.text)}"` : '')
1262
+ : '';
1263
+ result = `<a href="#${this.escape(this.slugify(meta.link || ''))}" data-wikilink-page="${this.escape(meta.link || '')}"${extra}>${result}</a>`;
1264
+ }
1265
+ }
1266
+ else if (meta?.link) {
1267
+ const isInternal = meta.linkType !== 'external';
1268
+ if (!this.config.ignoreInternalLinks || !isInternal) {
1269
+ const linkTitle = meta.title ? ` title="${this.escape(meta.title)}"` : '';
1270
+ result = `<a href="${(0, sanitize_js_1.sanitizeUrl)(meta.link)}"${linkTitle}${meta.linkType === 'external' ? ' target="_blank"' : ''}>${result}</a>`;
1271
+ }
1272
+ }
1273
+ if (meta?.abbreviationTitle) {
1274
+ result = `<abbr title="${this.escape(meta.abbreviationTitle)}">${result}</abbr>`;
1275
+ }
1276
+ if (meta?.citationKey) {
1277
+ result = this.config.htmlConfig.sourceAttributes
1278
+ // Attribute-driven emission: a <span class="citation"> carrying data-key, which the
1279
+ // widened parser reads back (the default <cite> shape is not attribute-keyed).
1280
+ ? `<span class="citation" data-key="${this.escape(meta.citationKey)}">[@${this.escape(meta.citationKey)}]</span>`
1281
+ // Default emission: a <cite> carrying the bare key, matching Pandoc's [@citekey].
1282
+ : `<cite data-citation-key="${this.escape(meta.citationKey)}">[@${this.escape(meta.citationKey)}]</cite>`;
1283
+ }
1284
+ return result;
1285
+ }
1286
+ getInlineStyles(node, options = {}) {
1287
+ const styles = [];
1288
+ // Colors/sizes/fonts/alignments are free strings from an untrusted document;
1289
+ // run each through sanitizeCssValue so it can't break out of the style="" attribute
1290
+ // or inject a resource-fetching CSS construct. Drop the declaration if nothing survives.
1291
+ const pushSafe = (prop, value) => {
1292
+ const safe = (0, sanitize_js_1.sanitizeCssValue)(value);
1293
+ if (safe)
1294
+ styles.push(`${prop}: ${safe}`);
1295
+ };
1296
+ if (node.metadata) {
1297
+ const meta = node.metadata;
1298
+ if (meta.alignment)
1299
+ pushSafe('text-align', meta.alignment);
1300
+ // A table cell's column alignment (GFM `:---`/`:---:`/`---:`) lives on
1301
+ // `CellMetadata.align`, not `alignment`. Emit it as `text-align` on the `<th>`/`<td>`
1302
+ // so `HtmlParser` reads it back and the pipe-table markers survive AST -> HTML -> AST.
1303
+ // An unaligned cell (no `align`) adds nothing, keeping its HTML byte-identical.
1304
+ if (node.type === 'cell' && meta.align)
1305
+ pushSafe('text-align', meta.align);
1306
+ if (meta.backgroundColor)
1307
+ pushSafe('background-color', meta.backgroundColor);
1308
+ if (meta.verticalAlign)
1309
+ pushSafe('vertical-align', meta.verticalAlign);
1310
+ if (meta.paragraphIndentation) {
1311
+ const ind = meta.paragraphIndentation;
1312
+ if (ind.left)
1313
+ styles.push(`margin-left: ${ind.left / 20}pt`);
1314
+ if (ind.right)
1315
+ styles.push(`margin-right: ${ind.right / 20}pt`);
1316
+ if (ind.firstLine)
1317
+ styles.push(`text-indent: ${ind.firstLine / 20}pt`);
1318
+ }
1319
+ }
1320
+ if (node.formatting) {
1321
+ const f = node.formatting;
1322
+ if (f.color)
1323
+ pushSafe('color', f.color);
1324
+ // Highlights are emitted as <mark> by formatText (see there); when that path owns the
1325
+ // background it passes skipBackgroundColor so the colour is not also duplicated here.
1326
+ if (f.backgroundColor && !options.skipBackgroundColor)
1327
+ pushSafe('background-color', f.backgroundColor);
1328
+ if (f.size && !options.skipFontSize)
1329
+ pushSafe('font-size', f.size);
1330
+ // A monospace run is emitted as <code> by formatText, so it must not also become a
1331
+ // font-family style here (that was the old, non-semantic inline-code shape).
1332
+ if (f.font && f.font !== 'monospace') {
1333
+ const safeFont = (0, sanitize_js_1.sanitizeCssValue)(f.font);
1334
+ if (safeFont)
1335
+ styles.push(`font-family: ${safeFont}, sans-serif`);
1336
+ }
1337
+ }
1338
+ return styles.join('; ');
1339
+ }
1340
+ getPremiumStyles(isSpreadsheet = false, isPresentation = false, isPdf = false) {
1341
+ let resolvedWidth = this.config.htmlConfig.containerWidth;
1342
+ if (!resolvedWidth || resolvedWidth === 'auto') {
1343
+ if (isSpreadsheet) {
1344
+ resolvedWidth = '100%';
1345
+ }
1346
+ else if (isPresentation) {
1347
+ resolvedWidth = '297mm';
1348
+ }
1349
+ else {
1350
+ resolvedWidth = '900px';
1351
+ }
1352
+ }
1353
+ else if (typeof resolvedWidth === 'number') {
1354
+ resolvedWidth = `${resolvedWidth}px`;
1355
+ }
1356
+ return `
1357
+ :root {
1358
+ --primary-color: #2c3e50;
1359
+ --text-color: #333;
1360
+ --bg-color: #f3f4f6;
1361
+ --container-bg: #ffffff;
1362
+ --border-color: #e9ecef;
1363
+ --accent-color: #3498db;
1364
+ --shadow: 0 10px 25px rgba(0,0,0,0.05);
1365
+ --container-width: ${resolvedWidth};
1366
+ }
1367
+ * {
1368
+ box-sizing: border-box;
1369
+ }
1370
+ body {
1371
+ font-family: 'Inter', -apple-system, sans-serif;
1372
+ background-color: var(--bg-color);
1373
+ color: var(--text-color);
1374
+ line-height: 1.6;
1375
+ margin: 0;
1376
+ padding: ${isSpreadsheet ? '0' : '50px 20px'};
1377
+ }
1378
+ .container {
1379
+ max-width: var(--container-width);
1380
+ margin: 0 auto;
1381
+ background: var(--container-bg);
1382
+ padding: 60px 80px;
1383
+ border-radius: 12px;
1384
+ box-shadow: var(--shadow);
1385
+ }
1386
+ .presentation-container, .pdf-container {
1387
+ max-width: var(--container-width);
1388
+ margin: 0 auto;
1389
+ }
1390
+ .presentation-container .metadata-summary, .pdf-container .metadata-summary {
1391
+ background: white;
1392
+ margin-bottom: 40px;
1393
+ padding: 30px;
1394
+ border-radius: 12px;
1395
+ box-shadow: var(--shadow);
1396
+ }
1397
+ .chart-container {
1398
+ margin: 30px auto;
1399
+ max-width: 900px;
1400
+ padding: 25px;
1401
+ background: white;
1402
+ border: 1px solid var(--border-color);
1403
+ border-radius: 16px;
1404
+ box-shadow: var(--shadow);
1405
+ height: 450px;
1406
+ }
1407
+ .chart-container canvas {
1408
+ width: 100% !important;
1409
+ height: 100% !important;
1410
+ }
1411
+ .spreadsheet-container {
1412
+ width: 100%;
1413
+ height: 100vh;
1414
+ display: flex;
1415
+ flex-direction: column;
1416
+ background: white;
1417
+ }
1418
+ .spreadsheet-container article {
1419
+ flex: 1;
1420
+ overflow: hidden;
1421
+ display: flex;
1422
+ flex-direction: column;
1423
+ }
1424
+ h1, h2, h3, h4, h5, h6 {
1425
+ color: var(--primary-color);
1426
+ margin-top: 1.6em;
1427
+ margin-bottom: 0.8em;
1428
+ font-weight: 700;
1429
+ }
1430
+ h1 { font-size: 2.4em; border-bottom: 2px solid var(--border-color); padding-bottom: 15px; }
1431
+ h2 { font-size: 1.9em; }
1432
+ h3 { font-size: 1.5em; }
1433
+
1434
+ p { margin-bottom: 1.3em; }
1435
+ p.empty-paragraph { margin: 0; min-height: 1em; }
1436
+
1437
+ ul, ol { margin: 1em 0; padding-left: 2em; }
1438
+ li { margin-bottom: 0.25em; }
1439
+ li > p { margin-bottom: 0.25em; }
1440
+
1441
+ .table-container {
1442
+ width: fit-content;
1443
+ max-width: 100%;
1444
+ overflow-x: auto;
1445
+ -webkit-overflow-scrolling: touch;
1446
+ margin: 25px auto;
1447
+ border: 1px solid var(--border-color);
1448
+ border-radius: 8px;
1449
+ background: white;
1450
+ }
1451
+ table {
1452
+ width: auto;
1453
+ max-width: 100%;
1454
+ min-width: ${isSpreadsheet ? '100%' : '300px'};
1455
+ border-collapse: separate;
1456
+ border-spacing: 0;
1457
+ margin: 0;
1458
+ border: none;
1459
+ }
1460
+ th, td {
1461
+ padding: 8px 12px;
1462
+ border-bottom: 1px solid var(--border-color);
1463
+ border-right: 1px solid var(--border-color);
1464
+ text-align: left;
1465
+ vertical-align: top;
1466
+ overflow-wrap: break-word;
1467
+ }
1468
+ th:last-child, td:last-child {
1469
+ border-right: none;
1470
+ }
1471
+ tr:last-child th, tr:last-child td {
1472
+ border-bottom: none;
1473
+ }
1474
+ th, td > p {
1475
+ margin: 0;
1476
+ }
1477
+ td > *:last-child, th > *:last-child {
1478
+ margin-bottom: 0;
1479
+ }
1480
+ th {
1481
+ background-color: #f8f9fa;
1482
+ color: var(--primary-color);
1483
+ font-weight: 600;
1484
+ text-transform: uppercase;
1485
+ font-size: 0.85em;
1486
+ letter-spacing: 0.05em;
1487
+ position: ${isSpreadsheet ? 'sticky' : 'static'};
1488
+ top: 0;
1489
+ z-index: 10;
1490
+ }
1491
+ tr:nth-child(even) { background-color: #fdfdfd; }
1492
+ tr:hover { background-color: #f1f4f9; }
1493
+
1494
+ /* Spreadsheet Specific */
1495
+ .spreadsheet-sheet {
1496
+ display: none;
1497
+ flex: 1;
1498
+ overflow: auto;
1499
+ position: relative;
1500
+ }
1501
+ .spreadsheet-sheet.active {
1502
+ display: block;
1503
+ }
1504
+ .spreadsheet-sheet td {
1505
+ padding: 8px 12px;
1506
+ font-size: 13px;
1507
+ white-space: nowrap;
1508
+ }
1509
+ /* Spreadsheet Grid styling */
1510
+ .excel-grid {
1511
+ border-collapse: collapse;
1512
+ border-spacing: 0;
1513
+ background: var(--container-bg);
1514
+ border: 1px solid var(--border-color);
1515
+ width: max-content;
1516
+ max-width: none;
1517
+ min-width: 0;
1518
+ }
1519
+ .excel-grid th, .excel-grid td {
1520
+ border: 1px solid var(--border-color);
1521
+ padding: 4px 8px;
1522
+ font-size: 13px;
1523
+ line-height: 1.2;
1524
+ overflow: hidden;
1525
+ text-overflow: ellipsis;
1526
+ white-space: nowrap;
1527
+ }
1528
+ .excel-col-header {
1529
+ background: #f8f9fa;
1530
+ color: #5f6368;
1531
+ font-weight: 500;
1532
+ text-align: center;
1533
+ user-select: none;
1534
+ border-bottom: 2px solid var(--border-color);
1535
+ position: sticky;
1536
+ top: 0;
1537
+ z-index: 10;
1538
+ min-width: 100px;
1539
+ }
1540
+ .col-resizer {
1541
+ position: absolute;
1542
+ top: 0;
1543
+ right: 0;
1544
+ width: 4px;
1545
+ height: 100%;
1546
+ cursor: col-resize;
1547
+ user-select: none;
1548
+ z-index: 20;
1549
+ }
1550
+ .col-resizer:hover, .col-resizer.resizing {
1551
+ background: var(--accent-color, #3498db);
1552
+ }
1553
+ .excel-row-num {
1554
+ background: #f8f9fa;
1555
+ color: #5f6368;
1556
+ text-align: center;
1557
+ font-weight: 500;
1558
+ width: 45px;
1559
+ min-width: 45px !important;
1560
+ max-width: 45px;
1561
+ user-select: none;
1562
+ border-right: 2px solid var(--border-color);
1563
+ position: sticky;
1564
+ left: 0;
1565
+ z-index: 5;
1566
+ }
1567
+ .row-resizer {
1568
+ position: absolute;
1569
+ bottom: 0;
1570
+ left: 0;
1571
+ width: 100%;
1572
+ height: 4px;
1573
+ cursor: row-resize;
1574
+ user-select: none;
1575
+ z-index: 20;
1576
+ }
1577
+ .row-resizer:hover, .row-resizer.resizing {
1578
+ background: var(--accent-color, #3498db);
1579
+ }
1580
+ .excel-row-num-header {
1581
+ background: #f1f3f4;
1582
+ width: 45px;
1583
+ min-width: 45px !important;
1584
+ max-width: 45px;
1585
+ position: sticky;
1586
+ top: 0;
1587
+ left: 0;
1588
+ z-index: 15;
1589
+ border-right: 2px solid var(--border-color);
1590
+ border-bottom: 2px solid var(--border-color);
1591
+ }
1592
+ .excel-cell-empty {
1593
+ background: var(--container-bg);
1594
+ }
1595
+ .excel-grid tr:hover td {
1596
+ background-color: #f1f4f9;
1597
+ }
1598
+ .excel-grid tr:hover td.excel-row-num {
1599
+ background-color: #f8f9fa; /* Keep header color */
1600
+ }
1601
+
1602
+ /* Tab Bar */
1603
+ .spreadsheet-tabs {
1604
+ background: #f1f3f4;
1605
+ border-top: 1px solid var(--border-color);
1606
+ display: flex;
1607
+ padding: 0 20px;
1608
+ z-index: 9999;
1609
+ height: 35px;
1610
+ align-items: center;
1611
+ overflow-x: auto;
1612
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
1613
+ }
1614
+ .spreadsheet-tab {
1615
+ padding: 0 20px;
1616
+ height: 100%;
1617
+ display: flex;
1618
+ align-items: center;
1619
+ text-decoration: none;
1620
+ color: #5f6368;
1621
+ font-size: 13px;
1622
+ border-right: 1px solid var(--border-color);
1623
+ background: #f1f3f4;
1624
+ white-space: nowrap;
1625
+ transition: all 0.2s;
1626
+ cursor: pointer;
1627
+ }
1628
+ .spreadsheet-tab:hover { background: #e8eaed; }
1629
+ .spreadsheet-tab.active { background: white; color: var(--accent-color); font-weight: 600; border-bottom: 2px solid var(--accent-color); }
1630
+
1631
+ /* Slide & Page Separation */
1632
+ .slide {
1633
+ background: white;
1634
+ aspect-ratio: 297 / 210;
1635
+ margin: 0 auto 40px auto;
1636
+ padding: 4% 6%;
1637
+ border-radius: 12px;
1638
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
1639
+ position: relative;
1640
+ display: flex;
1641
+ flex-direction: column;
1642
+ justify-content: flex-start;
1643
+ border: 1px solid var(--border-color);
1644
+ box-sizing: border-box;
1645
+ width: 100%;
1646
+ overflow: hidden;
1647
+ overflow-y: auto;
1648
+ overflow-wrap: break-word;
1649
+ page-break-after: always;
1650
+ }
1651
+ .slide:has(+ .slide-note) {
1652
+ margin-bottom: 0;
1653
+ border-bottom-left-radius: 0;
1654
+ border-bottom-right-radius: 0;
1655
+ border-bottom: none;
1656
+ }
1657
+ .slide table {
1658
+ width: auto;
1659
+ max-width: 100%;
1660
+ font-size: 0.85em;
1661
+ margin: 10px 0;
1662
+ }
1663
+ .slide th, .slide td {
1664
+ padding: 6px 10px;
1665
+ }
1666
+ .slide > * {
1667
+ flex-shrink: 0;
1668
+ }
1669
+ .slide > .table-container {
1670
+ flex-shrink: 1;
1671
+ min-height: 0;
1672
+ }
1673
+ .slide .table-container {
1674
+ margin: 10px auto;
1675
+ overflow-y: auto;
1676
+ }
1677
+ .slide .chart-container {
1678
+ margin: 15px auto;
1679
+ max-width: 100%;
1680
+ padding: 10px;
1681
+ height: 240px;
1682
+ box-shadow: none;
1683
+ border-radius: 8px;
1684
+ }
1685
+ .slide img {
1686
+ max-height: 200px;
1687
+ width: auto;
1688
+ margin: 10px auto;
1689
+ box-shadow: none;
1690
+ }
1691
+ .slide .image-container {
1692
+ margin: 15px 0;
1693
+ }
1694
+ .slide h1 { font-size: 1.8em; margin-top: 0.4em; margin-bottom: 0.3em; padding-bottom: 5px; }
1695
+ .slide h2 { font-size: 1.4em; margin-top: 0.4em; }
1696
+ .slide h3 { font-size: 1.25em; }
1697
+ .slide p { margin-bottom: 0.6em; font-size: 0.95em; }
1698
+ .slide li > p { margin-bottom: 0.25em; }
1699
+ .slide ul, .slide ol { margin: 0.6em 0; }
1700
+ .slide li { margin-bottom: 0.25em; }
1701
+ .slide-note {
1702
+ background: #fdfdfd;
1703
+ border: 1px solid var(--border-color);
1704
+ border-top: 1px dashed var(--border-color);
1705
+ border-radius: 0 0 12px 12px;
1706
+ padding: 30px 50px;
1707
+ margin: 0 auto 40px auto;
1708
+ width: 100%;
1709
+ box-sizing: border-box;
1710
+ font-size: 0.95em;
1711
+ color: var(--text-color);
1712
+ position: relative;
1713
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
1714
+ }
1715
+ .note-footnote, .note-endnote {
1716
+ margin: 30px auto;
1717
+ max-width: 90%;
1718
+ }
1719
+ .slide-note::before {
1720
+ content: "SLIDE NOTES";
1721
+ font-size: 0.7rem;
1722
+ font-weight: 800;
1723
+ color: #b2bec3;
1724
+ display: block;
1725
+ margin-bottom: 12px;
1726
+ letter-spacing: 1.5px;
1727
+ }
1728
+ .note-footnote::before { content: "FOOTNOTE" !important; }
1729
+ .note-endnote::before { content: "ENDNOTE" !important; }
1730
+ .slide-note p { margin-bottom: 0.8em; }
1731
+
1732
+ .slide::after {
1733
+ content: "Slide " attr(data-slide-num);
1734
+ position: absolute;
1735
+ bottom: 20px;
1736
+ right: 30px;
1737
+ font-size: 0.8em;
1738
+ color: #999;
1739
+ font-weight: 500;
1740
+ }
1741
+ .page {
1742
+ background: white;
1743
+ aspect-ratio: 1 / 1.4142;
1744
+ margin: 0 auto 40px auto;
1745
+ padding: 8% 10%;
1746
+ box-shadow: 0 5px 20px rgba(0,0,0,0.08);
1747
+ position: relative;
1748
+ border: 1px solid var(--border-color);
1749
+ box-sizing: border-box;
1750
+ width: 100%;
1751
+ page-break-after: always;
1752
+ }
1753
+ .page::after {
1754
+ content: "Page " attr(data-page-num);
1755
+ position: absolute;
1756
+ bottom: 20px;
1757
+ right: 30px;
1758
+ font-size: 0.8em;
1759
+ color: #999;
1760
+ font-weight: 500;
1761
+ }
1762
+
1763
+ /* High Fidelity Presentation Mode */
1764
+ @media screen and (max-width: 800px) {
1765
+ .slide { min-height: auto; aspect-ratio: auto; padding: 40px 30px; }
1766
+ .page { aspect-ratio: auto; padding: 40px; }
1767
+ }
1768
+
1769
+ /* Nested Table Styles */
1770
+ td table {
1771
+ margin: 15px 0;
1772
+ border-radius: 6px;
1773
+ box-shadow: 0 2px 8px rgba(0,0,0,0.03);
1774
+ background-color: #ffffff;
1775
+ border: 1px solid var(--border-color);
1776
+ overflow: hidden;
1777
+ }
1778
+ td td {
1779
+ padding: 10px 14px;
1780
+ font-size: 0.95em;
1781
+ }
1782
+
1783
+ img {
1784
+ max-width: 100%;
1785
+ height: auto;
1786
+ border-radius: 10px;
1787
+ display: block;
1788
+ margin: 40px auto;
1789
+ box-shadow: 0 15px 35px rgba(0,0,0,0.1);
1790
+ }
1791
+
1792
+ .image-container { text-align: center; margin: 30px 0; }
1793
+ .caption { font-size: 0.8em; color: #636e72; margin-top: 8px; font-style: italic; }
1794
+
1795
+ body { margin: 0; padding: 0; }
1796
+
1797
+ .page-break { border: none; border-top: 2px dashed var(--border-color); margin: 40px 0; position: relative; }
1798
+ .page-break::after { content: 'PAGE BREAK'; position: absolute; top: -10px; left: 50%; transform: translateX(-50%); background: white; padding: 0 15px; font-size: 10px; color: #b2bec3; font-weight: bold; letter-spacing: 1px; }
1799
+
1800
+ /* Metadata Styles */
1801
+ .metadata-summary {
1802
+ background: #f8f9fa;
1803
+ border: 1px solid #e9ecef;
1804
+ border-radius: 12px;
1805
+ padding: 25px;
1806
+ margin: ${isSpreadsheet ? '20px' : '0 0 40px 0'};
1807
+ }
1808
+ .meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; }
1809
+ .meta-item { border-bottom: 1px solid #eee; padding-bottom: 8px; }
1810
+ .meta-label { font-size: 0.65rem; color: #adb5bd; text-transform: uppercase; font-weight: 700; letter-spacing: 0.5px; }
1811
+ .meta-value { font-size: 0.9rem; color: #495057; font-weight: 600; }
1812
+ .meta-custom-section { margin-top: 20px; border-top: 2px solid #eee; padding-top: 15px; }
1813
+ .meta-section-title { font-size: 0.75rem; color: var(--accent-color); font-weight: 700; margin-bottom: 10px; }
1814
+ .meta-tags-grid { display: flex; flex-wrap: wrap; gap: 8px; }
1815
+ .meta-tag { background: rgba(52, 152, 219, 0.05); border: 1px solid rgba(52, 152, 219, 0.1); padding: 4px 10px; border-radius: 6px; font-size: 0.8rem; color: #495057; }
1816
+
1817
+ span[style*="color"] { font-weight: inherit; }
1818
+
1819
+ /* --- Print Optimization --- */
1820
+ @media print {
1821
+ @page {
1822
+ margin: 1.5cm;
1823
+ }
1824
+
1825
+ body {
1826
+ background: white !important;
1827
+ color: black !important;
1828
+ margin: 0 !important;
1829
+ padding: 0 !important;
1830
+ }
1831
+
1832
+ /* Hide web-only interactive elements */
1833
+ .spreadsheet-tabs, .sync-btn, button, .reparse-btn {
1834
+ display: none !important;
1835
+ }
1836
+
1837
+ /* Flatten Spreadsheets: Show all sheets in PDF */
1838
+ .spreadsheet-sheet {
1839
+ display: block !important;
1840
+ opacity: 1 !important;
1841
+ visibility: visible !important;
1842
+ page-break-after: always !important;
1843
+ margin-bottom: 3rem !important;
1844
+ height: auto !important;
1845
+ min-height: auto !important;
1846
+ overflow: visible !important;
1847
+ }
1848
+
1849
+ .page, .slide, .metadata-summary, .container, .pdf-container, .presentation-container, article {
1850
+ box-shadow: none !important;
1851
+ border: none !important;
1852
+ page-break-inside: avoid !important;
1853
+ break-inside: avoid !important;
1854
+ margin-bottom: 2rem !important;
1855
+ max-width: none !important;
1856
+ width: 100% !important;
1857
+ height: auto !important;
1858
+ min-height: auto !important;
1859
+ overflow: visible !important;
1860
+ display: block !important;
1861
+ }
1862
+
1863
+ h1, h2, h3, h4, h5, h6 {
1864
+ page-break-after: avoid !important;
1865
+ break-after: avoid !important;
1866
+ }
1867
+
1868
+ table, tr, img, .chart-container, li, .image-container {
1869
+ page-break-inside: avoid !important;
1870
+ break-inside: avoid !important;
1871
+ }
1872
+
1873
+ a {
1874
+ text-decoration: none !important;
1875
+ color: black !important;
1876
+ }
1877
+
1878
+ /* Avoid orphans/widows */
1879
+ p, li {
1880
+ orphans: 3;
1881
+ widows: 3;
1882
+ }
1883
+
1884
+ /* Ensure full width and reset web-specific heights */
1885
+ .page, .slide, .spreadsheet-sheet {
1886
+ width: 100% !important;
1887
+ max-width: none !important;
1888
+ min-height: auto !important;
1889
+ padding: 0 !important;
1890
+ margin: 0 0 2rem 0 !important;
1891
+ border: none !important;
1892
+ display: block !important;
1893
+ height: auto !important;
1894
+ }
1895
+ }
1896
+
1897
+ /* --- Custom User CSS --- */
1898
+ ${this.config.htmlConfig.customCss}
1899
+ `;
1900
+ }
1901
+ /**
1902
+ * Same as `getPremiumStyles()`, but wrapped in a CSS `@scope` block anchored to the
1903
+ * `.op-html-scope` wrapper so the rules only apply within the generated fragment - they
1904
+ * cannot leak onto a host page's own elements. `:root` and `body` selectors specifically
1905
+ * target the real page root/body, so they're remapped to `:scope` (the scope root, i.e. the
1906
+ * `.op-html-scope` wrapper) first; every other selector is naturally confined by `@scope`
1907
+ * without needing per-selector rewriting. `customCss` is included in this scoping too.
1908
+ */
1909
+ getScopedPremiumStyles(isSpreadsheet = false, isPresentation = false, isPdf = false) {
1910
+ const css = this.getPremiumStyles(isSpreadsheet, isPresentation, isPdf)
1911
+ .replace(/:root(\s*\{)/g, ':scope$1')
1912
+ .replace(/(^|\n)(\s*)body(\s*\{)/g, '$1$2:scope$3');
1913
+ return `@scope (.op-html-scope) {\n${css}\n}`;
1914
+ }
1915
+ slugify(text) {
1916
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
1917
+ }
1918
+ getColumnLetter(colIndex) {
1919
+ let temp = colIndex;
1920
+ let letter = '';
1921
+ while (temp >= 0) {
1922
+ letter = String.fromCharCode((temp % 26) + 65) + letter;
1923
+ temp = Math.floor(temp / 26) - 1;
1924
+ }
1925
+ return letter;
1926
+ }
1927
+ // Attribute/text escaping, URL sanitizing, and inline-script serialization all
1928
+ // live in ../utils/sanitize.js so every generator shares one implementation.
1929
+ // escape() stays as a thin wrapper because it has many call sites here.
1930
+ escape(text) {
1931
+ return (0, sanitize_js_1.escapeHtml)(text);
1932
+ }
1933
+ /** Converts a document-supplied date to an ISO string, or '' if it is invalid
1934
+ * (a malformed date would otherwise throw a RangeError and abort generation). */
1935
+ toIsoDate(value) {
1936
+ if (value === undefined || value === null || value === '')
1937
+ return '';
1938
+ const d = new Date(value);
1939
+ return isNaN(d.getTime()) ? '' : d.toISOString();
1940
+ }
1941
+ }
1942
+ exports.HtmlGenerator = HtmlGenerator;