@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,1819 @@
1
+ "use strict";
2
+ /**
3
+ * OpenDocument Format (ODF) Parser
4
+ *
5
+ * **ODF Overview:**
6
+ * ODF is an open standard for office documents (ISO/IEC 26300).
7
+ * Used by LibreOffice, OpenOffice, and other applications.
8
+ *
9
+ * **File Structure:**
10
+ * ODF files are ZIP archives containing:
11
+ * - `mimetype` - File type identification
12
+ * - `content.xml` - Main document content
13
+ * - `styles.xml` - Style definitions
14
+ * - `meta.xml` - Document metadata
15
+ * - `Pictures/*` - Embedded images
16
+ *
17
+ * **Supported Formats:**
18
+ * - ODT: Text documents (application/vnd.oasis.opendocument.text)
19
+ * - ODP: Presentations (application/vnd.oasis.opendocument.presentation)
20
+ * - ODS: Spreadsheets (application/vnd.oasis.opendocument.spreadsheet)
21
+ *
22
+ * @module OpenOfficeParser
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.parseOpenOffice = void 0;
26
+ const types_js_1 = require("../types.js");
27
+ const astUtils_js_1 = require("../utils/astUtils.js");
28
+ const chartUtils_js_1 = require("../utils/chartUtils.js");
29
+ const errorUtils_js_1 = require("../utils/errorUtils.js");
30
+ const mathUtils_js_1 = require("../utils/mathUtils.js");
31
+ /**
32
+ * Tracks how many table cells a single document has been allowed to materialize.
33
+ *
34
+ * ODF encodes runs of identical cells/rows as `table:number-columns-repeated` and
35
+ * `table:number-rows-repeated` rather than repeating markup, so a few hundred bytes of XML can ask
36
+ * for an arbitrary number of nodes - and the two multiply, so a row repeat times a column repeat
37
+ * compounds it. The ZIP limits cannot catch this: the XML is tiny before decompression and the
38
+ * expansion happens afterwards, while building the AST.
39
+ *
40
+ * The budget bounds what gets *materialized*, never the attribute itself. Capping the attribute
41
+ * would break ordinary documents - LibreOffice routinely writes `number-rows-repeated="1048566"`
42
+ * to mean "the rest of the sheet is empty", and those runs are legitimate.
43
+ *
44
+ * Warns once per document rather than per clamp, so a wide sheet doesn't emit thousands of
45
+ * identical warnings.
46
+ */
47
+ class CellBudget {
48
+ limit;
49
+ config;
50
+ remaining;
51
+ warned = false;
52
+ constructor(limit, config) {
53
+ this.limit = limit;
54
+ this.config = config;
55
+ this.remaining = limit;
56
+ }
57
+ /** How many of `wanted` may be created; 0 once exhausted. */
58
+ take(wanted) {
59
+ // `!(wanted > 0)` rather than `wanted <= 0` so a NaN is rejected too: `NaN <= 0` is
60
+ // false, so a garbage repeat attribute (`parseInt("abc")`) would otherwise fall through
61
+ // and drain the entire remaining budget, dropping every legitimate cell that followed.
62
+ if (!(wanted > 0))
63
+ return 0;
64
+ if (this.remaining <= 0) {
65
+ this.warn();
66
+ return 0;
67
+ }
68
+ if (wanted <= this.remaining) {
69
+ this.remaining -= wanted;
70
+ return wanted;
71
+ }
72
+ const granted = this.remaining;
73
+ this.remaining = 0;
74
+ this.warn();
75
+ return granted;
76
+ }
77
+ get exhausted() { return this.remaining <= 0; }
78
+ warn() {
79
+ if (this.warned)
80
+ return;
81
+ this.warned = true;
82
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.TABLE_CELL_LIMIT_EXCEEDED, this.config, this.limit);
83
+ }
84
+ }
85
+ /** Resolves the configured cell budget, falling back to the documented default. */
86
+ const createCellBudget = (config) => new CellBudget(config.decompressionLimits?.maxTableCells ?? 1000000, config);
87
+ /**
88
+ * Merges a style's formatting over what it inherits, dropping any flag the style explicitly turns
89
+ * off rather than carrying a `false` forward.
90
+ *
91
+ * Generators all test these flags for truthiness, so a retained `false` would render the same - but
92
+ * it would not *compare* the same, and `MarkdownGenerator.optimizeNodes` merges adjacent text nodes
93
+ * only when their formatting objects are equal. Leaving `bold: false` on one node and nothing on
94
+ * its neighbour would silently stop that merge and fragment the output. Same reasoning, and same
95
+ * shape, as `WordParser`'s direct-run-property merge.
96
+ */
97
+ const mergeFormatting = (inherited, override) => {
98
+ if (!override)
99
+ return { ...inherited };
100
+ const merged = { ...inherited };
101
+ for (const key of Object.keys(override)) {
102
+ const value = override[key];
103
+ if (value === false)
104
+ delete merged[key];
105
+ else if (value !== undefined)
106
+ merged[key] = value;
107
+ }
108
+ return merged;
109
+ };
110
+ const toRepeatCount = (attr) => {
111
+ const n = parseInt(attr || "1");
112
+ return Number.isFinite(n) && n > 0 ? n : 1;
113
+ };
114
+ const imageUtils_js_1 = require("../utils/imageUtils.js");
115
+ const ocrUtils_js_1 = require("../utils/ocrUtils.js");
116
+ const xmlUtils_js_1 = require("../utils/xmlUtils.js");
117
+ const zipUtils_js_1 = require("../utils/zipUtils.js");
118
+ /**
119
+ * Helper to clean and extract attachment name from xlink:href or paths.
120
+ * Handles trailing slashes, leading "./", and subdirectories.
121
+ */
122
+ const cleanAttachmentName = (href) => {
123
+ if (!href)
124
+ return '';
125
+ const cleaned = href.replace(/^\.\//, '').replace(/\/$/, '');
126
+ return cleaned.split('/').pop() || '';
127
+ };
128
+ /** The ODF document types this parser handles, used to validate a caller-supplied file type. */
129
+ const ODF_FILE_TYPES = ['odt', 'odp', 'ods'];
130
+ /**
131
+ * Parses an OpenOffice document (.odt, .odp, .ods) and extracts content.
132
+ *
133
+ * @param buffer - The ODF file as a Buffer
134
+ * @param config - Parser configuration
135
+ * @returns A promise resolving to the parsed AST
136
+ */
137
+ const parseOpenOffice = async (buffer, config) => {
138
+ // Honour cancellation requests immediately — before extracting the ZIP archive.
139
+ // ODF containers (ODT/ODS/ODP) bundle content.xml, styles.xml, and media files;
140
+ // aborting early avoids needlessly inflating and parsing all of those resources.
141
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
142
+ const contentFileRegex = /content\.xml/;
143
+ const objectContentFileRegex = /Object \d+\/content\.xml/;
144
+ const mediaFileRegex = /(Pictures|media)\/.*/;
145
+ const metaFileRegex = /meta\.xml/;
146
+ const stylesFileRegex = /styles\.xml/;
147
+ const mimetypeFileRegex = /mimetype/;
148
+ const files = await (0, zipUtils_js_1.extractFiles)(buffer, x => !!x.match(contentFileRegex) ||
149
+ !!x.match(objectContentFileRegex) ||
150
+ !!x.match(metaFileRegex) ||
151
+ !!x.match(stylesFileRegex) ||
152
+ !!x.match(mimetypeFileRegex) ||
153
+ (!!config.extractAttachments && !!x.match(mediaFileRegex)), config.decompressionLimits, config);
154
+ // 1. Determine File Type
155
+ const mimetypeFile = files.find(f => f.path === 'mimetype');
156
+ // The archive's own mimetype entry is authoritative when present. When it is missing,
157
+ // fall back to the type the caller asked for (or that was derived from the extension)
158
+ // rather than assuming text: guessing 'odt' for a spreadsheet sends the parser down the
159
+ // office:text branch, which finds nothing in an office:spreadsheet body and yields an
160
+ // empty document for a perfectly valid file.
161
+ let fileType = ODF_FILE_TYPES.includes(config.fileType)
162
+ ? config.fileType
163
+ : 'odt';
164
+ if (mimetypeFile) {
165
+ const mime = mimetypeFile.content.toString().trim();
166
+ if (mime.includes('spreadsheet'))
167
+ fileType = 'ods';
168
+ else if (mime.includes('presentation'))
169
+ fileType = 'odp';
170
+ else if (mime.includes('text'))
171
+ fileType = 'odt';
172
+ }
173
+ // The document body is the content.xml at the archive root. The fallback stays anchored
174
+ // and excludes embedded objects: an ODF file can carry Object N/content.xml for a chart
175
+ // or formula, and an unanchored match would promote one of those to the document body
176
+ // when the real one is missing, silently parsing a chart as if it were the whole file.
177
+ const mainContentFile = files.find(f => f.path === 'content.xml')
178
+ || (0, zipUtils_js_1.findRequiredPart)(files, path => /(^|\/)content\.xml$/.test(path) && !objectContentFileRegex.test(path), config, { fileType, part: 'content.xml' });
179
+ const stylesFile = files.find(f => f.path === 'styles.xml');
180
+ const stylesDom = stylesFile ? (0, xmlUtils_js_1.parseXmlString)(stylesFile.content.toString()) : undefined;
181
+ const content = [];
182
+ const notes = [];
183
+ // Style Map: styleName -> TextFormatting
184
+ // Inline style parsing (from content.xml automatic styles)
185
+ const styleMap = {};
186
+ const paragraphStyleMap = {};
187
+ const listCounters = {}; // Track item index per listId/level
188
+ let currentListId = null;
189
+ let lastListType = null;
190
+ let lastListStyle = null;
191
+ let listIdCounter = 0;
192
+ let lastWasList = false;
193
+ let traverse;
194
+ // Helper to parse styles
195
+ const parseStyles = (scope) => {
196
+ const styles = (0, xmlUtils_js_1.getElementsByTagName)(scope, "style:style");
197
+ for (const style of styles) {
198
+ const name = style.getAttribute("style:name");
199
+ if (!name)
200
+ continue;
201
+ const styleInfo = {};
202
+ // Parse paragraph properties for alignment and drop caps
203
+ const paraProps = (0, xmlUtils_js_1.getFirstElementByTagName)(style, "style:paragraph-properties");
204
+ if (paraProps) {
205
+ const textAlign = paraProps.getAttribute("fo:text-align");
206
+ if (textAlign) {
207
+ const alignMap = {
208
+ 'start': 'left',
209
+ 'left': 'left',
210
+ 'center': 'center',
211
+ 'end': 'right',
212
+ 'right': 'right',
213
+ 'justify': 'justify'
214
+ };
215
+ if (alignMap[textAlign]) {
216
+ styleInfo.alignment = alignMap[textAlign];
217
+ }
218
+ }
219
+ // Detect Drop Caps
220
+ const dropCap = (0, xmlUtils_js_1.getFirstElementByTagName)(paraProps, "style:drop-cap");
221
+ if (dropCap) {
222
+ styleInfo.dropCap = true;
223
+ }
224
+ // Page/column breaks. ODF attaches these to the paragraph style rather than
225
+ // writing an inline element the way DOCX's `<w:br w:type="page"/>` does, which is
226
+ // why `includeBreakNodes` produced nothing at all for ODF: there was no inline
227
+ // element to find. Only the two break kinds that map onto a BreakMetadata type
228
+ // are carried; `auto` and `even-page`/`odd-page` have no equivalent.
229
+ const breakBefore = paraProps.getAttribute("fo:break-before");
230
+ if (breakBefore === 'page' || breakBefore === 'column')
231
+ styleInfo.breakBefore = breakBefore;
232
+ const breakAfter = paraProps.getAttribute("fo:break-after");
233
+ if (breakAfter === 'page' || breakAfter === 'column')
234
+ styleInfo.breakAfter = breakAfter;
235
+ }
236
+ if (Object.keys(styleInfo).length > 0) {
237
+ paragraphStyleMap[name] = styleInfo;
238
+ }
239
+ // Parse text properties
240
+ const textProps = (0, xmlUtils_js_1.getFirstElementByTagName)(style, "style:text-properties");
241
+ // Parse table cell properties (for ODS background)
242
+ const cellProps = (0, xmlUtils_js_1.getFirstElementByTagName)(style, "style:table-cell-properties");
243
+ const formatting = {};
244
+ if (cellProps) {
245
+ const bgColor = cellProps.getAttribute("fo:background-color");
246
+ if (bgColor && bgColor !== 'transparent')
247
+ formatting.backgroundColor = bgColor;
248
+ }
249
+ if (textProps) {
250
+ // Record the *off* states as an explicit `false`, not as an absent key.
251
+ //
252
+ // Now that a paragraph style's text properties are inherited by the runs inside it,
253
+ // a span has to be able to turn one back off: LibreOffice writes
254
+ // `fo:font-weight="normal"` on the span whenever a user un-bolds part of a
255
+ // bold-styled paragraph. With only the `true` side recorded, that span had nothing
256
+ // to override the inherited value with and came out bold - wrong in the opposite
257
+ // direction from the bug the inheritance fixed. `TextFormatting`'s flags are
258
+ // `boolean | undefined` precisely so "explicitly off" is expressible.
259
+ const fontWeight = textProps.getAttribute("fo:font-weight") || textProps.getAttribute("style:font-weight-asian");
260
+ // Numeric weights are the same axis: 600+ is bold, below that is not.
261
+ if (fontWeight)
262
+ formatting.bold = fontWeight === "bold" || /^[6-9]00$/.test(fontWeight);
263
+ const fontStyle = textProps.getAttribute("fo:font-style") || textProps.getAttribute("style:font-style-asian");
264
+ if (fontStyle)
265
+ formatting.italic = fontStyle === "italic" || fontStyle === "oblique";
266
+ const underline = textProps.getAttribute("style:text-underline-style");
267
+ if (underline)
268
+ formatting.underline = underline !== "none";
269
+ const lineThrough = textProps.getAttribute("style:text-line-through-style");
270
+ if (lineThrough)
271
+ formatting.strikethrough = lineThrough !== "none";
272
+ const size = textProps.getAttribute("fo:font-size") || textProps.getAttribute("style:font-size-asian");
273
+ if (size)
274
+ formatting.size = size;
275
+ const color = textProps.getAttribute("fo:color");
276
+ if (color)
277
+ formatting.color = color;
278
+ // Background color (text level) - override cell level if present?
279
+ const bgColor = textProps.getAttribute("fo:background-color");
280
+ if (bgColor && bgColor !== 'transparent')
281
+ formatting.backgroundColor = bgColor;
282
+ // Font family
283
+ const fontName = textProps.getAttribute("style:font-name") || textProps.getAttribute("fo:font-family");
284
+ if (fontName)
285
+ formatting.font = fontName;
286
+ // Subscript/Superscript from text-position (e.g., "sub 58%" or "super 58%")
287
+ const textPosition = textProps.getAttribute("style:text-position");
288
+ if (textPosition) {
289
+ if (textPosition.startsWith("sub"))
290
+ formatting.subscript = true;
291
+ if (textPosition.startsWith("super"))
292
+ formatting.superscript = true;
293
+ }
294
+ }
295
+ if (Object.keys(formatting).length > 0)
296
+ styleMap[name] = formatting;
297
+ }
298
+ };
299
+ if (stylesDom) {
300
+ parseStyles(stylesDom);
301
+ }
302
+ /**
303
+ * Helper to parse a paragraph node (text:p or text:h) and extract its content.
304
+ * Returns the paragraph content without creating a content node.
305
+ *
306
+ * @param node - The paragraph element to parse
307
+ */
308
+ /**
309
+ * Helper to parse inline content (text, spans, links, notes, etc.) recursively.
310
+ *
311
+ * @param node - The element to parse (paragraph, span, or link)
312
+ * @param styleMap - Map of style names to formatting
313
+ * @param config - Parser configuration
314
+ * @param notes - Optional array to collect footnotes/endnotes
315
+ * @param paragraphStyleMap - Map of style names to alignments and props (needed for notes)
316
+ * @param parentFormatting - Formatting inherited from parent (e.g. span inside span)
317
+ * @param linkMetadata - Metadata inherited from parent link
318
+ * @returns Object containing text and children
319
+ */
320
+ const parseInlineContent = (node, styleMap, config, notes, paragraphStyleMap, parentFormatting = {}, linkMetadata, sourceXml = '') => {
321
+ const children = [];
322
+ const anchorIds = [];
323
+ let fullText = '';
324
+ if (!node.childNodes)
325
+ return { text: '', children: [], anchorIds: [] };
326
+ for (let i = 0; i < node.childNodes.length; i++) {
327
+ const child = node.childNodes[i];
328
+ if (child.nodeType === 3) { // Text node
329
+ const text = child.textContent || '';
330
+ if (text) {
331
+ fullText += text;
332
+ children.push({
333
+ type: 'text',
334
+ text: text,
335
+ formatting: parentFormatting,
336
+ metadata: linkMetadata ? { ...linkMetadata } : undefined
337
+ });
338
+ }
339
+ }
340
+ else if ((0, xmlUtils_js_1.isElement)(child)) {
341
+ const element = child;
342
+ const tagName = element.tagName;
343
+ if (tagName === 'text:bookmark' || tagName === 'text:bookmark-start') {
344
+ const name = element.getAttribute('text:name');
345
+ if (name)
346
+ anchorIds.push(name);
347
+ }
348
+ else if (tagName === 'text:s') {
349
+ // Space
350
+ const count = parseInt(element.getAttribute('text:c') || '1');
351
+ const spaces = ' '.repeat(count);
352
+ fullText += spaces;
353
+ children.push({
354
+ type: 'text',
355
+ text: spaces,
356
+ formatting: parentFormatting,
357
+ metadata: linkMetadata ? { ...linkMetadata } : undefined
358
+ });
359
+ }
360
+ else if (tagName === 'text:tab') {
361
+ // Tab
362
+ fullText += '\t';
363
+ children.push({
364
+ type: 'text',
365
+ text: '\t',
366
+ formatting: parentFormatting,
367
+ metadata: linkMetadata ? { ...linkMetadata } : undefined
368
+ });
369
+ }
370
+ else if (tagName === 'text:soft-page-break') {
371
+ // The page boundary the editor recorded at its last save. DOCX's equivalent
372
+ // is `w:lastRenderedPageBreak`, so it maps onto the same break type rather
373
+ // than onto 'page', which is reserved for a break the author asked for.
374
+ if (config.includeBreakNodes) {
375
+ children.push({ type: 'break', metadata: { breakType: 'lastRenderedPage' } });
376
+ }
377
+ }
378
+ else if (tagName === 'text:line-break') {
379
+ // Line break
380
+ fullText += '\n';
381
+ children.push({
382
+ type: 'text',
383
+ text: '\n',
384
+ formatting: parentFormatting,
385
+ metadata: { ...(linkMetadata || {}), isLineBreak: true }
386
+ });
387
+ }
388
+ else if (tagName === 'text:span') {
389
+ // Formatted text span
390
+ const styleName = element.getAttribute("text:style-name");
391
+ const formatting = styleName ? mergeFormatting(parentFormatting, styleMap[styleName]) : parentFormatting;
392
+ const spanContent = parseInlineContent(element, styleMap, config, notes, paragraphStyleMap, formatting, linkMetadata, sourceXml);
393
+ fullText += spanContent.text;
394
+ children.push(...spanContent.children);
395
+ anchorIds.push(...spanContent.anchorIds);
396
+ }
397
+ else if (tagName === 'text:a') {
398
+ // Hyperlink
399
+ let href = element.getAttribute('xlink:href') || '';
400
+ const isInternal = href.startsWith('#');
401
+ const linkType = isInternal ? 'internal' : 'external';
402
+ if (isInternal) {
403
+ // ODT internal links can be encoded and might have suffixes like |outline
404
+ try {
405
+ href = decodeURIComponent(href).split('|')[0];
406
+ }
407
+ catch (e) {
408
+ href = href.split('|')[0];
409
+ }
410
+ // Normalize internal link: if it contains #, keep only from # onwards
411
+ if (href.includes('#')) {
412
+ href = '#' + href.split('#').pop();
413
+ }
414
+ }
415
+ let newLinkMetadata;
416
+ if (!isInternal || !config.ignoreInternalLinks) {
417
+ newLinkMetadata = { link: href, linkType: linkType };
418
+ }
419
+ const linkContent = parseInlineContent(element, styleMap, config, notes, paragraphStyleMap, parentFormatting, newLinkMetadata, sourceXml);
420
+ fullText += linkContent.text;
421
+ children.push(...linkContent.children);
422
+ anchorIds.push(...linkContent.anchorIds);
423
+ }
424
+ else if (tagName === 'text:note' && !config.ignoreNotes) {
425
+ // Footnote or endnote
426
+ const noteClass = (element.getAttribute('text:note-class') || 'footnote');
427
+ const noteId = element.getAttribute('text:id') || element.getAttribute('xml:id') || undefined;
428
+ const noteBody = (0, xmlUtils_js_1.getFirstElementByTagName)(element, "text:note-body");
429
+ if (noteBody) {
430
+ // Extract note content recursively
431
+ const notePs = (0, xmlUtils_js_1.getElementsByTagName)(noteBody, "text:p");
432
+ const noteChildren = [];
433
+ let noteText = '';
434
+ for (const np of notePs) {
435
+ const npContent = parseParagraphContent(np, paragraphStyleMap, styleMap, config, sourceXml);
436
+ noteText += (noteText ? ' ' : '') + npContent.text;
437
+ const npNode = {
438
+ type: 'paragraph',
439
+ text: npContent.text,
440
+ children: npContent.children,
441
+ metadata: {
442
+ ...(npContent.alignment ? { alignment: npContent.alignment } : {}),
443
+ ...(npContent.anchorIds?.length ? { anchorIds: npContent.anchorIds } : {})
444
+ }
445
+ };
446
+ noteChildren.push(npNode);
447
+ }
448
+ const noteNode = {
449
+ type: 'note',
450
+ text: noteText,
451
+ children: noteChildren,
452
+ metadata: {
453
+ noteType: noteClass,
454
+ noteId: noteId
455
+ }
456
+ };
457
+ if (children.length > 0 && children[children.length - 1].type === 'text') {
458
+ const precedingNode = children[children.length - 1];
459
+ if (!precedingNode.notes) {
460
+ precedingNode.notes = [];
461
+ }
462
+ precedingNode.notes.push(noteNode);
463
+ }
464
+ else {
465
+ const emptyTextNode = { type: 'text', text: '' };
466
+ emptyTextNode.notes = [noteNode];
467
+ children.push(emptyTextNode);
468
+ }
469
+ }
470
+ }
471
+ else if (tagName === 'draw:frame') {
472
+ const frame = element;
473
+ const drawTextBox = (0, xmlUtils_js_1.getFirstElementByTagName)(frame, "draw:text-box");
474
+ const drawObject = (0, xmlUtils_js_1.getFirstElementByTagName)(frame, "draw:object");
475
+ if (drawTextBox) {
476
+ const textBoxChildren = [];
477
+ traverse(drawTextBox, textBoxChildren, false, sourceXml);
478
+ children.push(...textBoxChildren);
479
+ const textBoxText = textBoxChildren.map(c => c.text || '').join('\n');
480
+ fullText += textBoxText;
481
+ }
482
+ else if (drawObject) {
483
+ const href = drawObject.getAttribute("xlink:href");
484
+ let isFormula = false;
485
+ let formulaText = '';
486
+ let attachmentName = '';
487
+ if (href) {
488
+ attachmentName = cleanAttachmentName(href);
489
+ const objectPath = `${attachmentName}/content.xml`;
490
+ const objectFile = files.find(f => f.path === objectPath || f.path.endsWith(objectPath));
491
+ if (objectFile) {
492
+ const objXml = (0, xmlUtils_js_1.parseXmlString)(objectFile.content.toString());
493
+ const mathNode = (0, xmlUtils_js_1.getFirstElementByTagName)(objXml, "math");
494
+ if (mathNode) {
495
+ isFormula = true;
496
+ formulaText = (0, mathUtils_js_1.mathmlToLatex)(mathNode).trim();
497
+ }
498
+ }
499
+ }
500
+ if (isFormula) {
501
+ fullText += formulaText;
502
+ // A `code` node carrying `math`, not a plain `text` node: the formula
503
+ // is LaTeX, and marking it as such is what lets generators render it
504
+ // as maths rather than emit it as prose that happens to contain
505
+ // backslashes. Same node shape DOCX, PPTX, HTML and Markdown produce.
506
+ const formulaNode = {
507
+ type: 'code',
508
+ text: formulaText,
509
+ metadata: { math: 'inline', ...(linkMetadata ?? {}) }
510
+ };
511
+ if (config.includeRawContent) {
512
+ formulaNode.rawContent = (0, xmlUtils_js_1.getRawContent)(frame, sourceXml, config);
513
+ }
514
+ children.push(formulaNode);
515
+ }
516
+ else {
517
+ // Standard inline image extraction fallback if object is not a formula
518
+ let altText = '';
519
+ const svgTitle = (0, xmlUtils_js_1.getFirstElementByTagName)(frame, "svg:title");
520
+ const svgDesc = (0, xmlUtils_js_1.getFirstElementByTagName)(frame, "svg:desc");
521
+ if (svgTitle && svgTitle.textContent) {
522
+ altText = svgTitle.textContent;
523
+ }
524
+ else if (svgDesc && svgDesc.textContent) {
525
+ altText = svgDesc.textContent;
526
+ }
527
+ let imageHref = '';
528
+ const drawImages = (0, xmlUtils_js_1.getElementsByTagName)(frame, "draw:image");
529
+ if (drawImages.length > 0) {
530
+ imageHref = drawImages[0].getAttribute("xlink:href") || '';
531
+ if (imageHref) {
532
+ imageHref = cleanAttachmentName(imageHref);
533
+ }
534
+ }
535
+ const imageNode = {
536
+ type: 'image',
537
+ text: '',
538
+ children: [],
539
+ metadata: {
540
+ attachmentName: imageHref || attachmentName,
541
+ ...(altText ? { altText } : {})
542
+ }
543
+ };
544
+ if (config.includeRawContent) {
545
+ imageNode.rawContent = (0, xmlUtils_js_1.getRawContent)(frame, sourceXml, config);
546
+ }
547
+ children.push(imageNode);
548
+ }
549
+ }
550
+ else {
551
+ // Standard inline image extraction fallback
552
+ let altText = '';
553
+ const svgTitle = (0, xmlUtils_js_1.getFirstElementByTagName)(frame, "svg:title");
554
+ const svgDesc = (0, xmlUtils_js_1.getFirstElementByTagName)(frame, "svg:desc");
555
+ if (svgTitle && svgTitle.textContent) {
556
+ altText = svgTitle.textContent;
557
+ }
558
+ else if (svgDesc && svgDesc.textContent) {
559
+ altText = svgDesc.textContent;
560
+ }
561
+ let imageHref = '';
562
+ const drawImages = (0, xmlUtils_js_1.getElementsByTagName)(frame, "draw:image");
563
+ if (drawImages.length > 0) {
564
+ imageHref = drawImages[0].getAttribute("xlink:href") || '';
565
+ if (imageHref) {
566
+ imageHref = cleanAttachmentName(imageHref);
567
+ }
568
+ }
569
+ const imageNode = {
570
+ type: 'image',
571
+ text: '',
572
+ children: [],
573
+ metadata: {
574
+ attachmentName: imageHref,
575
+ ...(altText ? { altText } : {})
576
+ }
577
+ };
578
+ if (config.includeRawContent) {
579
+ imageNode.rawContent = (0, xmlUtils_js_1.getRawContent)(frame, sourceXml, config);
580
+ }
581
+ children.push(imageNode);
582
+ }
583
+ }
584
+ }
585
+ }
586
+ return { text: fullText, children, anchorIds };
587
+ };
588
+ /**
589
+ * Helper to parse a paragraph node (text:p or text:h) and extract its content.
590
+ * Returns the paragraph content without creating a content node.
591
+ *
592
+ * @param node - The paragraph element to parse
593
+ * @param paraStyleMap - Map of style names to alignments/props
594
+ * @param styleMap - Map of style names to formatting
595
+ * @param config - Parser configuration
596
+ * @returns Object containing text, children, alignment, and style info
597
+ */
598
+ const parseParagraphContent = (node, paraStyleMap, styleMap, config, sourceXml) => {
599
+ // Get paragraph style for alignment and drop caps
600
+ const paraStyle = node.getAttribute("text:style-name");
601
+ const styleInfo = paraStyle ? paraStyleMap[paraStyle] : undefined;
602
+ const alignment = styleInfo?.alignment;
603
+ const dropCap = styleInfo?.dropCap;
604
+ const formatting = mergeFormatting({}, paraStyle ? styleMap[paraStyle] : undefined);
605
+ // Parse content recursively using the new helper
606
+ const content = parseInlineContent(node, styleMap, config, notes, paraStyleMap, formatting, undefined, sourceXml);
607
+ // Add style name to metadata of children if they don't have one
608
+ if (paraStyle) {
609
+ content.children.forEach(child => {
610
+ if (child.type === 'text') {
611
+ if (!child.metadata)
612
+ child.metadata = {};
613
+ // Only add style if it's a text node and doesn't have one?
614
+ // Or just add it.
615
+ // Cast to any to avoid union type issues for now, or check type
616
+ const meta = child.metadata;
617
+ if (!meta.style)
618
+ meta.style = paraStyle;
619
+ }
620
+ });
621
+ }
622
+ // Fallback: if no children were created but there's text content
623
+ if (content.children.length === 0 && node.textContent) {
624
+ const fullText = node.textContent;
625
+ if (fullText.trim()) {
626
+ content.text = fullText;
627
+ content.children.push({
628
+ type: 'text',
629
+ text: fullText
630
+ });
631
+ }
632
+ }
633
+ // Handle Drop Cap: Apply large font to first letter if configured
634
+ if (dropCap && content.children.length > 0) {
635
+ const firstChild = content.children[0];
636
+ if (firstChild.type === 'text' && firstChild.text) {
637
+ if (firstChild.text.length === 1) {
638
+ // Already a single letter, just apply formatting
639
+ firstChild.formatting = { ...firstChild.formatting, size: '58.5pt' };
640
+ }
641
+ else {
642
+ // Split text node
643
+ const firstChar = firstChild.text[0];
644
+ const restText = firstChild.text.substring(1);
645
+ const dropCapNode = {
646
+ type: 'text',
647
+ text: firstChar,
648
+ formatting: { ...firstChild.formatting, size: '58.5pt' },
649
+ metadata: firstChild.metadata
650
+ };
651
+ // Update original node
652
+ firstChild.text = restText;
653
+ // Insert drop cap node
654
+ content.children.unshift(dropCapNode);
655
+ }
656
+ }
657
+ }
658
+ return { text: content.text, children: content.children, alignment, style: paraStyle || undefined, anchorIds: content.anchorIds };
659
+ };
660
+ /**
661
+ * Splits paragraph content into multiple segments based on line breaks.
662
+ * Used to handle soft line breaks within list items.
663
+ *
664
+ * @param pContent - The content of a single paragraph
665
+ * @returns Array of content segments
666
+ */
667
+ const splitParagraphByBreaks = (pContent) => {
668
+ const segments = [];
669
+ let currentText = "";
670
+ let currentChildren = [];
671
+ for (const child of pContent.children) {
672
+ if (child.type === "text" && child.metadata?.isLineBreak) {
673
+ segments.push({ text: currentText, children: currentChildren });
674
+ currentText = "";
675
+ currentChildren = [];
676
+ }
677
+ else {
678
+ currentText += child.text || "";
679
+ currentChildren.push(child);
680
+ }
681
+ }
682
+ segments.push({ text: currentText, children: currentChildren });
683
+ return segments;
684
+ };
685
+ /**
686
+ * Helper to parse a table node and extract its structure.
687
+ * Properly creates table → row → cell hierarchy with metadata.
688
+ *
689
+ * @param tableNode - The table:table element
690
+ * @param paraStyleMap - Map of style names to alignments
691
+ * @param styleMap - Map of style names to formatting
692
+ * @param config - Parser configuration
693
+ * @returns Table content node with proper structure
694
+ */
695
+ const parseTable = (tableNode, paraStyleMap, styleMap, config, sourceXml, cellBudget) => {
696
+ const rows = [];
697
+ // Use getDirectChildren to avoid nested table rows
698
+ const tableRows = (0, xmlUtils_js_1.getDirectChildren)(tableNode, "table:table-row");
699
+ let rowIndex = 0;
700
+ for (const row of tableRows) {
701
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
702
+ const cells = [];
703
+ // Use getDirectChildren to avoid nested table cells
704
+ const tableCells = (0, xmlUtils_js_1.getDirectChildren)(row, "table:table-cell");
705
+ const rowsRepeated = toRepeatCount(row.getAttribute("table:number-rows-repeated"));
706
+ let colIndex = 0;
707
+ for (const cell of tableCells) {
708
+ const cellChildren = [];
709
+ let cellTextRef = { value: '' };
710
+ const colsRepeated = toRepeatCount(cell.getAttribute("table:number-columns-repeated"));
711
+ const colSpan = parseInt(cell.getAttribute("table:number-columns-spanned") || "1");
712
+ const rowSpan = parseInt(cell.getAttribute("table:number-rows-spanned") || "1");
713
+ // Helper to recursively process cell children (handles frames, text-boxes, etc. in ODP)
714
+ const processChildren = (node) => {
715
+ if (!node.childNodes)
716
+ return;
717
+ for (let i = 0; i < node.childNodes.length; i++) {
718
+ const child = node.childNodes[i];
719
+ if ((0, xmlUtils_js_1.isElement)(child)) { // Element
720
+ const element = child;
721
+ if (element.tagName === "text:p" || element.tagName === "text:h") {
722
+ const pContent = parseParagraphContent(element, paraStyleMap, styleMap, config, sourceXml);
723
+ let pNode;
724
+ if (element.tagName === "text:h") {
725
+ pNode = {
726
+ type: 'heading',
727
+ text: pContent.text,
728
+ children: pContent.children,
729
+ metadata: {
730
+ level: parseInt(element.getAttribute("text:outline-level") || "1"),
731
+ ...(pContent.alignment ? { alignment: pContent.alignment } : {}),
732
+ ...(pContent.style ? { style: pContent.style } : {})
733
+ }
734
+ };
735
+ }
736
+ else {
737
+ pNode = {
738
+ type: 'paragraph',
739
+ text: pContent.text,
740
+ children: pContent.children,
741
+ metadata: {
742
+ ...(pContent.alignment ? { alignment: pContent.alignment } : {}),
743
+ ...(pContent.style ? { style: pContent.style } : {})
744
+ }
745
+ };
746
+ }
747
+ // Clean up metadata if empty
748
+ if (pNode.type === 'paragraph' && Object.keys(pNode.metadata || {}).length === 0) {
749
+ delete pNode.metadata;
750
+ }
751
+ if (config.includeRawContent) {
752
+ pNode.rawContent = (0, xmlUtils_js_1.getRawContent)(element, sourceXml, config);
753
+ }
754
+ cellChildren.push(pNode);
755
+ cellTextRef.value += pContent.text;
756
+ // Add newline if there are multiple paragraphs/headings
757
+ if (cellTextRef.value && !cellTextRef.value.endsWith('\n')) {
758
+ cellTextRef.value += '\n';
759
+ }
760
+ }
761
+ else if (element.tagName === "table:table") {
762
+ // Recursive call for nested table
763
+ const nestedTableNode = parseTable(element, paraStyleMap, styleMap, config, sourceXml, cellBudget);
764
+ cellChildren.push(nestedTableNode);
765
+ }
766
+ else if (element.tagName === "draw:frame" || element.tagName === "draw:text-box") {
767
+ // Recursively process container content (common in ODP)
768
+ processChildren(element);
769
+ }
770
+ }
771
+ }
772
+ };
773
+ processChildren(cell);
774
+ let cellText = cellTextRef.value;
775
+ // Trim trailing newline from cellText
776
+ if (cellText.endsWith('\n')) {
777
+ cellText = cellText.slice(0, -1);
778
+ }
779
+ // Add cell(s) for repeated columns
780
+ // Bounded by the document's cell budget, not by the attribute: the repeat count
781
+ // is attacker-influenced and this path materializes a node per iteration.
782
+ const allowedCols = cellBudget.take(colsRepeated);
783
+ for (let k = 0; k < allowedCols; k++) {
784
+ // Repeat expansion is the one place a small document produces a long loop,
785
+ // so it is also the one place a caller most needs to be able to cancel.
786
+ if ((k & 1023) === 0)
787
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
788
+ // Apply cell background color if defined in styleMap
789
+ const cellStyleName = cell.getAttribute("table:style-name");
790
+ const cellBgColor = cellStyleName && styleMap[cellStyleName]?.backgroundColor;
791
+ const cellNode = {
792
+ type: 'cell',
793
+ text: cellText,
794
+ children: cellChildren.length > 0 ? (k === 0 ? cellChildren : JSON.parse(JSON.stringify(cellChildren))) : [],
795
+ metadata: {
796
+ row: rowIndex,
797
+ col: colIndex,
798
+ ...(cellBgColor ? { backgroundColor: cellBgColor } : {})
799
+ }
800
+ };
801
+ const cellMetadata = cellNode.metadata;
802
+ if (colSpan > 1)
803
+ cellMetadata.colSpan = colSpan;
804
+ if (rowSpan > 1)
805
+ cellMetadata.rowSpan = rowSpan;
806
+ if (config.includeRawContent) {
807
+ cellNode.rawContent = (0, xmlUtils_js_1.getRawContent)(cell, sourceXml, config);
808
+ }
809
+ cells.push(cellNode);
810
+ colIndex++;
811
+ }
812
+ }
813
+ // Add row(s) for repeated rows. Every repetition past the first deep-copies the
814
+ // whole cell array, so rows x cols is what actually exhausts memory; charge those
815
+ // copies against the same budget.
816
+ const allowedRows = cells.length === 0
817
+ ? (rowsRepeated > 0 ? 1 + cellBudget.take(rowsRepeated - 1) : 0)
818
+ : Math.min(rowsRepeated, 1 + Math.floor(cellBudget.take(Math.max(0, (rowsRepeated - 1) * cells.length)) / cells.length));
819
+ for (let k = 0; k < allowedRows; k++) {
820
+ if ((k & 255) === 0)
821
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
822
+ const rowNode = {
823
+ type: 'row',
824
+ children: k === 0 ? cells : JSON.parse(JSON.stringify(cells))
825
+ };
826
+ // Fix row indices for repeated rows
827
+ if (k > 0) {
828
+ rowNode.children?.forEach(c => {
829
+ if (c.metadata && 'row' in c.metadata) {
830
+ c.metadata.row = rowIndex;
831
+ }
832
+ });
833
+ }
834
+ if (config.includeRawContent) {
835
+ rowNode.rawContent = (0, xmlUtils_js_1.getRawContent)(row, sourceXml, config);
836
+ }
837
+ rows.push(rowNode);
838
+ rowIndex++;
839
+ }
840
+ }
841
+ return {
842
+ type: 'table',
843
+ children: rows
844
+ };
845
+ };
846
+ const parseContentXml = (xmlString) => {
847
+ const xml = (0, xmlUtils_js_1.parseXmlString)(xmlString, { locator: config.includeRawContent });
848
+ const body = (0, xmlUtils_js_1.getFirstElementByTagName)(xml, "office:body");
849
+ if (!body)
850
+ return;
851
+ // One budget for the entire document. It has to span every table - spreadsheet sheets,
852
+ // ODT/ODP body tables, and nested tables alike - or a file sidesteps the cap simply by
853
+ // splitting a huge repeat expansion across many small tables. `traverse` and the
854
+ // spreadsheet branch below both close over this; `parseTable` receives it explicitly.
855
+ const cellBudget = createCellBudget(config);
856
+ // Automatic styles are local to content.xml, but their definitions have exactly the
857
+ // shape styles.xml uses, so they go through the same reader rather than a second copy of
858
+ // it - the copy is how `fo:break-before` came to be read in neither place.
859
+ const automaticStyles = (0, xmlUtils_js_1.getFirstElementByTagName)(xml, "office:automatic-styles");
860
+ if (automaticStyles) {
861
+ parseStyles(automaticStyles);
862
+ }
863
+ // Start traversal
864
+ const officeBody = (0, xmlUtils_js_1.getFirstElementByTagName)(xml, "office:body");
865
+ if (officeBody) {
866
+ const bodyContent = (0, xmlUtils_js_1.getDirectChildren)(officeBody, "office:text")[0] ||
867
+ (0, xmlUtils_js_1.getDirectChildren)(officeBody, "office:presentation")[0] ||
868
+ (0, xmlUtils_js_1.getDirectChildren)(officeBody, "office:spreadsheet")[0];
869
+ if (bodyContent) {
870
+ const bodyChildren = (0, xmlUtils_js_1.getDirectChildren)(bodyContent, "*");
871
+ const isSpreadsheet = bodyContent.tagName === "office:spreadsheet";
872
+ for (const child of bodyChildren) {
873
+ traverse(child, content, false, xmlString, isSpreadsheet);
874
+ }
875
+ }
876
+ }
877
+ /**
878
+ * Recursively traverses a node and its children to extract content.
879
+ * Properly handles paragraphs, headings, tables, lists, and frames.
880
+ *
881
+ * @param node - The element to traverse
882
+ * @param targetArray - The array to push extracted content nodes to
883
+ * @param forceHeading - If true, treats all paragraphs as headings (used for slide titles)
884
+ * @param sourceXml - The source XML string for raw content extraction
885
+ * @param asSheet - If true, treats tables as sheets (for ODS)
886
+ */
887
+ /**
888
+ * Emits the break a paragraph style asks for, on the given side of that paragraph.
889
+ *
890
+ * ODF has no inline break element for these - `fo:break-before="page"` sits on the style,
891
+ * so the break is a property of the paragraph rather than a run inside it. That makes it a
892
+ * sibling emitted around the paragraph node, not a child of it, which is the one structural
893
+ * difference from how DOCX's `<w:br w:type="page"/>` lands.
894
+ */
895
+ const pushStyleBreak = (styleName, targetArray, edge) => {
896
+ if (!config.includeBreakNodes || !styleName)
897
+ return;
898
+ const info = paragraphStyleMap[styleName];
899
+ const breakType = edge === 'before' ? info?.breakBefore : info?.breakAfter;
900
+ if (!breakType)
901
+ return;
902
+ targetArray.push({ type: 'break', metadata: { breakType } });
903
+ };
904
+ traverse = (node, targetArray, forceHeading = false, sourceXml = '', asSheet = false) => {
905
+ if (node.tagName === "text:p") {
906
+ pushStyleBreak(node.getAttribute("text:style-name"), targetArray, 'before');
907
+ const pContent = parseParagraphContent(node, paragraphStyleMap, styleMap, config, sourceXml);
908
+ const type = (forceHeading || (node.getAttribute("text:style-name") || '').toLowerCase().includes('title')) ? 'heading' : 'paragraph';
909
+ const metadata = {
910
+ ...(pContent.alignment ? { alignment: pContent.alignment } : {}),
911
+ ...(pContent.style ? { style: pContent.style } : {}),
912
+ ...(pContent.anchorIds?.length ? { anchorIds: pContent.anchorIds } : {})
913
+ };
914
+ const nodeId = node.getAttribute("xml:id") || node.getAttribute("text:id");
915
+ if (nodeId) {
916
+ if (!metadata.anchorIds)
917
+ metadata.anchorIds = [];
918
+ metadata.anchorIds.push(nodeId);
919
+ }
920
+ const pNode = {
921
+ type,
922
+ text: pContent.text,
923
+ children: pContent.children,
924
+ metadata
925
+ };
926
+ if (type === 'heading' && pNode.metadata) {
927
+ pNode.metadata.level = pNode.metadata.level || 1;
928
+ }
929
+ // Clean up metadata if empty
930
+ if (Object.keys(pNode.metadata || {}).length === 0)
931
+ delete pNode.metadata;
932
+ if (config.includeRawContent) {
933
+ pNode.rawContent = (0, xmlUtils_js_1.getRawContent)(node, sourceXml, config);
934
+ }
935
+ targetArray.push(pNode);
936
+ pushStyleBreak(node.getAttribute("text:style-name"), targetArray, 'after');
937
+ lastWasList = false;
938
+ }
939
+ else if (node.tagName === "text:h") {
940
+ pushStyleBreak(node.getAttribute("text:style-name"), targetArray, 'before');
941
+ const level = parseInt(node.getAttribute("text:outline-level") || "1");
942
+ const hContent = parseParagraphContent(node, paragraphStyleMap, styleMap, config, sourceXml);
943
+ const metadata = {
944
+ level,
945
+ ...(hContent.alignment ? { alignment: hContent.alignment } : {}),
946
+ ...(hContent.style ? { style: hContent.style } : {}),
947
+ ...(hContent.anchorIds?.length ? { anchorIds: hContent.anchorIds } : {})
948
+ };
949
+ const nodeId = node.getAttribute("xml:id") || node.getAttribute("text:id");
950
+ if (nodeId) {
951
+ if (!metadata.anchorIds)
952
+ metadata.anchorIds = [];
953
+ metadata.anchorIds.push(nodeId);
954
+ }
955
+ const hNode = {
956
+ type: 'heading',
957
+ text: hContent.text,
958
+ children: hContent.children,
959
+ metadata
960
+ };
961
+ if (config.includeRawContent) {
962
+ hNode.rawContent = (0, xmlUtils_js_1.getRawContent)(node, sourceXml, config);
963
+ }
964
+ targetArray.push(hNode);
965
+ pushStyleBreak(node.getAttribute("text:style-name"), targetArray, 'after');
966
+ lastWasList = false;
967
+ }
968
+ else if (node.tagName === "table:table") {
969
+ // Parse table with proper structure
970
+ const tableNode = parseTable(node, paragraphStyleMap, styleMap, config, sourceXml, cellBudget);
971
+ if (asSheet) {
972
+ tableNode.type = 'sheet';
973
+ const sheetName = node.getAttribute("table:name");
974
+ if (sheetName) {
975
+ tableNode.metadata = { ...tableNode.metadata, sheetName };
976
+ }
977
+ }
978
+ const tableId = node.getAttribute("xml:id") || node.getAttribute("table:name");
979
+ if (tableId) {
980
+ if (!tableNode.metadata)
981
+ tableNode.metadata = {};
982
+ tableNode.metadata.anchorIds = tableNode.metadata.anchorIds || [];
983
+ tableNode.metadata.anchorIds.push(tableId);
984
+ }
985
+ if (config.includeRawContent) {
986
+ tableNode.rawContent = (0, xmlUtils_js_1.getRawContent)(node, sourceXml, config);
987
+ }
988
+ targetArray.push(tableNode);
989
+ lastWasList = false;
990
+ }
991
+ else if (node.tagName === "text:list") {
992
+ // Parse list structure with proper listId tracking
993
+ const listItems = (0, xmlUtils_js_1.getDirectChildren)(node, "text:list-item");
994
+ // Determine list type by checking the list style definition
995
+ let listType = 'unordered';
996
+ let isVisible = false;
997
+ const listStyleName = node.getAttribute("text:style-name") || node.getAttribute("xml:id");
998
+ let styleNameToCheck = listStyleName;
999
+ // If no style name, check parent list for inherited style
1000
+ if (!styleNameToCheck) {
1001
+ let parentNode = node.parentNode;
1002
+ while (parentNode && !styleNameToCheck) {
1003
+ if (parentNode.nodeName === 'text:list') {
1004
+ styleNameToCheck = parentNode.getAttribute("text:style-name");
1005
+ if (styleNameToCheck)
1006
+ break;
1007
+ }
1008
+ parentNode = parentNode.parentNode;
1009
+ }
1010
+ }
1011
+ // Try to find list style in automatic styles or styles.xml to determine type and visibility
1012
+ if (styleNameToCheck) {
1013
+ if (automaticStyles) {
1014
+ const listStyles = (0, xmlUtils_js_1.getElementsByTagName)(automaticStyles, "text:list-style");
1015
+ for (const listStyle of listStyles) {
1016
+ if (listStyle.getAttribute("style:name") === styleNameToCheck) {
1017
+ // Check if it has bullet or number level styles
1018
+ const bulletLevels = (0, xmlUtils_js_1.getElementsByTagName)(listStyle, "text:list-level-style-bullet");
1019
+ const numberLevels = (0, xmlUtils_js_1.getElementsByTagName)(listStyle, "text:list-level-style-number");
1020
+ const imageLevels = (0, xmlUtils_js_1.getElementsByTagName)(listStyle, "text:list-level-style-image");
1021
+ if (numberLevels.length > 0) {
1022
+ listType = 'ordered';
1023
+ isVisible = numberLevels.some(l => !!l.getAttribute("style:num-format"));
1024
+ }
1025
+ else if (bulletLevels.length > 0) {
1026
+ listType = 'unordered';
1027
+ isVisible = bulletLevels.some(l => !!l.getAttribute("text:bullet-char"));
1028
+ }
1029
+ else if (imageLevels.length > 0) {
1030
+ listType = 'unordered';
1031
+ isVisible = true;
1032
+ }
1033
+ break;
1034
+ }
1035
+ }
1036
+ }
1037
+ if (!isVisible && stylesDom) {
1038
+ const officeStyles = (0, xmlUtils_js_1.getFirstElementByTagName)(stylesDom, "office:styles");
1039
+ if (officeStyles) {
1040
+ const listStyles = (0, xmlUtils_js_1.getElementsByTagName)(officeStyles, "text:list-style");
1041
+ for (const listStyle of listStyles) {
1042
+ if (listStyle.getAttribute("style:name") === styleNameToCheck) {
1043
+ const bulletLevels = (0, xmlUtils_js_1.getElementsByTagName)(listStyle, "text:list-level-style-bullet");
1044
+ const numberLevels = (0, xmlUtils_js_1.getElementsByTagName)(listStyle, "text:list-level-style-number");
1045
+ if (numberLevels.length > 0) {
1046
+ listType = 'ordered';
1047
+ isVisible = numberLevels.some(l => !!l.getAttribute("style:num-format"));
1048
+ }
1049
+ else if (bulletLevels.length > 0) {
1050
+ listType = 'unordered';
1051
+ isVisible = bulletLevels.some(l => !!l.getAttribute("text:bullet-char"));
1052
+ }
1053
+ break;
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ // If the list is not visible, it's likely a layout list used by Impress.
1060
+ // We should traverse its items and treat their content as regular nodes.
1061
+ if (!isVisible) {
1062
+ lastWasList = false;
1063
+ for (let i = 0; i < listItems.length; i++) {
1064
+ const item = listItems[i];
1065
+ if (item.childNodes) {
1066
+ for (let j = 0; j < item.childNodes.length; j++) {
1067
+ const child = item.childNodes[j];
1068
+ if ((0, xmlUtils_js_1.isElement)(child)) { // Element
1069
+ traverse(child, targetArray, forceHeading, sourceXml);
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ return;
1075
+ }
1076
+ // List Continuity Logic:
1077
+ // If this list follows another list of the same type and style, or we are in ODP and it's sequential,
1078
+ // we should reuse the previous listId to maintain numbering.
1079
+ const isODP = fileType === 'odp';
1080
+ const sameStyle = styleNameToCheck && styleNameToCheck === lastListStyle;
1081
+ const sameType = listType === lastListType;
1082
+ let listId;
1083
+ if (lastWasList && (sameStyle || (isODP && sameType))) {
1084
+ listId = currentListId;
1085
+ }
1086
+ else {
1087
+ // New list
1088
+ listId = styleNameToCheck || `list-${++listIdCounter}`;
1089
+ currentListId = listId;
1090
+ lastListType = listType;
1091
+ lastListStyle = styleNameToCheck;
1092
+ }
1093
+ lastWasList = true;
1094
+ // Calculate indentation level by counting parent text:list elements
1095
+ let indentation = 0;
1096
+ let parent = node.parentNode;
1097
+ while (parent) {
1098
+ if (parent.nodeName === 'text:list') {
1099
+ indentation++;
1100
+ }
1101
+ parent = parent.parentNode;
1102
+ }
1103
+ // Track list counters for this listId (similar to WordParser)
1104
+ if (!listCounters[listId]) {
1105
+ listCounters[listId] = {};
1106
+ }
1107
+ const indentKey = indentation.toString();
1108
+ if (listCounters[listId][indentKey] === undefined) {
1109
+ listCounters[listId][indentKey] = -1; // Will increment to 0 on first item
1110
+ }
1111
+ // Process each list item
1112
+ for (let i = 0; i < listItems.length; i++) {
1113
+ const item = listItems[i];
1114
+ let hasIndexedThisItem = false;
1115
+ // Iterate over direct children of list item (paragraphs, headings, nested lists)
1116
+ if (item.childNodes) {
1117
+ for (let j = 0; j < item.childNodes.length; j++) {
1118
+ const child = item.childNodes[j];
1119
+ if ((0, xmlUtils_js_1.isElement)(child)) { // Element
1120
+ const element = child;
1121
+ if (element.tagName === "text:p" || element.tagName === "text:h") {
1122
+ if (!hasIndexedThisItem) {
1123
+ listCounters[listId][indentKey]++;
1124
+ hasIndexedThisItem = true;
1125
+ for (let k = indentation + 1; k < 10; k++) {
1126
+ if (listCounters[listId][k.toString()] !== undefined) {
1127
+ listCounters[listId][k.toString()] = -1;
1128
+ }
1129
+ }
1130
+ }
1131
+ const itemIndex = listCounters[listId][indentKey];
1132
+ const pContent = parseParagraphContent(element, paragraphStyleMap, styleMap, config, sourceXml);
1133
+ const segments = splitParagraphByBreaks(pContent);
1134
+ for (let k = 0; k < segments.length; k++) {
1135
+ const segment = segments[k];
1136
+ if (!segment.text.trim() && segment.children.length === 0)
1137
+ continue;
1138
+ const isFirst = k === 0;
1139
+ const nodeType = isFirst ? 'list' : 'paragraph';
1140
+ const node = {
1141
+ type: nodeType,
1142
+ text: segment.text,
1143
+ children: segment.children,
1144
+ metadata: isFirst ? {
1145
+ listType,
1146
+ indentation,
1147
+ itemIndex,
1148
+ listId,
1149
+ alignment: pContent.alignment || 'left',
1150
+ style: pContent.style
1151
+ } : {
1152
+ alignment: pContent.alignment || 'left',
1153
+ style: pContent.style
1154
+ }
1155
+ };
1156
+ // Special case for headings in lists
1157
+ if (isFirst && element.tagName === "text:h") {
1158
+ const level = parseInt(element.getAttribute("text:outline-level") || "1");
1159
+ node.metadata.level = level;
1160
+ }
1161
+ if (config.includeRawContent)
1162
+ node.rawContent = (0, xmlUtils_js_1.getRawContent)(element, sourceXml, config);
1163
+ targetArray.push(node);
1164
+ }
1165
+ }
1166
+ else if (element.tagName === "text:list") {
1167
+ // Recursive call for nested list
1168
+ traverse(element, targetArray, forceHeading, sourceXml);
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ else if (node.tagName === "draw:frame") {
1176
+ const presClass = node.getAttribute("presentation:class");
1177
+ const isHeading = presClass === "title" || presClass === "sub-title";
1178
+ // In presentations, frames often contain text-boxes, images, tables, or objects
1179
+ const textBox = (0, xmlUtils_js_1.getFirstElementByTagName)(node, "draw:text-box");
1180
+ const image = (0, xmlUtils_js_1.getFirstElementByTagName)(node, "draw:image");
1181
+ const table = (0, xmlUtils_js_1.getFirstElementByTagName)(node, "table:table");
1182
+ const object = (0, xmlUtils_js_1.getFirstElementByTagName)(node, "draw:object");
1183
+ if (textBox) {
1184
+ traverse(textBox, targetArray, isHeading || forceHeading, sourceXml);
1185
+ }
1186
+ else if (table) {
1187
+ const tableNode = parseTable(table, paragraphStyleMap, styleMap, config, sourceXml, cellBudget);
1188
+ if (config.includeRawContent)
1189
+ tableNode.rawContent = (0, xmlUtils_js_1.getRawContent)(table, sourceXml, config);
1190
+ targetArray.push(tableNode);
1191
+ }
1192
+ else if (image) {
1193
+ // Extract alt text from svg:title or svg:desc
1194
+ let altText = '';
1195
+ const svgTitle = (0, xmlUtils_js_1.getFirstElementByTagName)(node, "svg:title");
1196
+ const svgDesc = (0, xmlUtils_js_1.getFirstElementByTagName)(node, "svg:desc");
1197
+ if (svgTitle && svgTitle.textContent) {
1198
+ altText = svgTitle.textContent;
1199
+ }
1200
+ else if (svgDesc && svgDesc.textContent) {
1201
+ altText = svgDesc.textContent;
1202
+ }
1203
+ // Extract image href to link to attachment
1204
+ let imageHref = image.getAttribute("xlink:href") || '';
1205
+ if (imageHref) {
1206
+ imageHref = cleanAttachmentName(imageHref);
1207
+ }
1208
+ const metadata = {
1209
+ attachmentName: imageHref,
1210
+ ...(altText ? { altText } : {})
1211
+ };
1212
+ const frameId = node.getAttribute("xml:id") || node.getAttribute("draw:name");
1213
+ if (frameId) {
1214
+ metadata.anchorIds = [frameId];
1215
+ }
1216
+ const imageNode = {
1217
+ type: 'image',
1218
+ text: '',
1219
+ children: [],
1220
+ metadata
1221
+ };
1222
+ if (config.includeRawContent) {
1223
+ imageNode.rawContent = (0, xmlUtils_js_1.getRawContent)(node, sourceXml, config);
1224
+ }
1225
+ targetArray.push(imageNode);
1226
+ }
1227
+ else if (object) {
1228
+ // Handle embedded objects like charts or math formulas
1229
+ const href = object.getAttribute("xlink:href");
1230
+ if (href) {
1231
+ const attachmentName = cleanAttachmentName(href);
1232
+ const objectPath = `${attachmentName}/content.xml`;
1233
+ const objectFile = files.find(f => f.path === objectPath || f.path.endsWith(objectPath));
1234
+ if (objectFile) {
1235
+ const objXml = (0, xmlUtils_js_1.parseXmlString)(objectFile.content.toString());
1236
+ const mathNode = (0, xmlUtils_js_1.getFirstElementByTagName)(objXml, "math");
1237
+ if (mathNode) {
1238
+ // Math formula object at block level - a display equation, so the
1239
+ // inner node is `math: 'block'` where the inline site above emits
1240
+ // `math: 'inline'`.
1241
+ const formulaText = (0, mathUtils_js_1.mathmlToLatex)(mathNode).trim();
1242
+ const formulaNode = {
1243
+ type: 'paragraph',
1244
+ text: formulaText,
1245
+ children: [
1246
+ {
1247
+ type: 'code',
1248
+ text: formulaText,
1249
+ metadata: { math: 'block' }
1250
+ }
1251
+ ]
1252
+ };
1253
+ if (config.includeRawContent) {
1254
+ formulaNode.rawContent = (0, xmlUtils_js_1.getRawContent)(node, sourceXml, config);
1255
+ }
1256
+ targetArray.push(formulaNode);
1257
+ }
1258
+ else {
1259
+ const chartData = (0, chartUtils_js_1.extractChartData)(objectFile.content);
1260
+ const chartNode = {
1261
+ type: 'chart',
1262
+ text: chartData.rawTexts.join(" "),
1263
+ metadata: {
1264
+ attachmentName: attachmentName,
1265
+ chartData
1266
+ }
1267
+ };
1268
+ if (config.includeRawContent)
1269
+ chartNode.rawContent = (0, xmlUtils_js_1.getRawContent)(node, sourceXml, config);
1270
+ targetArray.push(chartNode);
1271
+ }
1272
+ }
1273
+ else {
1274
+ const chartNode = {
1275
+ type: 'chart',
1276
+ text: "",
1277
+ metadata: { attachmentName: attachmentName }
1278
+ };
1279
+ if (config.includeRawContent)
1280
+ chartNode.rawContent = (0, xmlUtils_js_1.getRawContent)(node, sourceXml, config);
1281
+ targetArray.push(chartNode);
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+ else {
1287
+ if (node.childNodes) {
1288
+ for (let i = 0; i < node.childNodes.length; i++) {
1289
+ const child = node.childNodes[i];
1290
+ if ((0, xmlUtils_js_1.isElement)(child)) { // Element
1291
+ traverse(child, targetArray, forceHeading, sourceXml);
1292
+ }
1293
+ }
1294
+ }
1295
+ }
1296
+ };
1297
+ // ODS: Spreadsheet
1298
+ if (fileType === 'ods') {
1299
+ const spreadsheet = (0, xmlUtils_js_1.getFirstElementByTagName)(body, "office:spreadsheet");
1300
+ if (spreadsheet) {
1301
+ const tables = (0, xmlUtils_js_1.getElementsByTagName)(spreadsheet, "table:table");
1302
+ for (let i = 0; i < tables.length; i++) {
1303
+ const table = tables[i];
1304
+ const sheetName = table.getAttribute("table:name") || `Sheet${i + 1}`;
1305
+ const rows = [];
1306
+ const tableRows = (0, xmlUtils_js_1.getElementsByTagName)(table, "table:table-row");
1307
+ let rowIndex = 0;
1308
+ for (let r = 0; r < tableRows.length; r++) {
1309
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
1310
+ const row = tableRows[r];
1311
+ const cells = [];
1312
+ const tableCells = (0, xmlUtils_js_1.getElementsByTagName)(row, "table:table-cell");
1313
+ let colIndex = 0;
1314
+ const rowsRepeated = toRepeatCount(row.getAttribute("table:number-rows-repeated"));
1315
+ for (let c = 0; c < tableCells.length; c++) {
1316
+ const cell = tableCells[c];
1317
+ const colsRepeated = toRepeatCount(cell.getAttribute("table:number-columns-repeated"));
1318
+ // Extract text from cell (paragraphs inside cell)
1319
+ let cellText = "";
1320
+ const children = [];
1321
+ const ps = (0, xmlUtils_js_1.getElementsByTagName)(cell, "text:p");
1322
+ for (let p = 0; p < ps.length; p++) {
1323
+ const para = ps[p];
1324
+ // Parse text:span elements for formatted text
1325
+ const spans = (0, xmlUtils_js_1.getElementsByTagName)(para, "text:span");
1326
+ if (spans.length > 0) {
1327
+ for (const span of spans) {
1328
+ const styleName = span.getAttribute("text:style-name");
1329
+ // Through `mergeFormatting` like every other span site, so
1330
+ // an explicit `false` is dropped rather than written onto
1331
+ // the node - and so the node gets its own object instead of
1332
+ // aliasing the shared style-table entry.
1333
+ const formatting = mergeFormatting({}, styleName ? styleMap[styleName] : undefined);
1334
+ const text = span.textContent || '';
1335
+ cellText += text;
1336
+ const textNode = {
1337
+ type: 'text',
1338
+ text: text,
1339
+ formatting: formatting
1340
+ };
1341
+ children.push(textNode);
1342
+ }
1343
+ }
1344
+ else {
1345
+ // No spans - just direct text content
1346
+ const text = para.textContent || '';
1347
+ cellText += text;
1348
+ if (text.trim()) {
1349
+ const textNode = {
1350
+ type: 'text',
1351
+ text: text,
1352
+ formatting: {}
1353
+ };
1354
+ children.push(textNode);
1355
+ }
1356
+ }
1357
+ if (p < ps.length - 1)
1358
+ cellText += "\n";
1359
+ }
1360
+ // Check for embedded draw:frame (images) in cell
1361
+ const drawFrames = (0, xmlUtils_js_1.getElementsByTagName)(cell, "draw:frame");
1362
+ for (const frame of drawFrames) {
1363
+ // Extract alt text from svg:title or svg:desc
1364
+ let altText = '';
1365
+ const svgTitle = (0, xmlUtils_js_1.getFirstElementByTagName)(frame, "svg:title");
1366
+ const svgDesc = (0, xmlUtils_js_1.getFirstElementByTagName)(frame, "svg:desc");
1367
+ if (svgTitle && svgTitle.textContent) {
1368
+ altText = svgTitle.textContent;
1369
+ }
1370
+ else if (svgDesc && svgDesc.textContent) {
1371
+ altText = svgDesc.textContent;
1372
+ }
1373
+ // Extract image href
1374
+ let imageHref = '';
1375
+ const drawImages = (0, xmlUtils_js_1.getElementsByTagName)(frame, "draw:image");
1376
+ if (drawImages.length > 0) {
1377
+ const rawHref = drawImages[0].getAttribute("xlink:href");
1378
+ if (rawHref) {
1379
+ imageHref = cleanAttachmentName(rawHref);
1380
+ }
1381
+ }
1382
+ // Extract chart or math object href
1383
+ let chartHref = '';
1384
+ let isFormula = false;
1385
+ let formulaText = '';
1386
+ const drawObjects = (0, xmlUtils_js_1.getElementsByTagName)(frame, "draw:object");
1387
+ if (drawObjects.length > 0) {
1388
+ const href = drawObjects[0].getAttribute("xlink:href");
1389
+ if (href) {
1390
+ chartHref = cleanAttachmentName(href);
1391
+ const objectPath = `${chartHref}/content.xml`;
1392
+ const objectFile = files.find(f => f.path === objectPath || f.path.endsWith(objectPath));
1393
+ if (objectFile) {
1394
+ const objXml = (0, xmlUtils_js_1.parseXmlString)(objectFile.content.toString());
1395
+ const mathNode = (0, xmlUtils_js_1.getFirstElementByTagName)(objXml, "math");
1396
+ if (mathNode) {
1397
+ isFormula = true;
1398
+ formulaText = (0, mathUtils_js_1.mathmlToLatex)(mathNode).trim();
1399
+ }
1400
+ }
1401
+ }
1402
+ }
1403
+ if (isFormula) {
1404
+ cellText += formulaText;
1405
+ const formulaNode = {
1406
+ type: 'code',
1407
+ text: formulaText,
1408
+ metadata: { math: 'inline' }
1409
+ };
1410
+ if (config.includeRawContent) {
1411
+ formulaNode.rawContent = (0, xmlUtils_js_1.getRawContent)(frame, xmlString, config);
1412
+ }
1413
+ children.push(formulaNode);
1414
+ }
1415
+ else if (drawImages.length > 0) {
1416
+ // logic for image node
1417
+ const imageNode = {
1418
+ type: 'image',
1419
+ text: '', // Will be populated by assignAttachmentData
1420
+ children: [],
1421
+ metadata: {
1422
+ attachmentName: imageHref || chartHref, // Might be empty, will resolve in assignAttachmentData
1423
+ ...(altText ? { altText } : {})
1424
+ }
1425
+ };
1426
+ if (config.includeRawContent) {
1427
+ imageNode.rawContent = (0, xmlUtils_js_1.getRawContent)(frame, xmlString, config);
1428
+ }
1429
+ children.push(imageNode);
1430
+ }
1431
+ else if (chartHref) {
1432
+ const chartNode = {
1433
+ type: 'chart',
1434
+ text: '', // Will be populated by assignAttachmentData
1435
+ children: [],
1436
+ metadata: {
1437
+ attachmentName: chartHref
1438
+ }
1439
+ };
1440
+ if (config.includeRawContent) {
1441
+ chartNode.rawContent = (0, xmlUtils_js_1.getRawContent)(frame, xmlString, config);
1442
+ }
1443
+ children.push(chartNode);
1444
+ }
1445
+ }
1446
+ // Add cell(s). The repeat count is attacker-influenced, so the loop
1447
+ // is bounded by the document's remaining cell budget rather than by
1448
+ // the attribute. An empty ODS cell creates nothing, so it costs no
1449
+ // budget - which is what keeps the huge trailing-empty runs real
1450
+ // files carry (number-columns-repeated="16384") free.
1451
+ // For ODS an empty cell materializes nothing, so a huge
1452
+ // number-columns-repeated on a blank cell (the normal way ODF marks a
1453
+ // trailing empty run) is skipped in O(1) by advancing the column index
1454
+ // rather than spinning the loop colsRepeated times for zero output -
1455
+ // that spin was itself a CPU denial-of-service, unbounded by the cell
1456
+ // budget because it created no cells to charge against.
1457
+ const willMaterialize = (cellText || children.length > 0 || fileType !== 'ods');
1458
+ if (!willMaterialize) {
1459
+ colIndex += colsRepeated;
1460
+ }
1461
+ else {
1462
+ const allowedCols = cellBudget.take(colsRepeated);
1463
+ for (let k = 0; k < allowedCols; k++) {
1464
+ if ((k & 1023) === 0)
1465
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
1466
+ const cellNode = {
1467
+ type: 'cell',
1468
+ text: cellText,
1469
+ children: children,
1470
+ metadata: { row: rowIndex, col: colIndex }
1471
+ };
1472
+ if (config.includeRawContent) {
1473
+ cellNode.rawContent = (0, xmlUtils_js_1.getRawContent)(cell, xmlString, config);
1474
+ }
1475
+ cells.push(cellNode);
1476
+ colIndex++;
1477
+ }
1478
+ }
1479
+ }
1480
+ // Add row(s). This is where the two repeats multiply: each repetition
1481
+ // deep-copies the whole cell array, so rows x cols is what actually
1482
+ // exhausts memory. Charge the copies against the same budget.
1483
+ if (cells.length > 0) {
1484
+ const allowedRows = Math.min(rowsRepeated,
1485
+ // The first row reuses `cells` rather than copying, so only the
1486
+ // repeats beyond it cost budget.
1487
+ 1 + Math.floor(cellBudget.take(Math.max(0, (rowsRepeated - 1) * cells.length)) / cells.length));
1488
+ for (let k = 0; k < allowedRows; k++) {
1489
+ if ((k & 255) === 0)
1490
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
1491
+ const rowNode = {
1492
+ type: 'row',
1493
+ children: JSON.parse(JSON.stringify(cells)), // Deep copy for repeated rows
1494
+ metadata: undefined
1495
+ };
1496
+ // Fix row index in metadata for repeated rows
1497
+ if (k > 0) {
1498
+ rowNode.children?.forEach(c => {
1499
+ if (c.metadata && 'row' in c.metadata) {
1500
+ c.metadata.row = rowIndex;
1501
+ }
1502
+ });
1503
+ }
1504
+ if (config.includeRawContent) {
1505
+ rowNode.rawContent = (0, xmlUtils_js_1.getRawContent)(row, xmlString, config);
1506
+ }
1507
+ rows.push(rowNode);
1508
+ rowIndex++;
1509
+ }
1510
+ }
1511
+ else {
1512
+ rowIndex += rowsRepeated;
1513
+ }
1514
+ }
1515
+ const sheetNode = {
1516
+ type: 'sheet',
1517
+ children: rows,
1518
+ metadata: { sheetName }
1519
+ };
1520
+ if (config.includeRawContent) {
1521
+ sheetNode.rawContent = (0, xmlUtils_js_1.getRawContent)(table, xmlString, config);
1522
+ }
1523
+ content.push(sheetNode);
1524
+ }
1525
+ }
1526
+ }
1527
+ // ODP: Presentation
1528
+ else if (fileType === 'odp') {
1529
+ const presentation = (0, xmlUtils_js_1.getFirstElementByTagName)(body, "office:presentation");
1530
+ if (presentation) {
1531
+ const pages = (0, xmlUtils_js_1.getDirectChildren)(presentation, "draw:page");
1532
+ const odpNotes = [];
1533
+ for (let i = 0; i < pages.length; i++) {
1534
+ const page = pages[i];
1535
+ const slideNode = {
1536
+ type: 'slide',
1537
+ children: [],
1538
+ metadata: { slideNumber: i + 1 }
1539
+ };
1540
+ // Separate page content and notes
1541
+ let noteNode = undefined;
1542
+ const pageChildren = page.childNodes;
1543
+ if (pageChildren) {
1544
+ for (let j = 0; j < pageChildren.length; j++) {
1545
+ const child = pageChildren[j];
1546
+ if ((0, xmlUtils_js_1.isElement)(child)) { // Element
1547
+ const element = child;
1548
+ if (element.tagName === "presentation:notes") {
1549
+ if (!config.ignoreNotes) {
1550
+ noteNode = {
1551
+ type: 'note',
1552
+ children: [],
1553
+ metadata: {
1554
+ slideNumber: i + 1,
1555
+ noteId: `slide-note-${i + 1}`
1556
+ }
1557
+ };
1558
+ traverse(element, noteNode.children, false, xmlString);
1559
+ }
1560
+ continue;
1561
+ }
1562
+ traverse(element, slideNode.children, false, xmlString);
1563
+ }
1564
+ }
1565
+ }
1566
+ if (config.includeRawContent) {
1567
+ slideNode.rawContent = (0, xmlUtils_js_1.getRawContent)(page, xmlString, config);
1568
+ }
1569
+ content.push(slideNode);
1570
+ if (noteNode && noteNode.children && noteNode.children.length > 0) {
1571
+ if (!slideNode.notes)
1572
+ slideNode.notes = [];
1573
+ slideNode.notes.push(noteNode);
1574
+ }
1575
+ }
1576
+ if (odpNotes.length > 0) {
1577
+ content.push(...odpNotes);
1578
+ }
1579
+ }
1580
+ }
1581
+ // ODT: Text Document (and generic fallback)
1582
+ else {
1583
+ const textDoc = (0, xmlUtils_js_1.getFirstElementByTagName)(body, "office:text");
1584
+ if (textDoc) {
1585
+ traverse(textDoc, content, false, xmlString);
1586
+ }
1587
+ }
1588
+ };
1589
+ if (mainContentFile) {
1590
+ parseContentXml(mainContentFile.content.toString());
1591
+ }
1592
+ // Attachments
1593
+ const attachments = [];
1594
+ const mediaFiles = files.filter(f => f.path.match(/(Pictures|media)\/.*/));
1595
+ // ODP/ODT Chart Extraction
1596
+ if (config.extractAttachments) {
1597
+ const objectFiles = files.filter(f => f.path.match(/Object \d+\/content\.xml/));
1598
+ for (const objFile of objectFiles) {
1599
+ const objXml = (0, xmlUtils_js_1.parseXmlString)(objFile.content.toString());
1600
+ const isChart = (0, xmlUtils_js_1.getElementsByTagName)(objXml, "chart:chart").length > 0;
1601
+ if (isChart) {
1602
+ const objectId = objFile.path.split('/')[0];
1603
+ const attachment = {
1604
+ type: 'chart',
1605
+ mimeType: 'application/vnd.oasis.opendocument.chart', // Accurate ODF chart type
1606
+ data: objFile.content.toString('base64'),
1607
+ name: objectId,
1608
+ extension: 'xml'
1609
+ };
1610
+ // Extract data from chart XML
1611
+ const chartData = (0, chartUtils_js_1.extractChartData)(objFile.content);
1612
+ if (chartData.rawTexts.length > 0) {
1613
+ attachment.chartData = chartData;
1614
+ }
1615
+ attachments.push(attachment);
1616
+ }
1617
+ }
1618
+ }
1619
+ if (config.extractAttachments) {
1620
+ for (const media of mediaFiles) {
1621
+ const attachment = (0, imageUtils_js_1.createAttachment)(media.path.split('/').pop() || 'image', media.content);
1622
+ attachments.push(attachment);
1623
+ if (config.ocr) {
1624
+ if (attachment.mimeType.startsWith('image/')) {
1625
+ try {
1626
+ attachment.ocrText = (await (0, ocrUtils_js_1.performOcr)(media.content, { ...config.ocrConfig })).trim();
1627
+ }
1628
+ catch (e) {
1629
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.OCR_FAILED, config, attachment.name, e);
1630
+ }
1631
+ }
1632
+ }
1633
+ }
1634
+ }
1635
+ const metaFile = files.find(f => f.path.match(metaFileRegex));
1636
+ const metadata = metaFile ? (0, xmlUtils_js_1.parseOfficeMetadata)(metaFile.content.toString()) : {};
1637
+ // Helper: Resolve ODS chart cell references to actual values
1638
+ // ODS charts often link to cell ranges (e.g., [Sheet1.$A$1:.$A$5]) instead of embedding values
1639
+ const resolveChartReferences = (chartData, nodes) => {
1640
+ const getValuesFromReference = (ref) => {
1641
+ // Remove brackets: [Sheet.$A$1:.$A$5] -> Sheet.$A$1:.$A$5
1642
+ const cleanRef = ref.replace(/^\[|\]$/g, '');
1643
+ const [startPart, endPart] = cleanRef.split(':');
1644
+ const lastDotIdx = startPart.lastIndexOf('.');
1645
+ if (lastDotIdx === -1)
1646
+ return [ref];
1647
+ const sheetName = startPart.substring(0, lastDotIdx).replace(/^'|'$/g, '');
1648
+ const startCoord = startPart.substring(lastDotIdx + 1).replace(/\$/g, '');
1649
+ let endCoord = startCoord;
1650
+ if (endPart) {
1651
+ if (endPart.startsWith('.')) {
1652
+ endCoord = endPart.substring(1).replace(/\$/g, '');
1653
+ }
1654
+ else {
1655
+ const endLastDotIdx = endPart.lastIndexOf('.');
1656
+ endCoord = endPart.substring(endLastDotIdx + 1).replace(/\$/g, '');
1657
+ }
1658
+ }
1659
+ const parseCoord = (coord) => {
1660
+ const colMatch = coord.match(/[A-Z]+/);
1661
+ const rowMatch = coord.match(/\d+/);
1662
+ if (!colMatch || !rowMatch)
1663
+ return null;
1664
+ const colStr = colMatch[0];
1665
+ let colIdx = 0;
1666
+ for (let i = 0; i < colStr.length; i++) {
1667
+ colIdx = colIdx * 26 + (colStr.charCodeAt(i) - 'A'.charCodeAt(0) + 1);
1668
+ }
1669
+ colIdx -= 1;
1670
+ const rowIdx = parseInt(rowMatch[0]) - 1;
1671
+ return { r: rowIdx, c: colIdx };
1672
+ };
1673
+ const start = parseCoord(startCoord);
1674
+ const end = parseCoord(endCoord);
1675
+ if (!start || !end)
1676
+ return [ref];
1677
+ const sheet = nodes.find(n => n.type === 'sheet' && n.metadata?.sheetName === sheetName);
1678
+ if (!sheet || !sheet.children)
1679
+ return [ref];
1680
+ const values = [];
1681
+ // Collect all matching cells
1682
+ for (const row of sheet.children) {
1683
+ if (row.children) {
1684
+ for (const cell of row.children) {
1685
+ const meta = cell.metadata;
1686
+ if (meta && meta.row >= start.r && meta.row <= end.r && meta.col >= start.c && meta.col <= end.c) {
1687
+ values.push(cell.text || '');
1688
+ }
1689
+ }
1690
+ }
1691
+ }
1692
+ return values.length > 0 ? values : [];
1693
+ };
1694
+ // Resolve DataSets
1695
+ for (const ds of chartData.dataSets) {
1696
+ const newValues = [];
1697
+ for (const val of ds.values) {
1698
+ if (val.startsWith('['))
1699
+ newValues.push(...getValuesFromReference(val));
1700
+ else
1701
+ newValues.push(val);
1702
+ }
1703
+ ds.values = newValues;
1704
+ }
1705
+ // Resolve Labels
1706
+ const newLabels = [];
1707
+ for (const label of chartData.labels) {
1708
+ if (label.startsWith('['))
1709
+ newLabels.push(...getValuesFromReference(label));
1710
+ else
1711
+ newLabels.push(label);
1712
+ }
1713
+ chartData.labels = newLabels;
1714
+ // Rebuild rawTexts
1715
+ chartData.rawTexts = [];
1716
+ if (chartData.title)
1717
+ chartData.rawTexts.push(chartData.title);
1718
+ for (const ds of chartData.dataSets) {
1719
+ if (ds.name)
1720
+ chartData.rawTexts.push(ds.name);
1721
+ chartData.rawTexts.push(...chartData.labels);
1722
+ chartData.rawTexts.push(...ds.values);
1723
+ }
1724
+ };
1725
+ // Apply resolution to all chart attachments
1726
+ for (const att of attachments) {
1727
+ if (att.type === 'chart' && att.chartData) {
1728
+ resolveChartReferences(att.chartData, content);
1729
+ }
1730
+ }
1731
+ // Link OCR and Chart text to content nodes
1732
+ // Link OCR and Chart text to content nodes (with heuristic for unlinked images)
1733
+ const assignAttachmentData = (nodes) => {
1734
+ // Step 1: Identify unused image attachments globally
1735
+ const usedAttachmentNames = new Set();
1736
+ const traverseForNames = (ns) => {
1737
+ for (const n of ns) {
1738
+ if (n.metadata && 'attachmentName' in n.metadata) {
1739
+ const name = n.metadata.attachmentName;
1740
+ if (name)
1741
+ usedAttachmentNames.add(name);
1742
+ }
1743
+ if (n.children)
1744
+ traverseForNames(n.children);
1745
+ }
1746
+ };
1747
+ traverseForNames(nodes);
1748
+ const unusedImages = attachments.filter(a => a.type === 'image' && a.name && !usedAttachmentNames.has(a.name));
1749
+ let unusedImageIndex = 0;
1750
+ const processNode = (node) => {
1751
+ if ((node.type === 'image' || node.type === 'chart') && node.metadata && 'attachmentName' in node.metadata) {
1752
+ let attachmentName = node.metadata.attachmentName;
1753
+ // Heuristic: If name is empty, try to assign an unused image attachment
1754
+ if (!attachmentName && node.type === 'image' && unusedImageIndex < unusedImages.length) {
1755
+ const fallbackAtt = unusedImages[unusedImageIndex++];
1756
+ attachmentName = fallbackAtt.name;
1757
+ node.metadata.attachmentName = attachmentName;
1758
+ }
1759
+ if (attachmentName) {
1760
+ const attachment = attachments.find(a => a.name === attachmentName);
1761
+ if (attachment) {
1762
+ if (attachment.ocrText) {
1763
+ node.text = attachment.ocrText;
1764
+ }
1765
+ if (attachment.chartData && node.type === 'chart') {
1766
+ node.text = attachment.chartData.rawTexts.join(config.newlineDelimiter);
1767
+ }
1768
+ }
1769
+ }
1770
+ }
1771
+ // Internal recursion
1772
+ if (node.children) {
1773
+ node.children.forEach(processNode);
1774
+ }
1775
+ };
1776
+ nodes.forEach(processNode);
1777
+ };
1778
+ assignAttachmentData(content);
1779
+ // Create combined styleMap for metadata (matches DOCX format)
1780
+ const combinedStyleMap = {};
1781
+ for (const styleName in styleMap) {
1782
+ combinedStyleMap[styleName] = {
1783
+ formatting: styleMap[styleName],
1784
+ alignment: paragraphStyleMap[styleName]?.alignment
1785
+ };
1786
+ }
1787
+ // Also add styles that only have alignment
1788
+ for (const styleName in paragraphStyleMap) {
1789
+ if (!combinedStyleMap[styleName]) {
1790
+ combinedStyleMap[styleName] = {
1791
+ formatting: {},
1792
+ alignment: paragraphStyleMap[styleName]?.alignment
1793
+ };
1794
+ }
1795
+ }
1796
+ const toTextSync = () => content.map(c => {
1797
+ const getText = (node) => {
1798
+ let t = '';
1799
+ if (node.children && node.children.length > 0) {
1800
+ // Check if children have their own children (container vs leaf)
1801
+ // If children are leaf nodes (text/image), join with empty string
1802
+ // If children are container nodes (paragraphs/rows), join with newline
1803
+ const hasGrandChildren = node.children.some(child => child.children && child.children.length > 0);
1804
+ const separator = hasGrandChildren ? config.newlineDelimiter : '';
1805
+ t += node.children.map(getText).filter(t => t != '').join(separator);
1806
+ }
1807
+ else {
1808
+ t += node.text || '';
1809
+ }
1810
+ return t;
1811
+ };
1812
+ return getText(c);
1813
+ }).filter(t => t != '').join(config.newlineDelimiter);
1814
+ return (0, astUtils_js_1.createAST)(fileType, {
1815
+ ...metadata,
1816
+ styleMap: combinedStyleMap
1817
+ }, content, attachments, config, undefined, toTextSync);
1818
+ };
1819
+ exports.parseOpenOffice = parseOpenOffice;