@stll/folio-core 0.12.0 → 0.14.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/dist/ai-edits/apply.d.ts +2 -1
- package/dist/ai-edits/apply.js +118 -40
- package/dist/ai-edits/blockRange.js +1 -1
- package/dist/ai-edits/clean-text.js +2 -1
- package/dist/ai-edits/headless.js +3 -3
- package/dist/ai-edits/snapshot.js +21 -3
- package/dist/ai-edits/table-cell-mutations.js +2 -1
- package/dist/ai-edits/table-row-column-mutations.d.ts +9 -1
- package/dist/ai-edits/table-row-column-mutations.js +1 -1
- package/dist/ai-edits/types.d.ts +28 -1
- package/dist/ai-edits/word-diff.js +21 -0
- package/dist/compat/eigenpal.d.ts +3 -3
- package/dist/compat/eigenpal.js +3 -3
- package/dist/controller/headerFooterEditorManager.js +1 -1
- package/dist/controller/hiddenEditorManager.js +12 -1
- package/dist/controller/layoutPipeline.js +1 -1
- package/dist/controller/noteEditorManager.js +1 -1
- package/dist/document-operations.d.ts +13 -13
- package/dist/document-operations.js +31 -21
- package/dist/docx/blockContentParser.js +19 -5
- package/dist/docx/commentRangeIntegrity.d.ts +6 -0
- package/dist/docx/commentRangeIntegrity.js +186 -0
- package/dist/docx/conformance.js +57 -3
- package/dist/docx/encryption/agileDecryption.js +12 -2
- package/dist/docx/encryption/compoundFile.js +6 -1
- package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
- package/dist/docx/encryption/encryptionInfo.js +18 -2
- package/dist/docx/encryption/openEncryptedDocx.js +1 -1
- package/dist/docx/ensureParaIds.js +7 -6
- package/dist/docx/groupDrawingParser.js +10 -5
- package/dist/docx/hyperlinkParser.js +1 -1
- package/dist/docx/imageParser.js +10 -6
- package/dist/docx/metadataPrivacy.js +17 -3
- package/dist/docx/paragraphParser.js +7 -3
- package/dist/docx/rezip.js +19 -8
- package/dist/docx/runParser.js +13 -4
- package/dist/docx/sectionParser.js +17 -4
- package/dist/docx/selectiveSave.js +4 -0
- package/dist/docx/selectiveXmlPatch.d.ts +22 -1
- package/dist/docx/selectiveXmlPatch.js +61 -1
- package/dist/docx/serializer/blockSdtSerializer.js +3 -2
- package/dist/docx/serializer/commentSerializer.js +4 -4
- package/dist/docx/serializer/paragraphSerializer.js +16 -14
- package/dist/docx/serializer/runSerializer.js +21 -19
- package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
- package/dist/docx/serializer/tableSerializer.js +9 -7
- package/dist/docx/serializer/xmlUtils.d.ts +13 -1
- package/dist/docx/serializer/xmlUtils.js +27 -1
- package/dist/docx/server/docxToMarkdown.d.ts +15 -0
- package/dist/docx/server/docxToMarkdown.js +17 -0
- package/dist/docx/server/extractDocxText.js +55 -8
- package/dist/docx/settingsParser.js +15 -2
- package/dist/docx/tableParser.d.ts +8 -1
- package/dist/docx/tableParser.js +29 -8
- package/dist/docx/unzip.js +1 -0
- package/dist/docx/vmlImageParser.js +11 -3
- package/dist/docx/xmlParser.js +11 -1
- package/dist/fonts/embeddedFonts.d.ts +33 -5
- package/dist/fonts/embeddedFonts.js +37 -8
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/layout-bridge/convert/toFlowBlocks.js +15 -0
- package/dist/layout-engine/index.js +19 -12
- package/dist/layout-engine/measure/cache.js +2 -0
- package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
- package/dist/layout-engine/measure/floatingTablePosition.d.ts +9 -0
- package/dist/layout-engine/measure/floatingTablePosition.js +17 -0
- package/dist/layout-engine/measure/index.d.ts +2 -1
- package/dist/layout-engine/measure/index.js +2 -1
- package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
- package/dist/layout-engine/measure/lineBreakProvider.js +3 -3
- package/dist/layout-engine/measure/measureBlocks.js +11 -8
- package/dist/layout-engine/measure/measureHelpers.d.ts +10 -1
- package/dist/layout-engine/measure/measureHelpers.js +12 -1
- package/dist/layout-engine/measure/measureParagraph.js +16 -0
- package/dist/layout-engine/types.d.ts +22 -2
- package/dist/layout-painter/renderImage.d.ts +12 -1
- package/dist/layout-painter/renderImage.js +21 -4
- package/dist/layout-painter/renderPage.js +5 -8
- package/dist/layout-painter/renderParagraph.js +33 -9
- package/dist/layout-painter/renderTable.js +2 -1
- package/dist/layout-painter/renderWatermark.d.ts +1 -1
- package/dist/layout-painter/renderWatermark.js +2 -1
- package/dist/markdown/fromMarkdown.js +1 -1
- package/dist/prosemirror/attrs/index.d.ts +1 -1
- package/dist/prosemirror/attrs/index.js +21 -0
- package/dist/prosemirror/commands/comments.d.ts +76 -1
- package/dist/prosemirror/commands/comments.js +401 -28
- package/dist/prosemirror/commands/hyperlink.js +15 -1
- package/dist/prosemirror/commands/image.js +7 -1
- package/dist/prosemirror/commands/index.d.ts +2 -2
- package/dist/prosemirror/commands/index.js +2 -2
- package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
- package/dist/prosemirror/commentIdAllocator.js +13 -3
- package/dist/prosemirror/conversion/fromProseDoc.js +122 -5
- package/dist/prosemirror/conversion/index.js +1 -1
- package/dist/prosemirror/conversion/toProseDoc.js +20 -8
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
- package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
- package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +49 -13
- package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
- package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -3
- package/dist/prosemirror/index.d.ts +1 -1
- package/dist/prosemirror/index.js +1 -1
- package/dist/prosemirror/plugins/contentControlWidgets.d.ts +2 -2
- package/dist/prosemirror/plugins/contentControlWidgets.js +4 -4
- package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
- package/dist/prosemirror/plugins/revisionIds.js +57 -0
- package/dist/prosemirror/plugins/suggestionMode.js +4 -3
- package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
- package/dist/prosemirror/runFormattingMarkNames.js +40 -0
- package/dist/prosemirror/schema/index.d.ts +1 -1
- package/dist/prosemirror/schema/marks.d.ts +27 -4
- package/dist/prosemirror/schema/marks.js +15 -0
- package/dist/prosemirror/schema/nodes.d.ts +50 -2
- package/dist/prosemirror/utils/tabCalculator.js +3 -3
- package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +4 -3
- package/dist/utils/clipboard.d.ts +10 -1
- package/dist/utils/clipboard.js +33 -6
- package/dist/utils/colorResolver.d.ts +8 -1
- package/dist/utils/colorResolver.js +35 -5
- package/dist/utils/fontResolver.d.ts +2 -1
- package/dist/utils/fontResolver.js +9 -3
- package/dist/utils/hexId.d.ts +11 -1
- package/dist/utils/hexId.js +11 -1
- package/dist/utils/sanitizeImageSrc.d.ts +21 -0
- package/dist/utils/sanitizeImageSrc.js +30 -0
- package/dist/utils/stripXmlDeclarations.d.ts +11 -0
- package/dist/utils/stripXmlDeclarations.js +35 -0
- package/dist/version-comparison.d.ts +22 -2
- package/dist/version-comparison.js +41 -12
- package/dist/watermark/index.d.ts +11 -1
- package/dist/watermark/index.js +22 -1
- package/package.json +3 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { numPrEqual } from "../numberingParser.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isValidHexColor } from "../../utils/colorResolver.js";
|
|
3
|
+
import { escapeXml, intAttr, isSingleWellFormedElement } from "./xmlUtils.js";
|
|
3
4
|
import { reconcileRawSdtPr } from "../sdtPropertiesPatch.js";
|
|
4
5
|
import { serializeBorder } from "./borderSerializer.js";
|
|
5
6
|
import { serializeRun, serializeTextFormatting } from "./runSerializer.js";
|
|
6
7
|
import { serializeSectionProperties } from "./sectionPropertiesSerializer.js";
|
|
8
|
+
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
7
9
|
//#region src/docx/serializer/paragraphSerializer.ts
|
|
8
10
|
/**
|
|
9
11
|
* Serialize paragraph borders (w:pBdr)
|
|
@@ -44,15 +46,15 @@ function serializeParagraphBorders(borders) {
|
|
|
44
46
|
function serializeShading(shading) {
|
|
45
47
|
if (!shading) return "";
|
|
46
48
|
const attrs = [];
|
|
47
|
-
if (shading.pattern) attrs.push(`w:val="${shading.pattern}"`);
|
|
49
|
+
if (shading.pattern) attrs.push(`w:val="${escapeXml(shading.pattern)}"`);
|
|
48
50
|
else attrs.push("w:val=\"clear\"");
|
|
49
|
-
if (shading.color?.rgb) attrs.push(`w:color="${shading.color.rgb}"`);
|
|
51
|
+
if (shading.color?.rgb && isValidHexColor(shading.color.rgb)) attrs.push(`w:color="${escapeXml(shading.color.rgb)}"`);
|
|
50
52
|
else if (shading.color?.auto) attrs.push("w:color=\"auto\"");
|
|
51
|
-
if (shading.fill?.rgb) attrs.push(`w:fill="${shading.fill.rgb}"`);
|
|
53
|
+
if (shading.fill?.rgb && isValidHexColor(shading.fill.rgb)) attrs.push(`w:fill="${escapeXml(shading.fill.rgb)}"`);
|
|
52
54
|
else if (shading.fill?.auto) attrs.push("w:fill=\"auto\"");
|
|
53
|
-
if (shading.fill?.themeColor) attrs.push(`w:themeFill="${shading.fill.themeColor}"`);
|
|
54
|
-
if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${shading.fill.themeTint}"`);
|
|
55
|
-
if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${shading.fill.themeShade}"`);
|
|
55
|
+
if (shading.fill?.themeColor) attrs.push(`w:themeFill="${escapeXml(shading.fill.themeColor)}"`);
|
|
56
|
+
if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${escapeXml(shading.fill.themeTint)}"`);
|
|
57
|
+
if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${escapeXml(shading.fill.themeShade)}"`);
|
|
56
58
|
if (attrs.length === 0) return "";
|
|
57
59
|
return `<w:shd ${attrs.join(" ")}/>`;
|
|
58
60
|
}
|
|
@@ -132,7 +134,7 @@ function serializeFrameProperties(frame) {
|
|
|
132
134
|
* Serialize paragraph formatting properties to w:pPr XML
|
|
133
135
|
*/
|
|
134
136
|
function serializeTrackedChangeAttrs(info) {
|
|
135
|
-
const parts = [`w:id="${info.id}"`, `w:author="${escapeXml(info.author)}"`];
|
|
137
|
+
const parts = [`w:id="${normalizeRevisionId(info.id)}"`, `w:author="${escapeXml(info.author)}"`];
|
|
136
138
|
if (info.date !== void 0) parts.push(`w:date="${escapeXml(info.date)}"`);
|
|
137
139
|
return parts.join(" ");
|
|
138
140
|
}
|
|
@@ -198,7 +200,7 @@ function extractRPrInner(rPrXml) {
|
|
|
198
200
|
return rPrXml.slice(7, -8);
|
|
199
201
|
}
|
|
200
202
|
function serializeParagraphPropertyChange(change) {
|
|
201
|
-
const normalizedId =
|
|
203
|
+
const normalizedId = normalizeRevisionId(change.info.id);
|
|
202
204
|
const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
|
|
203
205
|
const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
|
|
204
206
|
const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
|
|
@@ -361,13 +363,13 @@ function serializeInlineSdt(sdt) {
|
|
|
361
363
|
default: return item;
|
|
362
364
|
}
|
|
363
365
|
}).join("");
|
|
364
|
-
const baseSdtPr = props.rawPropertiesXml
|
|
366
|
+
const baseSdtPr = props.rawPropertiesXml && isSingleWellFormedElement(props.rawPropertiesXml, "sdtPr") ? props.rawPropertiesXml : synthesizeInlineSdtPr(props);
|
|
365
367
|
const dateFullDate = props.sdtType === "date" && props.dateValueISO ? props.dateValueISO : void 0;
|
|
366
368
|
const dropdownLastValue = (props.sdtType === "dropdown" || props.sdtType === "comboBox") && typeof props.dropdownLastValue === "string" ? props.dropdownLastValue : void 0;
|
|
367
369
|
return `<w:sdt>${reconcileRawSdtPr(baseSdtPr, props, {
|
|
368
370
|
...dateFullDate !== void 0 ? { dateFullDate } : {},
|
|
369
371
|
...dropdownLastValue !== void 0 ? { dropdownLastValue } : {}
|
|
370
|
-
})}${props.rawEndPropertiesXml
|
|
372
|
+
})}${props.rawEndPropertiesXml && isSingleWellFormedElement(props.rawEndPropertiesXml, "sdtEndPr") ? props.rawEndPropertiesXml : ""}<w:sdtContent>${contentXml}</w:sdtContent></w:sdt>`;
|
|
371
373
|
}
|
|
372
374
|
function serializeMoveRangeStart(tag, marker) {
|
|
373
375
|
return `<w:${tag} ${[`w:id="${marker.id}"`, `w:name="${escapeXml(marker.name)}"`].join(" ")}/>`;
|
|
@@ -380,7 +382,7 @@ function rewriteRunTextAsDeleted(xml) {
|
|
|
380
382
|
}
|
|
381
383
|
function serializeTrackedChange(tag, change) {
|
|
382
384
|
const info = change.info;
|
|
383
|
-
const normalizedId =
|
|
385
|
+
const normalizedId = normalizeRevisionId(info.id);
|
|
384
386
|
const authorCandidate = typeof info.author === "string" ? info.author.trim() : "";
|
|
385
387
|
const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
|
|
386
388
|
const normalizedDate = typeof info.date === "string" ? info.date.trim() : void 0;
|
|
@@ -455,8 +457,8 @@ function serializeParagraphContent(content, explicitCommentReferenceIds) {
|
|
|
455
457
|
function serializeParagraph(paragraph) {
|
|
456
458
|
const parts = [];
|
|
457
459
|
const attrs = [];
|
|
458
|
-
if (paragraph.paraId) attrs.push(`w14:paraId="${paragraph.paraId}"`);
|
|
459
|
-
if (paragraph.textId) attrs.push(`w14:textId="${paragraph.textId}"`);
|
|
460
|
+
if (paragraph.paraId) attrs.push(`w14:paraId="${escapeXml(paragraph.paraId)}"`);
|
|
461
|
+
if (paragraph.textId) attrs.push(`w14:textId="${escapeXml(paragraph.textId)}"`);
|
|
460
462
|
const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
461
463
|
const pPrXml = serializeParagraphFormatting(paragraph.formatting, paragraph.propertyChanges, paragraph.pPrMark);
|
|
462
464
|
const sectionPropertiesXml = serializeSectionProperties(paragraph.sectionProperties);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { HIGHLIGHT_COLOR_VALUES } from "../../types/documentEnumValues.js";
|
|
2
|
+
import { isValidHexColor } from "../../utils/colorResolver.js";
|
|
2
3
|
import { escapeXml, intAttr } from "./xmlUtils.js";
|
|
3
4
|
import { serializeTable } from "./tableSerializer.js";
|
|
4
5
|
import { serializeParagraph } from "./paragraphSerializer.js";
|
|
5
6
|
import { panic } from "better-result";
|
|
7
|
+
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
6
8
|
//#region src/docx/serializer/runSerializer.ts
|
|
7
9
|
/**
|
|
8
10
|
* Run Serializer - Serialize runs to OOXML XML
|
|
@@ -42,10 +44,10 @@ function serializeColorElement(color) {
|
|
|
42
44
|
if (!color) return "";
|
|
43
45
|
const attrs = [];
|
|
44
46
|
if (color.auto) attrs.push("w:val=\"auto\"");
|
|
45
|
-
else if (color.rgb) attrs.push(`w:val="${color.rgb}"`);
|
|
46
|
-
if (color.themeColor) attrs.push(`w:themeColor="${color.themeColor}"`);
|
|
47
|
-
if (color.themeTint) attrs.push(`w:themeTint="${color.themeTint}"`);
|
|
48
|
-
if (color.themeShade) attrs.push(`w:themeShade="${color.themeShade}"`);
|
|
47
|
+
else if (color.rgb && isValidHexColor(color.rgb)) attrs.push(`w:val="${escapeXml(color.rgb)}"`);
|
|
48
|
+
if (color.themeColor) attrs.push(`w:themeColor="${escapeXml(color.themeColor)}"`);
|
|
49
|
+
if (color.themeTint) attrs.push(`w:themeTint="${escapeXml(color.themeTint)}"`);
|
|
50
|
+
if (color.themeShade) attrs.push(`w:themeShade="${escapeXml(color.themeShade)}"`);
|
|
49
51
|
if (attrs.length === 0) return "";
|
|
50
52
|
return `<w:color ${attrs.join(" ")}/>`;
|
|
51
53
|
}
|
|
@@ -55,15 +57,15 @@ function serializeColorElement(color) {
|
|
|
55
57
|
function serializeShading(shading) {
|
|
56
58
|
if (!shading) return "";
|
|
57
59
|
const attrs = [];
|
|
58
|
-
if (shading.pattern) attrs.push(`w:val="${shading.pattern}"`);
|
|
60
|
+
if (shading.pattern) attrs.push(`w:val="${escapeXml(shading.pattern)}"`);
|
|
59
61
|
else attrs.push("w:val=\"clear\"");
|
|
60
|
-
if (shading.color?.rgb) attrs.push(`w:color="${shading.color.rgb}"`);
|
|
62
|
+
if (shading.color?.rgb && isValidHexColor(shading.color.rgb)) attrs.push(`w:color="${escapeXml(shading.color.rgb)}"`);
|
|
61
63
|
else if (shading.color?.auto) attrs.push("w:color=\"auto\"");
|
|
62
|
-
if (shading.fill?.rgb) attrs.push(`w:fill="${shading.fill.rgb}"`);
|
|
64
|
+
if (shading.fill?.rgb && isValidHexColor(shading.fill.rgb)) attrs.push(`w:fill="${escapeXml(shading.fill.rgb)}"`);
|
|
63
65
|
else if (shading.fill?.auto) attrs.push("w:fill=\"auto\"");
|
|
64
|
-
if (shading.fill?.themeColor) attrs.push(`w:themeFill="${shading.fill.themeColor}"`);
|
|
65
|
-
if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${shading.fill.themeTint}"`);
|
|
66
|
-
if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${shading.fill.themeShade}"`);
|
|
66
|
+
if (shading.fill?.themeColor) attrs.push(`w:themeFill="${escapeXml(shading.fill.themeColor)}"`);
|
|
67
|
+
if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${escapeXml(shading.fill.themeTint)}"`);
|
|
68
|
+
if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${escapeXml(shading.fill.themeShade)}"`);
|
|
67
69
|
if (attrs.length === 0) return "";
|
|
68
70
|
return `<w:shd ${attrs.join(" ")}/>`;
|
|
69
71
|
}
|
|
@@ -137,10 +139,10 @@ function serializeTextFormatting(formatting) {
|
|
|
137
139
|
if (formatting.underline) {
|
|
138
140
|
const uAttrs = [`w:val="${formatting.underline.style}"`];
|
|
139
141
|
if (formatting.underline.color) {
|
|
140
|
-
if (formatting.underline.color.rgb) uAttrs.push(`w:color="${formatting.underline.color.rgb}"`);
|
|
141
|
-
if (formatting.underline.color.themeColor) uAttrs.push(`w:themeColor="${formatting.underline.color.themeColor}"`);
|
|
142
|
-
if (formatting.underline.color.themeTint) uAttrs.push(`w:themeTint="${formatting.underline.color.themeTint}"`);
|
|
143
|
-
if (formatting.underline.color.themeShade) uAttrs.push(`w:themeShade="${formatting.underline.color.themeShade}"`);
|
|
142
|
+
if (formatting.underline.color.rgb && isValidHexColor(formatting.underline.color.rgb)) uAttrs.push(`w:color="${escapeXml(formatting.underline.color.rgb)}"`);
|
|
143
|
+
if (formatting.underline.color.themeColor) uAttrs.push(`w:themeColor="${escapeXml(formatting.underline.color.themeColor)}"`);
|
|
144
|
+
if (formatting.underline.color.themeTint) uAttrs.push(`w:themeTint="${escapeXml(formatting.underline.color.themeTint)}"`);
|
|
145
|
+
if (formatting.underline.color.themeShade) uAttrs.push(`w:themeShade="${escapeXml(formatting.underline.color.themeShade)}"`);
|
|
144
146
|
}
|
|
145
147
|
parts.push(`<w:u ${uAttrs.join(" ")}/>`);
|
|
146
148
|
}
|
|
@@ -160,7 +162,7 @@ function extractRPrInner(rPrXml) {
|
|
|
160
162
|
return rPrXml.slice(7, -8);
|
|
161
163
|
}
|
|
162
164
|
function serializeRunPropertyChange(change) {
|
|
163
|
-
const normalizedId =
|
|
165
|
+
const normalizedId = normalizeRevisionId(change.info.id);
|
|
164
166
|
const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
|
|
165
167
|
const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
|
|
166
168
|
const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
|
|
@@ -246,11 +248,11 @@ function serializeNoBreakHyphen(_content) {
|
|
|
246
248
|
/** Serialize a color value to DrawingML a:srgbClr or a:schemeClr */
|
|
247
249
|
function serializeDrawingColor(color) {
|
|
248
250
|
if (!color) return "";
|
|
249
|
-
if (color.rgb) return `<a:srgbClr val="${color.rgb.replace("#", "")}"/>`;
|
|
251
|
+
if (color.rgb && isValidHexColor(color.rgb)) return `<a:srgbClr val="${escapeXml(color.rgb.replace("#", ""))}"/>`;
|
|
250
252
|
if (color.themeColor) {
|
|
251
|
-
let clr = `<a:schemeClr val="${color.themeColor}"`;
|
|
252
|
-
if (color.themeTint) clr += `><a:tint val="${color.themeTint}"/></a:schemeClr>`;
|
|
253
|
-
else if (color.themeShade) clr += `><a:shade val="${color.themeShade}"/></a:schemeClr>`;
|
|
253
|
+
let clr = `<a:schemeClr val="${escapeXml(color.themeColor)}"`;
|
|
254
|
+
if (color.themeTint) clr += `><a:tint val="${escapeXml(color.themeTint)}"/></a:schemeClr>`;
|
|
255
|
+
else if (color.themeShade) clr += `><a:shade val="${escapeXml(color.themeShade)}"/></a:schemeClr>`;
|
|
254
256
|
else clr += `/>`;
|
|
255
257
|
return clr;
|
|
256
258
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getUnserializedSectionPropertyChildNames } from "../sectionParser.js";
|
|
2
2
|
import { escapeXml, intAttr } from "./xmlUtils.js";
|
|
3
3
|
import { serializeBorder } from "./borderSerializer.js";
|
|
4
|
+
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
4
5
|
//#region src/docx/serializer/sectionPropertiesSerializer.ts
|
|
5
6
|
const serializeHeaderReference = (ref) => `<w:headerReference w:type="${ref.type}" r:id="${ref.rId}"/>`;
|
|
6
7
|
const serializeFooterReference = (ref) => `<w:footerReference w:type="${ref.type}" r:id="${ref.rId}"/>`;
|
|
@@ -132,7 +133,7 @@ function serializeOnOffElement(value, name) {
|
|
|
132
133
|
return value ? `<w:${name}/>` : `<w:${name} w:val="0"/>`;
|
|
133
134
|
}
|
|
134
135
|
function serializeSectionPropertyChange(change) {
|
|
135
|
-
const normalizedId =
|
|
136
|
+
const normalizedId = normalizeRevisionId(change.info.id);
|
|
136
137
|
const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
|
|
137
138
|
const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
|
|
138
139
|
const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { isValidHexColor } from "../../utils/colorResolver.js";
|
|
1
2
|
import { escapeXml, intAttr } from "./xmlUtils.js";
|
|
2
3
|
import { serializeBorder } from "./borderSerializer.js";
|
|
4
|
+
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
3
5
|
//#region src/docx/serializer/tableSerializer.ts
|
|
4
6
|
function normalizeTrackedChangeInfo(info) {
|
|
5
|
-
const normalizedId =
|
|
7
|
+
const normalizedId = normalizeRevisionId(info.id);
|
|
6
8
|
const authorCandidate = typeof info.author === "string" ? info.author.trim() : "";
|
|
7
9
|
const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
|
|
8
10
|
const normalizedDate = typeof info.date === "string" ? info.date.trim() : void 0;
|
|
@@ -77,15 +79,15 @@ function serializeCellMargins(margins, elementName) {
|
|
|
77
79
|
function serializeShading(shading) {
|
|
78
80
|
if (!shading) return "";
|
|
79
81
|
const attrs = [];
|
|
80
|
-
if (shading.pattern) attrs.push(`w:val="${shading.pattern}"`);
|
|
82
|
+
if (shading.pattern) attrs.push(`w:val="${escapeXml(shading.pattern)}"`);
|
|
81
83
|
else attrs.push("w:val=\"clear\"");
|
|
82
|
-
if (shading.color?.rgb) attrs.push(`w:color="${shading.color.rgb}"`);
|
|
84
|
+
if (shading.color?.rgb && isValidHexColor(shading.color.rgb)) attrs.push(`w:color="${escapeXml(shading.color.rgb)}"`);
|
|
83
85
|
else if (shading.color?.auto) attrs.push("w:color=\"auto\"");
|
|
84
|
-
if (shading.fill?.rgb) attrs.push(`w:fill="${shading.fill.rgb}"`);
|
|
86
|
+
if (shading.fill?.rgb && isValidHexColor(shading.fill.rgb)) attrs.push(`w:fill="${escapeXml(shading.fill.rgb)}"`);
|
|
85
87
|
else if (shading.fill?.auto) attrs.push("w:fill=\"auto\"");
|
|
86
|
-
if (shading.fill?.themeColor) attrs.push(`w:themeFill="${shading.fill.themeColor}"`);
|
|
87
|
-
if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${shading.fill.themeTint}"`);
|
|
88
|
-
if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${shading.fill.themeShade}"`);
|
|
88
|
+
if (shading.fill?.themeColor) attrs.push(`w:themeFill="${escapeXml(shading.fill.themeColor)}"`);
|
|
89
|
+
if (shading.fill?.themeTint) attrs.push(`w:themeFillTint="${escapeXml(shading.fill.themeTint)}"`);
|
|
90
|
+
if (shading.fill?.themeShade) attrs.push(`w:themeFillShade="${escapeXml(shading.fill.themeShade)}"`);
|
|
89
91
|
if (attrs.length === 0) return "";
|
|
90
92
|
return `<w:shd ${attrs.join(" ")}/>`;
|
|
91
93
|
}
|
|
@@ -16,5 +16,17 @@ declare function escapeXml(text: string): string;
|
|
|
16
16
|
* literal `"NaN"` or `"Infinity"` into the XML.
|
|
17
17
|
*/
|
|
18
18
|
declare function intAttr(value: number | undefined | null): string;
|
|
19
|
+
/**
|
|
20
|
+
* Verify that `xml` parses to exactly one root element named `expectedLocalName`
|
|
21
|
+
* (namespace prefix ignored). Used to gate raw XML snapshots — e.g. an SDT's
|
|
22
|
+
* `rawPropertiesXml`/`rawEndPropertiesXml` — before splicing them verbatim
|
|
23
|
+
* into a serialized document. Those snapshots are normally produced by our
|
|
24
|
+
* own parser, but they can also arrive from an untrusted surface (a
|
|
25
|
+
* programmatically constructed node, a collaboration payload); a value that
|
|
26
|
+
* is not a single well-formed `<w:sdtPr>`/`<w:sdtEndPr>` element could inject
|
|
27
|
+
* sibling markup or close the enclosing `<w:sdt>` early. Callers should fall
|
|
28
|
+
* back to a synthesized properties block when this returns `false`.
|
|
29
|
+
*/
|
|
30
|
+
declare function isSingleWellFormedElement(xml: string, expectedLocalName: string): boolean;
|
|
19
31
|
//#endregion
|
|
20
|
-
export { escapeXml, intAttr };
|
|
32
|
+
export { escapeXml, intAttr, isSingleWellFormedElement };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLocalName, parseXml } from "../xmlParser.js";
|
|
1
2
|
//#region src/docx/serializer/xmlUtils.ts
|
|
2
3
|
/**
|
|
3
4
|
* Shared XML utility functions for serializers.
|
|
@@ -21,5 +22,30 @@ function intAttr(value) {
|
|
|
21
22
|
if (value == null || !Number.isFinite(value)) return "0";
|
|
22
23
|
return String(Math.round(value));
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Verify that `xml` parses to exactly one root element named `expectedLocalName`
|
|
27
|
+
* (namespace prefix ignored). Used to gate raw XML snapshots — e.g. an SDT's
|
|
28
|
+
* `rawPropertiesXml`/`rawEndPropertiesXml` — before splicing them verbatim
|
|
29
|
+
* into a serialized document. Those snapshots are normally produced by our
|
|
30
|
+
* own parser, but they can also arrive from an untrusted surface (a
|
|
31
|
+
* programmatically constructed node, a collaboration payload); a value that
|
|
32
|
+
* is not a single well-formed `<w:sdtPr>`/`<w:sdtEndPr>` element could inject
|
|
33
|
+
* sibling markup or close the enclosing `<w:sdt>` early. Callers should fall
|
|
34
|
+
* back to a synthesized properties block when this returns `false`.
|
|
35
|
+
*/
|
|
36
|
+
function isSingleWellFormedElement(xml, expectedLocalName) {
|
|
37
|
+
const trimmed = xml.trim();
|
|
38
|
+
if (!trimmed) return false;
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = parseXml(trimmed);
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const roots = (parsed.elements ?? []).filter((element) => element.type === "element");
|
|
46
|
+
if (roots.length !== 1) return false;
|
|
47
|
+
const root = roots[0];
|
|
48
|
+
return root?.name !== void 0 && getLocalName(root.name) === expectedLocalName;
|
|
49
|
+
}
|
|
24
50
|
//#endregion
|
|
25
|
-
export { escapeXml, intAttr };
|
|
51
|
+
export { escapeXml, intAttr, isSingleWellFormedElement };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { MarkdownOptions } from "../../markdown/types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/docx/server/docxToMarkdown.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Convert a DOCX file to markdown in a single server-safe call.
|
|
6
|
+
*
|
|
7
|
+
* Composes {@link parseDocx} (bytes -> Document, with font preloading disabled
|
|
8
|
+
* so it never touches the DOM) and {@link toMarkdown} (Document -> markdown).
|
|
9
|
+
* This gives non-browser callers folio's full DOCX fidelity — headings,
|
|
10
|
+
* tables, lists, and content controls (`w:sdt`) at every position — without
|
|
11
|
+
* deep-importing internal subpaths or re-implementing an OOXML walker.
|
|
12
|
+
*/
|
|
13
|
+
declare function docxToMarkdown(input: ArrayBuffer | Uint8Array, opts?: MarkdownOptions): Promise<string>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { docxToMarkdown };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseDocx } from "../parser.js";
|
|
2
|
+
import { toMarkdown } from "../../markdown/index.js";
|
|
3
|
+
//#region src/docx/server/docxToMarkdown.ts
|
|
4
|
+
/**
|
|
5
|
+
* Convert a DOCX file to markdown in a single server-safe call.
|
|
6
|
+
*
|
|
7
|
+
* Composes {@link parseDocx} (bytes -> Document, with font preloading disabled
|
|
8
|
+
* so it never touches the DOM) and {@link toMarkdown} (Document -> markdown).
|
|
9
|
+
* This gives non-browser callers folio's full DOCX fidelity — headings,
|
|
10
|
+
* tables, lists, and content controls (`w:sdt`) at every position — without
|
|
11
|
+
* deep-importing internal subpaths or re-implementing an OOXML walker.
|
|
12
|
+
*/
|
|
13
|
+
async function docxToMarkdown(input, opts) {
|
|
14
|
+
return toMarkdown(await parseDocx(input, { preloadFonts: false }), opts);
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { docxToMarkdown };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { findAllDeep, findChild, findDeep, getAttributeAnyPrefix, getLocalName, getTextContent, parseXml } from "../xmlParser.js";
|
|
1
|
+
import { findAllDeep, findChild, findDeep, getAttribute, getAttributeAnyPrefix, getLocalName, getTextContent, parseXml } from "../xmlParser.js";
|
|
2
|
+
import { RELATIONSHIP_TYPES, parseRelationships } from "../relsParser.js";
|
|
2
3
|
import { loadDocxArchive } from "./boundedArchive.js";
|
|
3
4
|
//#region src/docx/server/extractDocxText.ts
|
|
4
|
-
const
|
|
5
|
+
const DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
|
|
5
6
|
const childElements = (element) => element.elements?.filter((child) => child.type === "element") ?? [];
|
|
6
7
|
const collectText = (element) => {
|
|
7
8
|
let text = "";
|
|
@@ -86,12 +87,10 @@ const extractContainer = ({ container, source, startIndex }) => {
|
|
|
86
87
|
charCount
|
|
87
88
|
};
|
|
88
89
|
};
|
|
89
|
-
const extractParts = async ({ archive, source, rootName, startIndex }) => {
|
|
90
|
+
const extractParts = async ({ archive, source, rootName, startIndex, paths }) => {
|
|
90
91
|
const paragraphs = [];
|
|
91
92
|
let charCount = 0;
|
|
92
93
|
let nextIndex = startIndex;
|
|
93
|
-
const prefix = `word/${source}`;
|
|
94
|
-
const paths = archive.entries.filter((path) => HEADER_FOOTER_PATH.test(path) && path.startsWith(prefix)).toSorted();
|
|
95
94
|
for (const path of paths) {
|
|
96
95
|
const xml = await archive.readEntryString(path);
|
|
97
96
|
if (xml === null) continue;
|
|
@@ -111,6 +110,50 @@ const extractParts = async ({ archive, source, rootName, startIndex }) => {
|
|
|
111
110
|
charCount
|
|
112
111
|
};
|
|
113
112
|
};
|
|
113
|
+
/** A `word/_rels/document.xml.rels` `Target` is relative to `word/`; resolve it to a full archive-entry path. */
|
|
114
|
+
const resolveWordPartPath = (target) => target.startsWith("/") ? target.slice(1) : `word/${target}`;
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the header/footer parts actually wired into the document via
|
|
117
|
+
* `word/_rels/document.xml.rels` + each section's `w:headerReference` /
|
|
118
|
+
* `w:footerReference`, instead of extracting every `word/header*.xml` /
|
|
119
|
+
* `word/footer*.xml` entry by filename. A DOCX can carry an orphaned
|
|
120
|
+
* header/footer part (stale, or planted by an attacker) that no section
|
|
121
|
+
* references — reading it unconditionally would surface prompt-injection or
|
|
122
|
+
* stale content that Word itself never renders.
|
|
123
|
+
*/
|
|
124
|
+
const resolveReferencedHeaderFooterParts = async (archive, documentRoot) => {
|
|
125
|
+
const relsXml = await archive.readEntryString(DOCUMENT_RELS_PATH);
|
|
126
|
+
if (relsXml === null) return {
|
|
127
|
+
headers: [],
|
|
128
|
+
footers: []
|
|
129
|
+
};
|
|
130
|
+
const relationships = parseRelationships(relsXml);
|
|
131
|
+
const headerRIds = /* @__PURE__ */ new Set();
|
|
132
|
+
const footerRIds = /* @__PURE__ */ new Set();
|
|
133
|
+
for (const sectPr of findAllDeep(documentRoot, "w", "sectPr")) {
|
|
134
|
+
for (const ref of findAllDeep(sectPr, "w", "headerReference")) {
|
|
135
|
+
const rId = getAttribute(ref, "r", "id");
|
|
136
|
+
if (rId !== null) headerRIds.add(rId);
|
|
137
|
+
}
|
|
138
|
+
for (const ref of findAllDeep(sectPr, "w", "footerReference")) {
|
|
139
|
+
const rId = getAttribute(ref, "r", "id");
|
|
140
|
+
if (rId !== null) footerRIds.add(rId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const resolvePaths = (rIds, relationshipType) => {
|
|
144
|
+
const paths = /* @__PURE__ */ new Set();
|
|
145
|
+
for (const rId of rIds) {
|
|
146
|
+
const relationship = relationships.get(rId);
|
|
147
|
+
if (!relationship || relationship.type !== relationshipType || relationship.targetMode === "External") continue;
|
|
148
|
+
paths.add(resolveWordPartPath(relationship.target));
|
|
149
|
+
}
|
|
150
|
+
return [...paths].toSorted();
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
headers: resolvePaths(headerRIds, RELATIONSHIP_TYPES.header),
|
|
154
|
+
footers: resolvePaths(footerRIds, RELATIONSHIP_TYPES.footer)
|
|
155
|
+
};
|
|
156
|
+
};
|
|
114
157
|
const createEmptyResult = () => ({
|
|
115
158
|
paragraphs: [],
|
|
116
159
|
charCount: 0,
|
|
@@ -121,13 +164,16 @@ const extractDocxText = async (bytes) => {
|
|
|
121
164
|
const archive = await loadDocxArchive(bytes);
|
|
122
165
|
const documentXml = await archive.readEntryString("word/document.xml");
|
|
123
166
|
if (documentXml === null) return createEmptyResult();
|
|
124
|
-
const
|
|
167
|
+
const root = parseXml(documentXml);
|
|
168
|
+
const body = findDeep(root, "w", "body");
|
|
125
169
|
if (!body) return createEmptyResult();
|
|
170
|
+
const referencedParts = await resolveReferencedHeaderFooterParts(archive, root);
|
|
126
171
|
const headers = await extractParts({
|
|
127
172
|
archive,
|
|
128
173
|
source: "header",
|
|
129
174
|
rootName: "hdr",
|
|
130
|
-
startIndex: 0
|
|
175
|
+
startIndex: 0,
|
|
176
|
+
paths: referencedParts.headers
|
|
131
177
|
});
|
|
132
178
|
const bodyResult = extractContainer({
|
|
133
179
|
container: body,
|
|
@@ -138,7 +184,8 @@ const extractDocxText = async (bytes) => {
|
|
|
138
184
|
archive,
|
|
139
185
|
source: "footer",
|
|
140
186
|
rootName: "ftr",
|
|
141
|
-
startIndex: headers.paragraphs.length + bodyResult.paragraphs.length
|
|
187
|
+
startIndex: headers.paragraphs.length + bodyResult.paragraphs.length,
|
|
188
|
+
paths: referencedParts.footers
|
|
142
189
|
});
|
|
143
190
|
return {
|
|
144
191
|
paragraphs: [
|
|
@@ -10,6 +10,19 @@ const DEFAULT_TAB_STOP_TWIPS = 720;
|
|
|
10
10
|
const MAX_TAB_STOP_TWIPS = 31680;
|
|
11
11
|
const MAX_HYPHENATION_ZONE_TWIPS = 31680;
|
|
12
12
|
const MAX_CONSECUTIVE_HYPHEN_LIMIT = 255;
|
|
13
|
+
/**
|
|
14
|
+
* Floor on `w:defaultTabStop`, in twips (1/12 inch). A near-zero value would
|
|
15
|
+
* force the layout engine to generate thousands of tab stops across a
|
|
16
|
+
* single line; below this floor we substitute the OOXML default instead.
|
|
17
|
+
*/
|
|
18
|
+
const MIN_TAB_STOP_TWIPS = 120;
|
|
19
|
+
/**
|
|
20
|
+
* Sanity cap on parsed `w:noLineBreaksBefore`/`w:noLineBreaksAfter`
|
|
21
|
+
* character lists. Typical kinsoku overrides list a handful of prohibited
|
|
22
|
+
* characters; anything past this is corruption or a hostile input crafted
|
|
23
|
+
* to inflate the per-character membership set built at measure time.
|
|
24
|
+
*/
|
|
25
|
+
const MAX_KINSOKU_CHARACTERS_LENGTH = 128;
|
|
13
26
|
function parseSettings(xml) {
|
|
14
27
|
const root = xml ? parseXmlDocument(xml) : null;
|
|
15
28
|
const settings = { defaultTabStop: parseDefaultTabStop(root) };
|
|
@@ -76,7 +89,7 @@ function parseKinsokuOverride(root, name) {
|
|
|
76
89
|
if (!characters) return;
|
|
77
90
|
const language = getAttribute(element, "w", "lang") || void 0;
|
|
78
91
|
return {
|
|
79
|
-
characters,
|
|
92
|
+
characters: Array.from(characters).slice(0, MAX_KINSOKU_CHARACTERS_LENGTH).join(""),
|
|
80
93
|
...language ? { language } : {}
|
|
81
94
|
};
|
|
82
95
|
}
|
|
@@ -87,7 +100,7 @@ function parseDefaultTabStop(root) {
|
|
|
87
100
|
const raw = getAttribute(el, "w", "val");
|
|
88
101
|
if (raw === null) return 720;
|
|
89
102
|
const parsed = Number.parseInt(raw, 10);
|
|
90
|
-
if (!Number.isFinite(parsed) || parsed
|
|
103
|
+
if (!Number.isFinite(parsed) || parsed < MIN_TAB_STOP_TWIPS || parsed > MAX_TAB_STOP_TWIPS) return 720;
|
|
91
104
|
return parsed;
|
|
92
105
|
}
|
|
93
106
|
//#endregion
|
|
@@ -4,6 +4,13 @@ import { StyleMap } from "./styleParser.js";
|
|
|
4
4
|
import { XmlElement } from "./xmlParser.js";
|
|
5
5
|
|
|
6
6
|
//#region src/docx/tableParser.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Sanity cap on `w:gridSpan` (and the derived table column count). Word's
|
|
9
|
+
* practical column limit is 63; a hostile/corrupt value here would blow up
|
|
10
|
+
* every downstream structure sized by column count (TableMap, border grids,
|
|
11
|
+
* cell-grid arrays).
|
|
12
|
+
*/
|
|
13
|
+
declare const MAX_TABLE_COLUMNS = 63;
|
|
7
14
|
/**
|
|
8
15
|
* Parse a table measurement (width, height, etc.)
|
|
9
16
|
*
|
|
@@ -199,4 +206,4 @@ declare function getHeaderRows(table: document_d_exports.Table): document_d_expo
|
|
|
199
206
|
*/
|
|
200
207
|
declare function isFloatingTable(table: document_d_exports.Table): boolean;
|
|
201
208
|
//#endregion
|
|
202
|
-
export { getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, parseBorderSpec, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseShading, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTableRow, parseTableRowProperties };
|
|
209
|
+
export { MAX_TABLE_COLUMNS, getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, parseBorderSpec, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseShading, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTableRow, parseTableRowProperties };
|
package/dist/docx/tableParser.js
CHANGED
|
@@ -4,8 +4,16 @@ import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
|
|
|
4
4
|
import { parseParagraph } from "./paragraphParser.js";
|
|
5
5
|
import { appendBookmarkMarkerToLastParagraphInBlocks, appendBookmarkMarkerToLastParagraphInCells, prependBookmarkMarkersToFirstParagraphInBlocks, prependBookmarkMarkersToFirstParagraphInCell } from "./bookmarkPlacement.js";
|
|
6
6
|
import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
|
|
7
|
+
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
7
8
|
//#region src/docx/tableParser.ts
|
|
8
9
|
/**
|
|
10
|
+
* Sanity cap on `w:gridSpan` (and the derived table column count). Word's
|
|
11
|
+
* practical column limit is 63; a hostile/corrupt value here would blow up
|
|
12
|
+
* every downstream structure sized by column count (TableMap, border grids,
|
|
13
|
+
* cell-grid arrays).
|
|
14
|
+
*/
|
|
15
|
+
const MAX_TABLE_COLUMNS = 63;
|
|
16
|
+
/**
|
|
9
17
|
* Parse a table measurement (width, height, etc.)
|
|
10
18
|
*
|
|
11
19
|
* @param element - Element with w:w and w:type attributes
|
|
@@ -32,11 +40,13 @@ function parseTrackedChangeInfo(node) {
|
|
|
32
40
|
const parsedId = rawId ? Number.parseInt(rawId, 10) : 0;
|
|
33
41
|
const author = (getAttribute(node, "w", "author") ?? "").trim();
|
|
34
42
|
const date = (getAttribute(node, "w", "date") ?? "").trim();
|
|
43
|
+
const initials = (getAttribute(node, "w", "initials") ?? "").trim();
|
|
35
44
|
const info = {
|
|
36
|
-
id:
|
|
45
|
+
id: normalizeRevisionId(parsedId),
|
|
37
46
|
author: author.length > 0 ? author : "Unknown"
|
|
38
47
|
};
|
|
39
48
|
if (date.length > 0) info.date = date;
|
|
49
|
+
if (initials.length > 0) info.initials = initials;
|
|
40
50
|
return info;
|
|
41
51
|
}
|
|
42
52
|
function parsePropertyChangeInfo(node) {
|
|
@@ -485,7 +495,7 @@ function parseTableCellProperties(tcPrElement) {
|
|
|
485
495
|
const gridSpanElement = findChild(tcPrElement, "w", "gridSpan");
|
|
486
496
|
if (gridSpanElement) {
|
|
487
497
|
const gridSpan = parseNumericAttribute(gridSpanElement, "w", "val");
|
|
488
|
-
if (gridSpan !== void 0 && gridSpan > 1) formatting.gridSpan = gridSpan;
|
|
498
|
+
if (gridSpan !== void 0 && gridSpan > 1) formatting.gridSpan = Math.min(gridSpan, 63);
|
|
489
499
|
}
|
|
490
500
|
const vMergeElement = findChild(tcPrElement, "w", "vMerge");
|
|
491
501
|
if (vMergeElement) if (getAttribute(vMergeElement, "w", "val") === "restart") formatting.vMerge = "restart";
|
|
@@ -514,23 +524,34 @@ function parseCellContent(tcElement, styles, theme, numbering, rels, media, opti
|
|
|
514
524
|
const content = [];
|
|
515
525
|
const pendingBookmarkMarkers = [];
|
|
516
526
|
const elements = getChildElements(tcElement);
|
|
517
|
-
|
|
518
|
-
if (!child.name)
|
|
527
|
+
const parseCellChild = (child) => {
|
|
528
|
+
if (!child.name) return;
|
|
519
529
|
const localName = getLocalName(child.name);
|
|
520
530
|
if (localName === "p") {
|
|
521
531
|
const para = parseParagraph(child, styles, theme, numbering, rels, media, options);
|
|
522
532
|
enrichParagraphTextBoxes(para, child, styles, theme, numbering, rels, media, parseTable);
|
|
523
533
|
prependPendingBookmarkMarkers(para, pendingBookmarkMarkers);
|
|
524
534
|
content.push(para);
|
|
525
|
-
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (localName === "tbl") {
|
|
526
538
|
const table = parseTable(child, styles, theme, numbering, rels, media, options);
|
|
527
539
|
if (prependBookmarkMarkersToFirstParagraphInBlocks([table], pendingBookmarkMarkers)) pendingBookmarkMarkers.length = 0;
|
|
528
540
|
content.push(table);
|
|
529
|
-
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (localName === "sdt") {
|
|
544
|
+
const sdtContent = findChildByLocalName(child, "sdtContent");
|
|
545
|
+
if (!sdtContent) return;
|
|
546
|
+
for (const sdtChild of getChildElements(sdtContent)) parseCellChild(sdtChild);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (localName === "bookmarkStart" || localName === "bookmarkEnd") {
|
|
530
550
|
const marker = parseBookmarkMarker(child, localName);
|
|
531
551
|
if (!appendBookmarkMarkerToLastParagraphInBlocks(content, marker)) pendingBookmarkMarkers.push(marker);
|
|
532
552
|
}
|
|
533
|
-
}
|
|
553
|
+
};
|
|
554
|
+
for (const child of elements) parseCellChild(child);
|
|
534
555
|
if (content.length === 0) content.push({
|
|
535
556
|
type: "paragraph",
|
|
536
557
|
content: [...pendingBookmarkMarkers]
|
|
@@ -838,4 +859,4 @@ function isFloatingTable(table) {
|
|
|
838
859
|
return table.formatting?.floating !== void 0;
|
|
839
860
|
}
|
|
840
861
|
//#endregion
|
|
841
|
-
export { getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, parseBorderSpec, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseShading, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTableRow, parseTableRowProperties };
|
|
862
|
+
export { MAX_TABLE_COLUMNS, getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, parseBorderSpec, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseShading, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTableRow, parseTableRowProperties };
|
package/dist/docx/unzip.js
CHANGED
|
@@ -261,6 +261,7 @@ function isPreservableDocxEntry(path) {
|
|
|
261
261
|
if (!isSafeDocxPath(path)) return false;
|
|
262
262
|
const lowerPath = path.toLowerCase();
|
|
263
263
|
if (lowerPath.startsWith("word/media/")) return PRESERVABLE_MEDIA_MIME_TYPES.has(getMediaMimeType(path));
|
|
264
|
+
if (lowerPath.startsWith("docprops/thumbnail.")) return PRESERVABLE_MEDIA_MIME_TYPES.has(getMediaMimeType(path));
|
|
264
265
|
if (lowerPath.startsWith("word/fonts/")) return true;
|
|
265
266
|
if (!(lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels"))) return false;
|
|
266
267
|
return lowerPath === "[content_types].xml" || lowerPath.startsWith("_rels/") || lowerPath.startsWith("docprops/") || lowerPath.startsWith("word/") || lowerPath.startsWith("customxml/");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cloneWithXmlnsDeclarations, elementToXml, findAllDeep, findChild, getAttribute, getChildElements, getLocalName } from "./xmlParser.js";
|
|
2
2
|
import { pixelsToEmu } from "../utils/units.js";
|
|
3
|
+
import { sanitizeImageSrc } from "../utils/sanitizeImageSrc.js";
|
|
3
4
|
import { resolveImageData } from "./imageParser.js";
|
|
4
5
|
import { isWatermarkShape } from "./watermarkParser.js";
|
|
5
6
|
//#region src/docx/vmlImageParser.ts
|
|
@@ -49,16 +50,22 @@ function cssLengthToPx(raw) {
|
|
|
49
50
|
default: return;
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
54
|
+
"__proto__",
|
|
55
|
+
"constructor",
|
|
56
|
+
"prototype"
|
|
57
|
+
]);
|
|
52
58
|
/** Read a `style="k1:v1;k2:v2"` attribute into a lowercased key/value record. */
|
|
53
59
|
function parseStyleAttr(style) {
|
|
54
|
-
const out =
|
|
60
|
+
const out = Object.create(null);
|
|
55
61
|
if (!style) return out;
|
|
56
62
|
for (const decl of style.split(";")) {
|
|
57
63
|
const colon = decl.indexOf(":");
|
|
58
64
|
if (colon < 0) continue;
|
|
59
65
|
const key = decl.slice(0, colon).trim().toLowerCase();
|
|
60
66
|
const value = decl.slice(colon + 1).trim();
|
|
61
|
-
if (key
|
|
67
|
+
if (!key || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
68
|
+
out[key] = value;
|
|
62
69
|
}
|
|
63
70
|
return out;
|
|
64
71
|
}
|
|
@@ -216,7 +223,8 @@ function parseVmlImageContent(pictElement, rels, media, rootXmlns = {}) {
|
|
|
216
223
|
},
|
|
217
224
|
wrap: { type: "inline" }
|
|
218
225
|
};
|
|
219
|
-
|
|
226
|
+
const safeSrc = sanitizeImageSrc(src);
|
|
227
|
+
if (safeSrc) image.src = safeSrc;
|
|
220
228
|
if (mimeType) image.mimeType = mimeType;
|
|
221
229
|
if (filename) image.filename = filename;
|
|
222
230
|
const title = getAttribute(imagedata, "o", "title");
|