@stll/folio-core 0.44.0 → 0.45.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/__fixtures__/paragraphs.js +2 -2
- package/dist/ai-edits/headless.js +1 -0
- package/dist/compare/content-alignment.js +78 -53
- package/dist/compare/inline-atoms.js +34 -20
- package/dist/content-controls/mutateContentControls.js +4 -2
- package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
- package/dist/document-operations.js +14 -3
- package/dist/docx/borderParser.js +5 -5
- package/dist/docx/commentParser.js +47 -36
- package/dist/docx/commentThreadKey.d.ts +18 -0
- package/dist/docx/commentThreadKey.js +22 -0
- package/dist/docx/diagramPreview.js +87 -27
- package/dist/docx/groupDrawingParser.js +3 -3
- package/dist/docx/hyperlinkParser.js +2 -2
- package/dist/docx/imageParser.d.ts +9 -1
- package/dist/docx/imageParser.js +58 -12
- package/dist/docx/imageRawXml.d.ts +14 -1
- package/dist/docx/imageRawXml.js +30 -6
- package/dist/docx/mathToMathml.js +12 -14
- package/dist/docx/nonVisualDrawingProps.d.ts +34 -0
- package/dist/docx/nonVisualDrawingProps.js +46 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +3 -0
- package/dist/docx/parser.js +2 -2
- package/dist/docx/previewBudget.d.ts +64 -0
- package/dist/docx/previewBudget.js +88 -0
- package/dist/docx/revisionIdNormalization.js +17 -5
- package/dist/docx/rezip.js +17 -18
- package/dist/docx/sdtPropertiesPatch.js +24 -18
- package/dist/docx/sectionReferenceHistory.js +2 -2
- package/dist/docx/selectiveSave.js +6 -6
- package/dist/docx/serializer/blockSdtSerializer.js +38 -26
- package/dist/docx/serializer/borderSerializer.d.ts +1 -1
- package/dist/docx/serializer/borderSerializer.js +13 -12
- package/dist/docx/serializer/commentSerializer.d.ts +41 -16
- package/dist/docx/serializer/commentSerializer.js +82 -85
- package/dist/docx/serializer/fontTableSerializer.js +6 -6
- package/dist/docx/serializer/headerFooterSerializer.js +5 -5
- package/dist/docx/serializer/markupRangeAttributes.js +2 -2
- package/dist/docx/serializer/numberingSerializer.js +7 -6
- package/dist/docx/serializer/paragraphSerializer.js +19 -18
- package/dist/docx/serializer/partNamespaces.js +2 -2
- package/dist/docx/serializer/runSerializer.js +48 -28
- package/dist/docx/serializer/sectionPropertiesSerializer.js +11 -10
- package/dist/docx/serializer/settingsSerializer.js +4 -3
- package/dist/docx/serializer/stylesSerializer.js +6 -6
- package/dist/docx/serializer/tableSerializer.js +10 -9
- package/dist/docx/serializer/textFormattingSerializer.js +29 -28
- package/dist/docx/serializer/themeSerializer.js +6 -6
- package/dist/docx/serializer/trackedChangeAttributes.js +2 -2
- package/dist/docx/serializer/xmlUtils.d.ts +1 -2
- package/dist/docx/serializer/xmlUtils.js +1 -13
- package/dist/docx/server/boundedArchive.d.ts +12 -0
- package/dist/docx/server/boundedArchive.js +20 -1
- package/dist/docx/server/validateDocxConformance.js +22 -1
- package/dist/docx/shapeParser.js +7 -5
- package/dist/docx/textBoxParser.js +7 -2
- package/dist/docx/unzip.d.ts +23 -0
- package/dist/docx/unzip.js +32 -22
- package/dist/docx/verbatimCapture.js +1 -1
- package/dist/docx/vmlImageParser.js +3 -2
- package/dist/docx/vmlPreview.d.ts +1 -3
- package/dist/docx/vmlPreview.js +2 -30
- package/dist/docx/xmlParser.d.ts +16 -1
- package/dist/docx/xmlParser.js +56 -26
- package/dist/docx/xmlResourceLimits.d.ts +89 -9
- package/dist/docx/xmlResourceLimits.js +105 -24
- package/dist/internal/paragraphFormattingSerialization.js +3 -2
- package/dist/layout-painter/renderImage.js +4 -3
- package/dist/layout-painter/renderParagraph.js +4 -3
- package/dist/managers/autoSaveCodec.js +2 -8
- package/dist/markdown/images.js +1 -4
- package/dist/prosemirror/attrs/index.js +69 -0
- package/dist/prosemirror/commands/image.js +1 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +69 -29
- package/dist/prosemirror/conversion/toProseDoc.js +83 -19
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +2 -3
- package/dist/prosemirror/extensions/nodes/ImageExtension.js +4 -0
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +7 -2
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +8 -4
- package/dist/prosemirror/paragraphFormattingProvenance.d.ts +6 -3
- package/dist/prosemirror/paragraphFormattingProvenance.js +12 -3
- package/dist/prosemirror/schema/nodes.d.ts +50 -1
- package/dist/utils/base64.d.ts +36 -0
- package/dist/utils/base64.js +40 -0
- package/dist/utils/clipboard.js +2 -1
- package/dist/utils/units.d.ts +10 -1
- package/dist/utils/units.js +12 -1
- package/dist/utils/urlSecurity.d.ts +8 -2
- package/dist/utils/urlSecurity.js +21 -3
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { findChildByLocalName, getLocalName } from "./xmlParser.js";
|
|
2
|
+
import { escapeXmlText } from "@stll/docx-core";
|
|
2
3
|
//#region src/docx/mathToMathml.ts
|
|
3
4
|
/**
|
|
4
5
|
* OMML → MathML conversion.
|
|
@@ -95,13 +96,13 @@ const MATH_OPERATOR_HINT = /* @__PURE__ */ new Set([
|
|
|
95
96
|
"…"
|
|
96
97
|
]);
|
|
97
98
|
function renderElement(el) {
|
|
98
|
-
if (el.type === "text") return
|
|
99
|
+
if (el.type === "text") return escapeXmlText(textValue(el));
|
|
99
100
|
if (el.type !== "element") return "";
|
|
100
101
|
switch (el.name ? getLocalName(el.name) : "") {
|
|
101
102
|
case "oMath":
|
|
102
103
|
case "oMathPara": return renderChildren(el);
|
|
103
104
|
case "r": return renderRun(el);
|
|
104
|
-
case "t": return
|
|
105
|
+
case "t": return escapeXmlText(extractText(el));
|
|
105
106
|
case "f": return renderFraction(el);
|
|
106
107
|
case "num":
|
|
107
108
|
case "den": return wrapMrow(renderChildren(el));
|
|
@@ -179,8 +180,8 @@ function tokenizeMathText(text) {
|
|
|
179
180
|
let bufKind = null;
|
|
180
181
|
const flush = () => {
|
|
181
182
|
if (!buf) return;
|
|
182
|
-
if (bufKind === "digit") out += `<mn>${
|
|
183
|
-
else out += `<mi>${
|
|
183
|
+
if (bufKind === "digit") out += `<mn>${escapeXmlText(buf)}</mn>`;
|
|
184
|
+
else out += `<mi>${escapeXmlText(buf)}</mi>`;
|
|
184
185
|
buf = "";
|
|
185
186
|
bufKind = null;
|
|
186
187
|
};
|
|
@@ -193,7 +194,7 @@ function tokenizeMathText(text) {
|
|
|
193
194
|
const kind = classifyChar(ch);
|
|
194
195
|
if (kind === "operator") {
|
|
195
196
|
flush();
|
|
196
|
-
out += `<mo>${
|
|
197
|
+
out += `<mo>${escapeXmlText(ch)}</mo>`;
|
|
197
198
|
continue;
|
|
198
199
|
}
|
|
199
200
|
if (kind === "digit") {
|
|
@@ -202,7 +203,7 @@ function tokenizeMathText(text) {
|
|
|
202
203
|
continue;
|
|
203
204
|
}
|
|
204
205
|
flush();
|
|
205
|
-
out += `<mi>${
|
|
206
|
+
out += `<mi>${escapeXmlText(ch)}</mi>`;
|
|
206
207
|
}
|
|
207
208
|
flush();
|
|
208
209
|
return out;
|
|
@@ -258,7 +259,7 @@ function renderNary(el) {
|
|
|
258
259
|
const chr = (chrEl ? getAttrValue(chrEl, "val") : null) || "∫";
|
|
259
260
|
const limLocEl = naryPr ? findChildByLocalName(naryPr, "limLoc") : null;
|
|
260
261
|
const limLoc = (limLocEl ? getAttrValue(limLocEl, "val") : null) || "subSup";
|
|
261
|
-
const opMml = `<mo>${
|
|
262
|
+
const opMml = `<mo>${escapeXmlText(chr)}</mo>`;
|
|
262
263
|
const subMml = sub ? wrapMrow(renderChildren(sub)) : "";
|
|
263
264
|
const supMml = sup ? wrapMrow(renderChildren(sup)) : "";
|
|
264
265
|
const bodyMml = body ? wrapMrow(renderChildren(body)) : "<mrow/>";
|
|
@@ -277,8 +278,8 @@ function renderDelimiter(el) {
|
|
|
277
278
|
const begChr = begChrEl ? getAttrValue(begChrEl, "val") ?? "(" : "(";
|
|
278
279
|
const endChr = endChrEl ? getAttrValue(endChrEl, "val") ?? ")" : ")";
|
|
279
280
|
const sepChr = sepChrEl ? getAttrValue(sepChrEl, "val") ?? "|" : "|";
|
|
280
|
-
const inner = (el.elements ?? []).filter((c) => c.type === "element" && getLocalName(c.name ?? "") === "e").map((c) => wrapMrow(renderChildren(c))).join(`<mo>${
|
|
281
|
-
return `<mrow><mo>${
|
|
281
|
+
const inner = (el.elements ?? []).filter((c) => c.type === "element" && getLocalName(c.name ?? "") === "e").map((c) => wrapMrow(renderChildren(c))).join(`<mo>${escapeXmlText(sepChr)}</mo>`);
|
|
282
|
+
return `<mrow><mo>${escapeXmlText(begChr)}</mo>${inner}<mo>${escapeXmlText(endChr)}</mo></mrow>`;
|
|
282
283
|
}
|
|
283
284
|
function renderMatrix(el) {
|
|
284
285
|
return `<mtable>${(el.elements ?? []).filter((c) => c.type === "element" && getLocalName(c.name ?? "") === "mr").map(renderMatrixRow).join("")}</mtable>`;
|
|
@@ -291,7 +292,7 @@ function renderAccent(el) {
|
|
|
291
292
|
const chrEl = accPr ? findChildByLocalName(accPr, "chr") : null;
|
|
292
293
|
const chr = chrEl ? getAttrValue(chrEl, "val") ?? "̂" : "̂";
|
|
293
294
|
const base = findChildByLocalName(el, "e");
|
|
294
|
-
return `<mover accent="true">${base ? wrapMrow(renderChildren(base)) : "<mrow/>"}<mo>${
|
|
295
|
+
return `<mover accent="true">${base ? wrapMrow(renderChildren(base)) : "<mrow/>"}<mo>${escapeXmlText(chr)}</mo></mover>`;
|
|
295
296
|
}
|
|
296
297
|
function renderBar(el) {
|
|
297
298
|
const barPr = findChildByLocalName(el, "barPr");
|
|
@@ -311,7 +312,7 @@ function renderGroupChr(el) {
|
|
|
311
312
|
const chr = chrEl ? getAttrValue(chrEl, "val") ?? defaultChr : defaultChr;
|
|
312
313
|
const base = findChildByLocalName(el, "e");
|
|
313
314
|
const baseMml = base ? wrapMrow(renderChildren(base)) : "<mrow/>";
|
|
314
|
-
const grouper = `<mo stretchy="true">${
|
|
315
|
+
const grouper = `<mo stretchy="true">${escapeXmlText(chr)}</mo>`;
|
|
315
316
|
return pos === "top" ? `<mover>${baseMml}${grouper}</mover>` : `<munder>${baseMml}${grouper}</munder>`;
|
|
316
317
|
}
|
|
317
318
|
function renderLimLow(el) {
|
|
@@ -350,8 +351,5 @@ function getAttrValue(el, localAttr) {
|
|
|
350
351
|
return String(v);
|
|
351
352
|
}
|
|
352
353
|
}
|
|
353
|
-
function escapeXml(s) {
|
|
354
|
-
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
355
|
-
}
|
|
356
354
|
//#endregion
|
|
357
355
|
export { ommlToMathml };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { XmlElement } from "./xmlParser.js";
|
|
2
|
+
//#region src/docx/nonVisualDrawingProps.d.ts
|
|
3
|
+
/** The authored name, alt text and title of a drawing object. */
|
|
4
|
+
type NonVisualDrawingNames = {
|
|
5
|
+
/** `@name`, schema-required, so `""` means the object was never named. */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** `@descr`: alt text, accessibility content. */
|
|
8
|
+
alt?: string;
|
|
9
|
+
/** `@title`. */
|
|
10
|
+
title?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Read `@name`, `@descr` and `@title` off a `CT_NonVisualDrawingProps` element.
|
|
14
|
+
*
|
|
15
|
+
* An attribute that is not there stays absent. `@name` is schema-required, so
|
|
16
|
+
* an unnamed object still writes one, and `name=""` is the marker the writer
|
|
17
|
+
* below mints for exactly that: reading it back as an authored name would make
|
|
18
|
+
* `absent → save → parse` land on `""` instead of absent, and no reader can
|
|
19
|
+
* tell the two apart. `@descr` and `@title` are optional and written only when
|
|
20
|
+
* authored, so `""` in either is a string someone wrote.
|
|
21
|
+
*/
|
|
22
|
+
declare const parseNonVisualDrawingNames: (element: XmlElement | null | undefined) => NonVisualDrawingNames;
|
|
23
|
+
/**
|
|
24
|
+
* The `name`, `descr` and `title` attributes of a `CT_NonVisualDrawingProps`
|
|
25
|
+
* element, leading space included.
|
|
26
|
+
*
|
|
27
|
+
* `@name` is schema-required, so an object carrying none writes the empty
|
|
28
|
+
* string. folio does not name an object it did not author: a generated
|
|
29
|
+
* "Shape 3" would overwrite the author's own name for every drawing whose
|
|
30
|
+
* name did not survive the model, and read back as authored content.
|
|
31
|
+
*/
|
|
32
|
+
declare const serializeNonVisualDrawingNames: (names: NonVisualDrawingNames) => string;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { NonVisualDrawingNames, parseNonVisualDrawingNames, serializeNonVisualDrawingNames };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getAttribute } from "./xmlParser.js";
|
|
2
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
3
|
+
//#region src/docx/nonVisualDrawingProps.ts
|
|
4
|
+
/**
|
|
5
|
+
* `CT_NonVisualDrawingProps` (`wp:docPr`, `wps:cNvPr`, `pic:cNvPr`) carries the
|
|
6
|
+
* three authored strings every drawing object has: its name, its alt text
|
|
7
|
+
* (`descr`) and its title. They are authored content, not folio's to mint, so
|
|
8
|
+
* one reader and one writer own them for every drawing kind.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Read `@name`, `@descr` and `@title` off a `CT_NonVisualDrawingProps` element.
|
|
12
|
+
*
|
|
13
|
+
* An attribute that is not there stays absent. `@name` is schema-required, so
|
|
14
|
+
* an unnamed object still writes one, and `name=""` is the marker the writer
|
|
15
|
+
* below mints for exactly that: reading it back as an authored name would make
|
|
16
|
+
* `absent → save → parse` land on `""` instead of absent, and no reader can
|
|
17
|
+
* tell the two apart. `@descr` and `@title` are optional and written only when
|
|
18
|
+
* authored, so `""` in either is a string someone wrote.
|
|
19
|
+
*/
|
|
20
|
+
const parseNonVisualDrawingNames = (element) => {
|
|
21
|
+
if (!element) return {};
|
|
22
|
+
const name = getAttribute(element, null, "name");
|
|
23
|
+
const descr = getAttribute(element, null, "descr");
|
|
24
|
+
const title = getAttribute(element, null, "title");
|
|
25
|
+
return {
|
|
26
|
+
...name !== null && name !== "" ? { name } : {},
|
|
27
|
+
...descr !== null ? { alt: descr } : {},
|
|
28
|
+
...title !== null ? { title } : {}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The `name`, `descr` and `title` attributes of a `CT_NonVisualDrawingProps`
|
|
33
|
+
* element, leading space included.
|
|
34
|
+
*
|
|
35
|
+
* `@name` is schema-required, so an object carrying none writes the empty
|
|
36
|
+
* string. folio does not name an object it did not author: a generated
|
|
37
|
+
* "Shape 3" would overwrite the author's own name for every drawing whose
|
|
38
|
+
* name did not survive the model, and read back as authored content.
|
|
39
|
+
*/
|
|
40
|
+
const serializeNonVisualDrawingNames = (names) => {
|
|
41
|
+
const descr = names.alt === void 0 ? "" : ` descr="${escapeXmlAttribute(names.alt)}"`;
|
|
42
|
+
const title = names.title === void 0 ? "" : ` title="${escapeXmlAttribute(names.title)}"`;
|
|
43
|
+
return ` name="${escapeXmlAttribute(names.name ?? "")}"${descr}${title}`;
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { parseNonVisualDrawingNames, serializeNonVisualDrawingNames };
|
|
@@ -198,6 +198,9 @@ const enrichTextBoxRuns = ({ content, xmlChildren, styles, theme, numbering, rel
|
|
|
198
198
|
type: "shape",
|
|
199
199
|
shapeType: "textBox",
|
|
200
200
|
size: textBox.size,
|
|
201
|
+
...textBox.name !== void 0 ? { name: textBox.name } : {},
|
|
202
|
+
...textBox.alt !== void 0 ? { alt: textBox.alt } : {},
|
|
203
|
+
...textBox.title !== void 0 ? { title: textBox.title } : {},
|
|
201
204
|
...textBox.position !== void 0 ? { position: textBox.position } : {},
|
|
202
205
|
...textBox.wrap !== void 0 ? { wrap: textBox.wrap } : {},
|
|
203
206
|
...textBox.fill !== void 0 ? { fill: textBox.fill } : {},
|
package/dist/docx/parser.js
CHANGED
|
@@ -23,6 +23,7 @@ import { UNNUMBERED_PARAGRAPH_WARNING, UNNUMBERED_STYLE_WARNING, normalizeNumber
|
|
|
23
23
|
import { assignDocumentParagraphPropertySourceContract } from "./paragraphPropertySource.js";
|
|
24
24
|
import { createParseWarningCollector } from "./parseContext.js";
|
|
25
25
|
import { formatParseWarnings } from "./parseWarningMessage.js";
|
|
26
|
+
import { enforcePackagePreviewBudget } from "./previewBudget.js";
|
|
26
27
|
import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./relsParser.js";
|
|
27
28
|
import { normalizeRenderedPageBreakHints } from "./renderedPageBreakNormalization.js";
|
|
28
29
|
import { parseSettings } from "./settingsParser.js";
|
|
@@ -30,7 +31,6 @@ import { parseStylesPackage } from "./styleParser.js";
|
|
|
30
31
|
import { applyThemeFontLang, parseTheme } from "./themeParser.js";
|
|
31
32
|
import { UNBALANCED_MOVE_RANGE_WARNING, normalizeTrackedMoveRanges } from "./trackedMoveRangeNormalization.js";
|
|
32
33
|
import { getMediaMimeType, mediaToDataUrl, unzipDocx } from "./unzip.js";
|
|
33
|
-
import { enforcePackageVmlPreviewBudget } from "./vmlPreview.js";
|
|
34
34
|
import { FOLIO_XML_RESOURCE_LIMITS } from "./xmlResourceLimits.js";
|
|
35
35
|
import { TaggedError } from "better-result";
|
|
36
36
|
import { PARSE_WARNING_CODES } from "@stll/docx-core/model";
|
|
@@ -281,7 +281,7 @@ async function parseDocx(input, options = {}) {
|
|
|
281
281
|
...requiredFonts.length > 0 ? { requiredFonts } : {}
|
|
282
282
|
};
|
|
283
283
|
assignDocumentParagraphPropertySourceContract(document, await paragraphPropertySourceDigest);
|
|
284
|
-
|
|
284
|
+
enforcePackagePreviewBudget(document.package);
|
|
285
285
|
const validation = validateFolioDocumentModel(document);
|
|
286
286
|
const parsedCompleteModel = parseHeadersFooters && parseNotes;
|
|
287
287
|
if (!validation.valid && parsedCompleteModel) throw new DocxModelValidationError("Parsed DOCX produced an invalid document model", validation.issues);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/docx/previewBudget.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Every synthetic preview a parse attaches to the model, and the budget each
|
|
4
|
+
* one answers to.
|
|
5
|
+
*
|
|
6
|
+
* A preview is not document content. It is a drawing folio makes up so a shape
|
|
7
|
+
* it cannot project still occupies the page, and the package round-trips from
|
|
8
|
+
* its preserved XML whether the preview exists or not. So a preview is the one
|
|
9
|
+
* thing in the model that may be dropped, and a package that would retain more
|
|
10
|
+
* of it than it is worth has it dropped rather than being refused.
|
|
11
|
+
*
|
|
12
|
+
* Producer and budget have to agree on what a preview looks like. They used to
|
|
13
|
+
* agree by coincidence: the VML producer wrote its filename as a literal and
|
|
14
|
+
* the budget recognized it as a constant in another file, so renaming one
|
|
15
|
+
* would have stopped the other charging for it without anything failing. The
|
|
16
|
+
* table below is that agreement written once, and a producer builds its image
|
|
17
|
+
* out of the entry the budget matches against.
|
|
18
|
+
*/
|
|
19
|
+
declare const VML_PREVIEW_DATA_URL_PREFIX = "data:image/svg+xml;charset=utf-8,";
|
|
20
|
+
declare const PREVIEW_KINDS: {
|
|
21
|
+
/** A VML shape folio renders rather than projects (`v:shape`, `v:rect`, ...). */
|
|
22
|
+
readonly vmlShape: {
|
|
23
|
+
readonly mimeType: "image/svg+xml";
|
|
24
|
+
readonly filename: "vml-shape-preview.svg";
|
|
25
|
+
readonly srcPrefix: "data:image/svg+xml;charset=utf-8,";
|
|
26
|
+
readonly maxPackageCharacters: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A SmartArt diagram: its extent filled with one flat rectangle per shape.
|
|
30
|
+
*
|
|
31
|
+
* A raster rather than a vector because the display list decodes only base64
|
|
32
|
+
* PNG and JPEG, so a vector preview would be missing from every display-list
|
|
33
|
+
* backend (PDF among them) while still showing in the DOM. That is what
|
|
34
|
+
* makes this preview expensive: one of them is 7.3 MB of data URL whatever
|
|
35
|
+
* the package weighs, because its cost follows the extent the author chose
|
|
36
|
+
* rather than anything the drawing contains.
|
|
37
|
+
*
|
|
38
|
+
* Across the public corpus, the fifty packages that produce one retain a
|
|
39
|
+
* median of 7.3 MB and a maximum of 51.3 MB (ten previews, from a package
|
|
40
|
+
* under a megabyte). The cap is set above that maximum: it refuses no
|
|
41
|
+
* legitimate file in the corpus while bounding what had no bound at all, and
|
|
42
|
+
* it is a ceiling rather than a fix. The fix is for the preview to be a
|
|
43
|
+
* descriptor the renderer rasterizes, which needs the display-list contract
|
|
44
|
+
* to carry one.
|
|
45
|
+
*/
|
|
46
|
+
readonly smartArt: {
|
|
47
|
+
readonly mimeType: "image/png";
|
|
48
|
+
readonly filename: "smartart-preview.png";
|
|
49
|
+
readonly srcPrefix: "data:image/png;base64,";
|
|
50
|
+
readonly maxPackageCharacters: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
type PreviewKindName = keyof typeof PREVIEW_KINDS;
|
|
54
|
+
/** Per-kind allowances for one package, defaulting to the table's caps. */
|
|
55
|
+
type PreviewBudgetOverrides = Partial<Record<PreviewKindName, number>>;
|
|
56
|
+
/**
|
|
57
|
+
* Charge every generated preview in the model against its kind's allowance and
|
|
58
|
+
* drop the `src` of those past it. Dropping leaves the image in place with its
|
|
59
|
+
* size and wrap, so the page still reserves the space the drawing occupies,
|
|
60
|
+
* and never touches the preserved XML the package saves from.
|
|
61
|
+
*/
|
|
62
|
+
declare const enforcePackagePreviewBudget: (root: unknown, overrides?: PreviewBudgetOverrides) => void;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { PREVIEW_KINDS, PreviewBudgetOverrides, VML_PREVIEW_DATA_URL_PREFIX, enforcePackagePreviewBudget };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//#region src/docx/previewBudget.ts
|
|
2
|
+
const VML_PREVIEW_DATA_URL_PREFIX = "data:image/svg+xml;charset=utf-8,";
|
|
3
|
+
const MEBIBYTE = 1024 * 1024;
|
|
4
|
+
const PREVIEW_KINDS = {
|
|
5
|
+
/** A VML shape folio renders rather than projects (`v:shape`, `v:rect`, ...). */
|
|
6
|
+
vmlShape: {
|
|
7
|
+
mimeType: "image/svg+xml",
|
|
8
|
+
filename: "vml-shape-preview.svg",
|
|
9
|
+
srcPrefix: VML_PREVIEW_DATA_URL_PREFIX,
|
|
10
|
+
maxPackageCharacters: 8 * MEBIBYTE
|
|
11
|
+
},
|
|
12
|
+
/**
|
|
13
|
+
* A SmartArt diagram: its extent filled with one flat rectangle per shape.
|
|
14
|
+
*
|
|
15
|
+
* A raster rather than a vector because the display list decodes only base64
|
|
16
|
+
* PNG and JPEG, so a vector preview would be missing from every display-list
|
|
17
|
+
* backend (PDF among them) while still showing in the DOM. That is what
|
|
18
|
+
* makes this preview expensive: one of them is 7.3 MB of data URL whatever
|
|
19
|
+
* the package weighs, because its cost follows the extent the author chose
|
|
20
|
+
* rather than anything the drawing contains.
|
|
21
|
+
*
|
|
22
|
+
* Across the public corpus, the fifty packages that produce one retain a
|
|
23
|
+
* median of 7.3 MB and a maximum of 51.3 MB (ten previews, from a package
|
|
24
|
+
* under a megabyte). The cap is set above that maximum: it refuses no
|
|
25
|
+
* legitimate file in the corpus while bounding what had no bound at all, and
|
|
26
|
+
* it is a ceiling rather than a fix. The fix is for the preview to be a
|
|
27
|
+
* descriptor the renderer rasterizes, which needs the display-list contract
|
|
28
|
+
* to carry one.
|
|
29
|
+
*/
|
|
30
|
+
smartArt: {
|
|
31
|
+
mimeType: "image/png",
|
|
32
|
+
filename: "smartart-preview.png",
|
|
33
|
+
srcPrefix: "data:image/png;base64,",
|
|
34
|
+
maxPackageCharacters: 64 * MEBIBYTE
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const KIND_NAMES = Object.keys(PREVIEW_KINDS);
|
|
38
|
+
/**
|
|
39
|
+
* The kind a model image was generated as, or `undefined` for one the package
|
|
40
|
+
* actually carries. A generated preview has no relationship behind it, so
|
|
41
|
+
* `rId` is empty; the filename and data-URL prefix name which producer made it.
|
|
42
|
+
*/
|
|
43
|
+
const previewKindOf = (value) => {
|
|
44
|
+
if (!("type" in value) || value.type !== "image" || !("rId" in value) || value.rId !== "" || !("src" in value) || typeof value.src !== "string" || !("mimeType" in value) || !("filename" in value)) return;
|
|
45
|
+
const { src, mimeType, filename } = value;
|
|
46
|
+
return KIND_NAMES.find((name) => {
|
|
47
|
+
const kind = PREVIEW_KINDS[name];
|
|
48
|
+
return mimeType === kind.mimeType && filename === kind.filename && src.startsWith(kind.srcPrefix);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Charge every generated preview in the model against its kind's allowance and
|
|
53
|
+
* drop the `src` of those past it. Dropping leaves the image in place with its
|
|
54
|
+
* size and wrap, so the page still reserves the space the drawing occupies,
|
|
55
|
+
* and never touches the preserved XML the package saves from.
|
|
56
|
+
*/
|
|
57
|
+
const enforcePackagePreviewBudget = (root, overrides = {}) => {
|
|
58
|
+
const remaining = new Map(KIND_NAMES.map((name) => [name, Math.max(0, overrides[name] ?? PREVIEW_KINDS[name].maxPackageCharacters)]));
|
|
59
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
60
|
+
const visit = (value) => {
|
|
61
|
+
if (value === null || typeof value !== "object" || visited.has(value)) return;
|
|
62
|
+
visited.add(value);
|
|
63
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return;
|
|
64
|
+
if (value instanceof Map) {
|
|
65
|
+
for (const child of value.values()) visit(child);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
for (const child of value) visit(child);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const kind = previewKindOf(value);
|
|
73
|
+
if (kind !== void 0) {
|
|
74
|
+
const image = value;
|
|
75
|
+
const length = image.src.length;
|
|
76
|
+
const left = remaining.get(kind);
|
|
77
|
+
if (length <= left) remaining.set(kind, left - length);
|
|
78
|
+
else {
|
|
79
|
+
remaining.set(kind, 0);
|
|
80
|
+
delete image.src;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const child of Object.values(value)) visit(child);
|
|
84
|
+
};
|
|
85
|
+
visit(root);
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { PREVIEW_KINDS, VML_PREVIEW_DATA_URL_PREFIX, enforcePackagePreviewBudget };
|
|
@@ -117,7 +117,10 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
|
|
|
117
117
|
const occurrencesByPath = /* @__PURE__ */ new Map();
|
|
118
118
|
const reserved = /* @__PURE__ */ new Set();
|
|
119
119
|
for (const [path, xml] of candidates) {
|
|
120
|
-
assertXmlResourceLimits(
|
|
120
|
+
assertXmlResourceLimits({
|
|
121
|
+
xml,
|
|
122
|
+
partPath: path
|
|
123
|
+
});
|
|
121
124
|
const ids = [];
|
|
122
125
|
if (rewriteStreamingXmlDecimalAttributes(xml, (element) => {
|
|
123
126
|
const identified = identifiedElement(element);
|
|
@@ -127,7 +130,9 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
|
|
|
127
130
|
return null;
|
|
128
131
|
}).status === "unsupported") throw new XmlResourceLimitError({
|
|
129
132
|
message: `Revision-id normalization could not safely scan ${path}`,
|
|
130
|
-
limit: "syntax"
|
|
133
|
+
limit: "syntax",
|
|
134
|
+
observed: 0,
|
|
135
|
+
allowed: 0
|
|
131
136
|
});
|
|
132
137
|
occurrencesByPath.set(path, ids);
|
|
133
138
|
}
|
|
@@ -137,14 +142,19 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
|
|
|
137
142
|
else firstSeen.add(id);
|
|
138
143
|
if (repeatedPaths.size > 0) for (const [path, xml] of parts) {
|
|
139
144
|
if (occurrencesByPath.has(path) || !ANNOTATION_ELEMENT_CANDIDATE.test(xml)) continue;
|
|
140
|
-
assertXmlResourceLimits(
|
|
145
|
+
assertXmlResourceLimits({
|
|
146
|
+
xml,
|
|
147
|
+
partPath: path
|
|
148
|
+
});
|
|
141
149
|
if (rewriteStreamingXmlDecimalAttributes(xml, (element) => {
|
|
142
150
|
const identified = identifiedElement(element);
|
|
143
151
|
if (identified !== null) reserved.add(identified.id);
|
|
144
152
|
return null;
|
|
145
153
|
}).status === "unsupported") throw new XmlResourceLimitError({
|
|
146
154
|
message: `Revision-id normalization could not safely scan ${path}`,
|
|
147
|
-
limit: "syntax"
|
|
155
|
+
limit: "syntax",
|
|
156
|
+
observed: 0,
|
|
157
|
+
allowed: 0
|
|
148
158
|
});
|
|
149
159
|
}
|
|
150
160
|
let nextId = 0;
|
|
@@ -185,7 +195,9 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
|
|
|
185
195
|
});
|
|
186
196
|
if (rewritten.status === "unsupported") throw new XmlResourceLimitError({
|
|
187
197
|
message: `Revision-id normalization could not safely rewrite ${path}`,
|
|
188
|
-
limit: "syntax"
|
|
198
|
+
limit: "syntax",
|
|
199
|
+
observed: 0,
|
|
200
|
+
allowed: 0
|
|
189
201
|
});
|
|
190
202
|
normalized.set(path, rewritten.value);
|
|
191
203
|
}
|
package/dist/docx/rezip.js
CHANGED
|
@@ -19,7 +19,7 @@ import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./r
|
|
|
19
19
|
import { removeResolvedHeaderFooterParts } from "./removeHeaderFooterParts.js";
|
|
20
20
|
import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
|
|
21
21
|
import { buildPatchedNotePartXml, collectChangedNoteParaIds, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
|
|
22
|
-
import {
|
|
22
|
+
import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
|
|
23
23
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
24
24
|
import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
|
|
25
25
|
import { serializeHeaderFooter } from "./serializer/headerFooterSerializer.js";
|
|
@@ -29,11 +29,10 @@ import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
|
|
|
29
29
|
import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
|
|
30
30
|
import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerializer.js";
|
|
31
31
|
import { serializeThemeXml } from "./serializer/themeSerializer.js";
|
|
32
|
-
import { escapeXml } from "./serializer/xmlUtils.js";
|
|
33
32
|
import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, WORDPROCESSINGML_NAMESPACE_URIS, findChild, getAttribute, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
|
|
34
33
|
import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
|
|
35
34
|
import { panic } from "better-result";
|
|
36
|
-
import { validateDocxPackage } from "@stll/docx-core";
|
|
35
|
+
import { escapeXmlAttribute, escapeXmlText, validateDocxPackage } from "@stll/docx-core";
|
|
37
36
|
import JSZip from "jszip";
|
|
38
37
|
//#region src/docx/rezip.ts
|
|
39
38
|
/**
|
|
@@ -85,7 +84,7 @@ function findMaxRId(relsXml) {
|
|
|
85
84
|
}
|
|
86
85
|
const isWordprocessingElement = (element, localName) => getLocalName(element.name) === localName && WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "");
|
|
87
86
|
const countDocumentSections = (xml) => {
|
|
88
|
-
assertXmlResourceLimits(xml);
|
|
87
|
+
assertXmlResourceLimits({ xml });
|
|
89
88
|
let count = 0;
|
|
90
89
|
const pending = [{
|
|
91
90
|
element: parseXml(xml),
|
|
@@ -106,7 +105,7 @@ const countDocumentSections = (xml) => {
|
|
|
106
105
|
};
|
|
107
106
|
const extractHeaderFooterReferences = (xml) => {
|
|
108
107
|
const references = [];
|
|
109
|
-
assertXmlResourceLimits(xml);
|
|
108
|
+
assertXmlResourceLimits({ xml });
|
|
110
109
|
const pending = [parseXml(xml)];
|
|
111
110
|
while (pending.length > 0) {
|
|
112
111
|
const node = pending.pop();
|
|
@@ -166,15 +165,15 @@ async function serializeCommentsToZip(doc, zip, compressionLevel) {
|
|
|
166
165
|
if (comments.length === 0) {
|
|
167
166
|
if (sourceCommentsXml === void 0 || !hasCommentEntries(sourceCommentsXml)) return;
|
|
168
167
|
}
|
|
169
|
-
|
|
170
|
-
const commentsXml = serializeComments(
|
|
168
|
+
const plan = planCommentParts(comments);
|
|
169
|
+
const commentsXml = serializeComments(plan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
|
|
171
170
|
zip.file(sourceCommentsFile?.name ?? "word/comments.xml", commentsXml, {
|
|
172
171
|
compression: "DEFLATE",
|
|
173
172
|
compressionOptions: { level: compressionLevel }
|
|
174
173
|
});
|
|
175
174
|
await ensureCommentsContentType(zip, compressionLevel);
|
|
176
175
|
await ensureCommentsRelationship(zip, compressionLevel);
|
|
177
|
-
await syncCommentsExtendedPart(
|
|
176
|
+
await syncCommentsExtendedPart(plan, zip, compressionLevel);
|
|
178
177
|
}
|
|
179
178
|
const hasCommentEntries = (xml) => {
|
|
180
179
|
const root = parseXml(xml);
|
|
@@ -189,8 +188,8 @@ const hasCommentEntries = (xml) => {
|
|
|
189
188
|
* removed part triggers Word's repair prompt). The reply-thread markers in
|
|
190
189
|
* `document.xml` are synthesized separately (see {@link applyReplyThreadMarkers}).
|
|
191
190
|
*/
|
|
192
|
-
async function syncCommentsExtendedPart(
|
|
193
|
-
const xml = serializeCommentsExtended(
|
|
191
|
+
async function syncCommentsExtendedPart(plan, zip, compressionLevel) {
|
|
192
|
+
const xml = serializeCommentsExtended(plan);
|
|
194
193
|
const existing = findZipEntryCaseInsensitive(zip, COMMENTS_EXTENDED_PART_LOWER);
|
|
195
194
|
if (!xml) {
|
|
196
195
|
if (!existing) return;
|
|
@@ -396,7 +395,7 @@ async function processNewImages(parts, zip, compressionLevel) {
|
|
|
396
395
|
compression: "DEFLATE",
|
|
397
396
|
compressionOptions: { level: compressionLevel }
|
|
398
397
|
});
|
|
399
|
-
relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${
|
|
398
|
+
relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(relativeTargetForPart(partPath, mediaPath))}"/>`);
|
|
400
399
|
extensionsAdded.add(extension);
|
|
401
400
|
if (drawing.rawXml) drawing.rawXml = rebindDrawingImageRelationship({
|
|
402
401
|
xml: drawing.rawXml,
|
|
@@ -476,7 +475,7 @@ async function processNewHyperlinks(parts, zip, compressionLevel) {
|
|
|
476
475
|
if (!hyperlink.href) continue;
|
|
477
476
|
maxId++;
|
|
478
477
|
const newRId = `rId${maxId}`;
|
|
479
|
-
relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.hyperlink}" Target="${
|
|
478
|
+
relEntries.push(`<Relationship Id="${newRId}" Type="${RELATIONSHIP_TYPES.hyperlink}" Target="${escapeXmlAttribute(hyperlink.href)}" TargetMode="External"/>`);
|
|
480
479
|
hyperlink.rId = newRId;
|
|
481
480
|
}
|
|
482
481
|
zip.file(relsPath, relsXml.replace("</Relationships>", `${relEntries.join("")}</Relationships>`), {
|
|
@@ -952,7 +951,7 @@ async function addRelationship(originalBuffer, relationship) {
|
|
|
952
951
|
const relsXml = await relsFile.async("text");
|
|
953
952
|
const newRId = `rId${findMaxRId(relsXml) + 1}`;
|
|
954
953
|
const targetModeAttr = relationship.targetMode === "External" ? " TargetMode=\"External\"" : "";
|
|
955
|
-
const newRelElement = `<Relationship Id="${newRId}" Type="${relationship.type}" Target="${
|
|
954
|
+
const newRelElement = `<Relationship Id="${newRId}" Type="${relationship.type}" Target="${escapeXmlAttribute(relationship.target)}"${targetModeAttr}/>`;
|
|
956
955
|
const updatedRelsXml = relsXml.replace("</Relationships>", `${newRelElement}</Relationships>`);
|
|
957
956
|
zip.file(relsPath, updatedRelsXml);
|
|
958
957
|
return {
|
|
@@ -1101,7 +1100,7 @@ async function materializeNewHeaderFooterParts(doc, zip, compressionLevel) {
|
|
|
1101
1100
|
type: relType,
|
|
1102
1101
|
target: filename
|
|
1103
1102
|
});
|
|
1104
|
-
relEntries.push(`<Relationship Id="${
|
|
1103
|
+
relEntries.push(`<Relationship Id="${escapeXmlAttribute(effectiveRId)}" Type="${relType}" Target="${filename}"/>`);
|
|
1105
1104
|
overrides.push(`<Override PartName="/word/${filename}" ContentType="${contentType}"/>`);
|
|
1106
1105
|
}
|
|
1107
1106
|
};
|
|
@@ -1218,7 +1217,7 @@ async function rebindWatermarkRelIds(doc, zip, compressionLevel) {
|
|
|
1218
1217
|
}
|
|
1219
1218
|
if (!resolvedRId) {
|
|
1220
1219
|
resolvedRId = `rId${findMaxRId(relsXml) + 1}`;
|
|
1221
|
-
const relXml = canonical.mode === "external" ? `<Relationship Id="${resolvedRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${
|
|
1220
|
+
const relXml = canonical.mode === "external" ? `<Relationship Id="${resolvedRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(canonical.url)}" TargetMode="External"/>` : `<Relationship Id="${resolvedRId}" Type="${RELATIONSHIP_TYPES.image}" Target="${escapeXmlAttribute(relativeTargetForPart(partPath, canonical.absolute))}"/>`;
|
|
1222
1221
|
relsXmlByPath.set(relsPath, relsXml.replace("</Relationships>", `${relXml}</Relationships>`));
|
|
1223
1222
|
changedPaths.add(relsPath);
|
|
1224
1223
|
}
|
|
@@ -1531,7 +1530,7 @@ function updateCoreProperties(corePropsXml, { updateModifiedDate, modifiedBy })
|
|
|
1531
1530
|
if (result.includes("<dcterms:modified")) result = result.replace(/<dcterms:modified[^<>]*>[^<]*<\/dcterms:modified>/u, `<dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>`);
|
|
1532
1531
|
}
|
|
1533
1532
|
if (modifiedBy) {
|
|
1534
|
-
if (result.includes("<cp:lastModifiedBy")) result = result.replace(/<cp:lastModifiedBy>[^<]*<\/cp:lastModifiedBy>/u, `<cp:lastModifiedBy>${
|
|
1533
|
+
if (result.includes("<cp:lastModifiedBy")) result = result.replace(/<cp:lastModifiedBy>[^<]*<\/cp:lastModifiedBy>/u, `<cp:lastModifiedBy>${escapeXmlText(modifiedBy)}</cp:lastModifiedBy>`);
|
|
1535
1534
|
}
|
|
1536
1535
|
return result;
|
|
1537
1536
|
}
|
|
@@ -1638,13 +1637,13 @@ const createEmptyDocxZip = ({ creator, application }) => {
|
|
|
1638
1637
|
</w:document>`);
|
|
1639
1638
|
zip.file(STYLES_PART_PATH, SEED_STYLES_XML);
|
|
1640
1639
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1641
|
-
const creatorElement = creator === void 0 ? "" : `\n <dc:creator>${
|
|
1640
|
+
const creatorElement = creator === void 0 ? "" : `\n <dc:creator>${escapeXmlText(creator)}</dc:creator>`;
|
|
1642
1641
|
zip.file("docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
1643
1642
|
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${creatorElement}
|
|
1644
1643
|
<dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created>
|
|
1645
1644
|
<dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>
|
|
1646
1645
|
</cp:coreProperties>`);
|
|
1647
|
-
const applicationElements = application === void 0 ? "" : `\n <Application>${
|
|
1646
|
+
const applicationElements = application === void 0 ? "" : `\n <Application>${escapeXmlText(application)}</Application>\n <AppVersion>${CREATED_APP_VERSION}</AppVersion>`;
|
|
1648
1647
|
zip.file("docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
1649
1648
|
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">${applicationElements}
|
|
1650
1649
|
</Properties>`);
|
|
@@ -1,21 +1,27 @@
|
|
|
1
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
1
2
|
//#region src/docx/sdtPropertiesPatch.ts
|
|
2
|
-
const XML_ATTR_ESCAPES = {
|
|
3
|
-
"&": "&",
|
|
4
|
-
"<": "<",
|
|
5
|
-
">": ">",
|
|
6
|
-
"\"": """,
|
|
7
|
-
"'": "'"
|
|
8
|
-
};
|
|
9
3
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
4
|
+
* Surgical patches for the captured `<w:sdtPr>` XML string.
|
|
5
|
+
*
|
|
6
|
+
* Background. The block-SDT serializer (commit 3) replays
|
|
7
|
+
* `properties.rawPropertiesXml` verbatim so unmodeled OOXML markers
|
|
8
|
+
* (`w:dataBinding`, `w15:repeatingSection`, custom XML mappings) survive
|
|
9
|
+
* a round trip. That replay is correct for unchanged controls but goes
|
|
10
|
+
* stale the moment the editor mutates a modeled property: a user
|
|
11
|
+
* toggling a checkbox, picking a date, or choosing a dropdown value
|
|
12
|
+
* updates `properties.checked` / `dateFormat` / etc., but the raw
|
|
13
|
+
* `w14:checked w14:val="0"` already encoded by the source DOCX stays
|
|
14
|
+
* in `rawPropertiesXml` and gets written back on save — Word reopens
|
|
15
|
+
* the document with the user's interactive change discarded.
|
|
16
|
+
*
|
|
17
|
+
* `reconcileRawSdtPr` walks every modeled field that has an OOXML
|
|
18
|
+
* representation inside `<w:sdtPr>` and, if the field is set on the
|
|
19
|
+
* model, rewrites the matching element in the raw string. Unmodeled
|
|
20
|
+
* markers are untouched, so dataBinding / repeatingSection round-trip
|
|
21
|
+
* stays lossless even after the user has mutated the control.
|
|
22
|
+
*
|
|
23
|
+
* Picked up from upstream eigenpal/docx-editor#661.
|
|
15
24
|
*/
|
|
16
|
-
function escapeXmlAttr(value) {
|
|
17
|
-
return value.replace(/[&<>"']/gu, (ch) => XML_ATTR_ESCAPES[ch] ?? ch);
|
|
18
|
-
}
|
|
19
25
|
/**
|
|
20
26
|
* Drop any `*:lastValue="…"` attribute (any namespace prefix, or
|
|
21
27
|
* unprefixed) from an attribute-list string. We avoid a single greedy
|
|
@@ -116,8 +122,8 @@ function reconcileRawSdtPr(raw, props, options = {}) {
|
|
|
116
122
|
if (fullDate !== void 0 || dateFormat !== void 0) {
|
|
117
123
|
const wDate = /<(?<prefix>\w+):date\b(?<attrs>[^>]*)>(?<inner>[\s\S]*?)<\/\w+:date>/iu;
|
|
118
124
|
const wDateSelf = /<(?<prefix>\w+):date\b(?<attrs>[^/>]*)\/>/iu;
|
|
119
|
-
const fullDateAttr = fullDate !== void 0 ? ` w:fullDate="${
|
|
120
|
-
const formatChild = dateFormat !== void 0 ? `<w:dateFormat w:val="${
|
|
125
|
+
const fullDateAttr = fullDate !== void 0 ? ` w:fullDate="${escapeXmlAttribute(fullDate)}"` : "";
|
|
126
|
+
const formatChild = dateFormat !== void 0 ? `<w:dateFormat w:val="${escapeXmlAttribute(dateFormat)}"/>` : "";
|
|
121
127
|
if (wDate.test(next)) next = next.replace(wDate, (_match, prefix, matchedAttrs, inner) => {
|
|
122
128
|
let body = inner.replaceAll(/<\w+:dateFormat\b[^>]*(?:\/>|>[\s\S]*?<\/\w+:dateFormat>)/giu, "");
|
|
123
129
|
if (formatChild) body = `${formatChild}${body}`;
|
|
@@ -128,7 +134,7 @@ function reconcileRawSdtPr(raw, props, options = {}) {
|
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
if ((props.sdtType === "dropdown" || props.sdtType === "comboBox") && options.dropdownLastValue !== void 0) {
|
|
131
|
-
const escapedValue =
|
|
137
|
+
const escapedValue = escapeXmlAttribute(options.dropdownLastValue);
|
|
132
138
|
const opened = /<(?<prefix>\w+):(?<name>dropDownList|comboBox)\b(?<attrs>[^>]*)>(?<inner>[\s\S]*?)<\/\w+:(?:dropDownList|comboBox)>/iu;
|
|
133
139
|
const selfClosing = /<(?<prefix>\w+):(?<name>dropDownList|comboBox)\b(?<attrs>[^/>]*)\/>/iu;
|
|
134
140
|
if (opened.test(next)) next = next.replace(opened, (_match, prefix, name, attrs, inner) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { canonicalJson } from "../utils/canonicalJson.js";
|
|
2
|
-
import { escapeXml } from "./serializer/xmlUtils.js";
|
|
3
2
|
import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, WORDPROCESSINGML_NAMESPACE_URIS, findChildrenByNamespaceUri, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri } from "./xmlParser.js";
|
|
4
3
|
import { TaggedError } from "better-result";
|
|
4
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
5
5
|
//#region src/docx/sectionReferenceHistory.ts
|
|
6
6
|
const SECTION_REFERENCE_HISTORY_NAMESPACE = "urn:stella:folio:section-reference-history:1";
|
|
7
7
|
const HISTORY_NAMESPACES = /* @__PURE__ */ new Set([SECTION_REFERENCE_HISTORY_NAMESPACE]);
|
|
@@ -41,7 +41,7 @@ const serializeSectionReferenceHistory = (references) => {
|
|
|
41
41
|
if (new Set(values.map(({ type }) => type)).size !== values.length) return invalidHistory();
|
|
42
42
|
return values.map(({ type, rId }) => {
|
|
43
43
|
if (!rId || rId.trim() !== rId) return invalidHistory();
|
|
44
|
-
return `<w:${kind}Reference w:type="${type}" r:id="${
|
|
44
|
+
return `<w:${kind}Reference w:type="${type}" r:id="${escapeXmlAttribute(rId)}"/>`;
|
|
45
45
|
}).join("");
|
|
46
46
|
};
|
|
47
47
|
const content = serializeReferences("header", references.headerReferences ?? []) + serializeReferences("footer", references.footerReferences ?? []);
|