@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.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +46 -0
- package/assets/skill/SKILL.md +7 -6
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +32 -33
- package/bunfig.toml +2 -0
- package/package.json +40 -26
- package/spec/cli.md +21 -4
- package/spec/db/schema.sql +146 -1
- package/spec/mcp.md +26 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +2 -0
- package/src/cli/commands/ask.ts +6 -1
- package/src/cli/commands/daemon.ts +21 -8
- package/src/cli/commands/embed.ts +77 -41
- package/src/cli/detach.ts +3 -2
- package/src/config/types.ts +3 -3
- package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
- package/src/converters/adapters/officeparser/adapter.ts +1 -2
- package/src/converters/versions.ts +6 -8
- package/src/core/context-evidence.ts +8 -4
- package/src/core/job-manager.ts +95 -13
- package/src/core/network-boundary-inventory.ts +10 -0
- package/src/core/shutdown-budget.ts +45 -0
- package/src/embed/backlog.ts +107 -4
- package/src/embed/batch.ts +42 -2
- package/src/embed/fingerprint.ts +16 -0
- package/src/embed/retry.ts +113 -5
- package/src/embed/variant-backlog.ts +105 -0
- package/src/embed/variant-plan.ts +62 -0
- package/src/embed/variant-retry.ts +113 -0
- package/src/ingestion/graph-reconciliation.ts +327 -0
- package/src/ingestion/sync.ts +9 -272
- package/src/llm/http-inference.ts +6 -0
- package/src/llm/httpEmbedding.ts +37 -6
- package/src/llm/httpGeneration.ts +18 -3
- package/src/llm/httpRerank.ts +23 -5
- package/src/llm/inference-cancellation.ts +168 -0
- package/src/llm/inference-scope.ts +202 -0
- package/src/llm/lazy-ports.ts +115 -0
- package/src/llm/native-worker/client.ts +541 -0
- package/src/llm/native-worker/dispatcher.ts +228 -0
- package/src/llm/native-worker/embedding-identity.ts +33 -0
- package/src/llm/native-worker/entry.ts +173 -0
- package/src/llm/native-worker/errors.ts +32 -0
- package/src/llm/native-worker/evaluation.ts +16 -0
- package/src/llm/native-worker/owned-exit.ts +108 -0
- package/src/llm/native-worker/owner.ts +141 -0
- package/src/llm/native-worker/ports.ts +317 -0
- package/src/llm/native-worker/protocol.ts +442 -0
- package/src/llm/native-worker/runtime-config.ts +92 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
- package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
- package/src/llm/nodeLlamaCpp/generation.ts +34 -5
- package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
- package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
- package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
- package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
- package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
- package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
- package/src/llm/types.ts +35 -5
- package/src/mcp/context.ts +27 -0
- package/src/mcp/http-transport.ts +12 -10
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-profile.ts +30 -8
- package/src/mcp/tools/context.ts +8 -11
- package/src/mcp/tools/embed.ts +1 -1
- package/src/mcp/tools/index-cmd.ts +1 -1
- package/src/mcp/tools/index.ts +10 -8
- package/src/mcp/tools/query.ts +14 -30
- package/src/mcp/tools/vsearch.ts +1 -1
- package/src/pipeline/answer.ts +23 -3
- package/src/pipeline/claim-verifier.ts +6 -0
- package/src/pipeline/expansion.ts +43 -40
- package/src/pipeline/explain.ts +6 -2
- package/src/pipeline/filters.ts +63 -0
- package/src/pipeline/fusion.ts +29 -9
- package/src/pipeline/graph-retrieval.ts +29 -9
- package/src/pipeline/hybrid.ts +198 -55
- package/src/pipeline/hydration.ts +161 -0
- package/src/pipeline/owner-fusion.ts +87 -0
- package/src/pipeline/rerank.ts +35 -11
- package/src/pipeline/search.ts +13 -2
- package/src/pipeline/types.ts +5 -3
- package/src/pipeline/vsearch.ts +87 -7
- package/src/sdk/client.ts +47 -3
- package/src/sdk/embed.ts +63 -39
- package/src/serve/background-runtime.ts +1 -1
- package/src/serve/context.ts +41 -56
- package/src/serve/embed-scheduler.ts +58 -35
- package/src/serve/public/components/IndexingProgress.tsx +46 -60
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/shiki-language-ids.ts +14 -0
- package/src/serve/resident-admission.ts +36 -36
- package/src/serve/resident-background-work.ts +20 -2
- package/src/serve/resident-request.ts +11 -5
- package/src/serve/resident-runtime.ts +97 -61
- package/src/serve/resident-shutdown.ts +153 -0
- package/src/serve/routes/api.ts +3 -1
- package/src/serve/server.ts +47 -26
- package/src/store/migrations/028-vector-variants.ts +54 -0
- package/src/store/migrations/029-graph-reference-state.ts +77 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +251 -183
- package/src/store/sqlite/eligibility.ts +174 -0
- package/src/store/sqlite/graph-edge-application.ts +66 -0
- package/src/store/sqlite/graph-reference-state.ts +194 -0
- package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
- package/src/store/types.ts +80 -12
- package/src/store/vector/eligibility.ts +36 -0
- package/src/store/vector/freshness.ts +33 -6
- package/src/store/vector/lazy.ts +81 -0
- package/src/store/vector/sqlite-vec.ts +106 -54
- package/src/store/vector/stats.ts +14 -3
- package/src/store/vector/types.ts +35 -2
- package/src/store/vector/variant-search.ts +192 -0
- package/src/store/vector/variants.ts +451 -0
- package/vendor/converters/markitdown-ts/LICENSE +21 -0
- package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
- package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
- package/vendor/converters/markitdown-ts/package.json +77 -0
- package/vendor/converters/officeparser/LICENSE +21 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
- package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
- package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
- package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
- package/vendor/converters/officeparser/dist/cli.js +381 -0
- package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
- package/vendor/converters/officeparser/dist/defaults.js +218 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
- package/vendor/converters/officeparser/dist/index.d.ts +60 -0
- package/vendor/converters/officeparser/dist/index.js +72 -0
- package/vendor/converters/officeparser/dist/index.mjs +18 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
- package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
- package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
- package/vendor/converters/officeparser/dist/types.js +107 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
- package/vendor/converters/officeparser/package.json +147 -0
- package/vendor/converters/upstream-manifest.json +124 -0
- package/vendor/dependency-fixes/README.md +77 -0
- package/vendor/dependency-fixes/vendor-converters.py +83 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-627emwpj.js +0 -75
- /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;
|