@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
|
@@ -8,7 +8,7 @@ import { RELATIONSHIP_TYPES } from "./relsParser.js";
|
|
|
8
8
|
import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedInlineResources, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
|
|
9
9
|
import "./selectiveSaveFlags.js";
|
|
10
10
|
import { buildPatchedDocumentXml, buildPatchedNoteXml, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
|
|
11
|
-
import {
|
|
11
|
+
import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
|
|
12
12
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
13
13
|
import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
|
|
14
14
|
import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
|
|
@@ -84,8 +84,8 @@ const commentsExtendedInfoEqual = (a, b) => {
|
|
|
84
84
|
* - Threading changed: write the part and ensure its override + relationship,
|
|
85
85
|
* bailing if the packaging files needed to wire it are absent.
|
|
86
86
|
*/
|
|
87
|
-
async function patchCommentsExtended(zip,
|
|
88
|
-
const desiredXml = serializeCommentsExtended(
|
|
87
|
+
async function patchCommentsExtended(zip, plan, updates) {
|
|
88
|
+
const desiredXml = serializeCommentsExtended(plan);
|
|
89
89
|
const existing = findZipEntryCaseInsensitive(zip, COMMENTS_EXTENDED_PART_LOWER);
|
|
90
90
|
if (!desiredXml) return existing === null;
|
|
91
91
|
const baselineInfo = existing ? parseCommentsExtended(await existing.async("text")) : /* @__PURE__ */ new Map();
|
|
@@ -190,6 +190,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
190
190
|
if (!validateFolioDocumentModel(doc).valid) return null;
|
|
191
191
|
const comments = doc.package.document.comments ?? [];
|
|
192
192
|
const hasComments = comments.length > 0;
|
|
193
|
+
const commentPlan = planCommentParts(comments);
|
|
193
194
|
try {
|
|
194
195
|
const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
|
|
195
196
|
for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
|
|
@@ -215,9 +216,8 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
215
216
|
}
|
|
216
217
|
const sourceCommentsFile = zip.file("word/comments.xml");
|
|
217
218
|
if (hasComments || sourceCommentsFile) {
|
|
218
|
-
ensureThreadedCommentParaIds(comments);
|
|
219
219
|
const sourceBindings = sourceCommentsFile ? readRootNamespaceBindings(await sourceCommentsFile.async("text")) : void 0;
|
|
220
|
-
updates.set("word/comments.xml", serializeComments(
|
|
220
|
+
updates.set("word/comments.xml", serializeComments(commentPlan, sourceBindings));
|
|
221
221
|
}
|
|
222
222
|
if (hasComments) {
|
|
223
223
|
const ctFile = zip.file("[Content_Types].xml");
|
|
@@ -235,7 +235,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
-
if (!await patchCommentsExtended(zip,
|
|
238
|
+
if (!await patchCommentsExtended(zip, commentPlan, updates)) return null;
|
|
239
239
|
await patchNumberingPart(zip, doc, updates);
|
|
240
240
|
for (const [path, xml] of await collectHeaderFooterUpdates(doc, zip)) updates.set(path, xml);
|
|
241
241
|
await queueSettingsUpdates(zip, updates);
|
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
import { reconcileRawSdtPr } from "../sdtPropertiesPatch.js";
|
|
2
2
|
import { isSingleWellFormedElement } from "./xmlUtils.js";
|
|
3
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
3
4
|
//#region src/docx/serializer/blockSdtSerializer.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Block-level SDT serializer.
|
|
7
|
+
*
|
|
8
|
+
* Replays `<w:sdtPr>` (and `<w:sdtEndPr>` when present) verbatim from the
|
|
9
|
+
* `rawPropertiesXml` / `rawEndPropertiesXml` strings captured by the parser.
|
|
10
|
+
* That keeps OOXML element order intact (`CT_SdtPr` is an `xsd:sequence`,
|
|
11
|
+
* ECMA-376 §17.5.2) and round-trips unmodeled features — `w:dataBinding`,
|
|
12
|
+
* `w15:repeatingSection`, `@lastValue`, custom XML mappings — without us
|
|
13
|
+
* having to enumerate them.
|
|
14
|
+
*
|
|
15
|
+
* If a `BlockSdt` was constructed programmatically (no parsed snapshot to
|
|
16
|
+
* replay), fall back to a minimal projection from the modeled fields so the
|
|
17
|
+
* result is still a valid `<w:sdt>`.
|
|
18
|
+
*
|
|
19
|
+
* Sharing the helper between the document body and the header/footer
|
|
20
|
+
* serializers keeps body↔HF parity in one place.
|
|
21
|
+
*/
|
|
7
22
|
function serializeFallbackSdtPr(props) {
|
|
8
23
|
const parts = [];
|
|
9
24
|
if (props.id !== void 0) parts.push(`<w:id w:val="${props.id}"/>`);
|
|
10
|
-
if (props.alias) parts.push(`<w:alias w:val="${
|
|
11
|
-
if (props.tag) parts.push(`<w:tag w:val="${
|
|
25
|
+
if (props.alias) parts.push(`<w:alias w:val="${escapeXmlAttribute(props.alias)}"/>`);
|
|
26
|
+
if (props.tag) parts.push(`<w:tag w:val="${escapeXmlAttribute(props.tag)}"/>`);
|
|
12
27
|
if (props.lock) parts.push(`<w:lock w:val="${props.lock}"/>`);
|
|
13
|
-
if (props.placeholder) parts.push(`<w:placeholder><w:docPart w:val="${
|
|
28
|
+
if (props.placeholder) parts.push(`<w:placeholder><w:docPart w:val="${escapeXmlAttribute(props.placeholder)}"/></w:placeholder>`);
|
|
14
29
|
if (props.showingPlaceholder) parts.push("<w:showingPlcHdr/>");
|
|
15
30
|
switch (props.sdtType) {
|
|
16
31
|
case "plainText":
|
|
17
32
|
parts.push("<w:text/>");
|
|
18
33
|
break;
|
|
19
34
|
case "date": {
|
|
20
|
-
const fullDateAttr = props.dateValueISO ? ` w:fullDate="${
|
|
21
|
-
const formatChild = props.dateFormat ? `<w:dateFormat w:val="${
|
|
35
|
+
const fullDateAttr = props.dateValueISO ? ` w:fullDate="${escapeXmlAttribute(props.dateValueISO)}"` : "";
|
|
36
|
+
const formatChild = props.dateFormat ? `<w:dateFormat w:val="${escapeXmlAttribute(props.dateFormat)}"/>` : "";
|
|
22
37
|
if (fullDateAttr || formatChild) parts.push(`<w:date${fullDateAttr}>${formatChild}</w:date>`);
|
|
23
38
|
else parts.push("<w:date/>");
|
|
24
39
|
break;
|
|
@@ -26,7 +41,7 @@ function serializeFallbackSdtPr(props) {
|
|
|
26
41
|
case "dropdown":
|
|
27
42
|
case "comboBox": {
|
|
28
43
|
const tag = props.sdtType === "dropdown" ? "w:dropDownList" : "w:comboBox";
|
|
29
|
-
const items = (props.listItems ?? []).map((item) => `<w:listItem w:displayText="${
|
|
44
|
+
const items = (props.listItems ?? []).map((item) => `<w:listItem w:displayText="${escapeXmlAttribute(item.displayText)}" w:value="${escapeXmlAttribute(item.value)}"/>`).join("");
|
|
30
45
|
parts.push(`<${tag}>${items}</${tag}>`);
|
|
31
46
|
break;
|
|
32
47
|
}
|
|
@@ -48,25 +63,22 @@ function serializeFallbackSdtPr(props) {
|
|
|
48
63
|
}
|
|
49
64
|
return `<w:sdtPr>${parts.join("")}</w:sdtPr>`;
|
|
50
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* `properties.dropdownLastValue` is the only record of what was selected.
|
|
68
|
+
*
|
|
69
|
+
* The XSD default of `@w:lastValue` is the empty string, so "never selected",
|
|
70
|
+
* "cleared" and "selected" are three distinguishable states, and the body's
|
|
71
|
+
* display text is evidence for none of them: it is equally the placeholder of
|
|
72
|
+
* a dropdown nobody has touched, and a displayText shared by two list items
|
|
73
|
+
* picks the wrong sibling. The parser records the authored `@w:lastValue` and
|
|
74
|
+
* `setContentControlValue` records a pick; what neither wrote is no selection.
|
|
75
|
+
*
|
|
76
|
+
* `""` is a value a producer can author (`<w:listItem w:value=""/>`), so
|
|
77
|
+
* presence is the test, not truthiness.
|
|
78
|
+
*/
|
|
51
79
|
function extractDropdownLastValue(blockSdt) {
|
|
52
80
|
if (blockSdt.properties.sdtType !== "dropdown" && blockSdt.properties.sdtType !== "comboBox") return;
|
|
53
|
-
|
|
54
|
-
if (modeled !== void 0) return modeled;
|
|
55
|
-
if (blockSdt.properties.rawPropertiesXml !== void 0) return;
|
|
56
|
-
const firstBlock = blockSdt.content[0];
|
|
57
|
-
if (!firstBlock || firstBlock.type !== "paragraph") return;
|
|
58
|
-
const parts = [];
|
|
59
|
-
for (const child of firstBlock.content) if (child.type === "run") {
|
|
60
|
-
for (const item of child.content) if (item.type === "text") parts.push(item.text);
|
|
61
|
-
}
|
|
62
|
-
const text = parts.join("");
|
|
63
|
-
if (text.length === 0) return;
|
|
64
|
-
const items = blockSdt.properties.listItems;
|
|
65
|
-
if (items) {
|
|
66
|
-
const match = items.find((item) => item.displayText === text);
|
|
67
|
-
if (match) return match.value;
|
|
68
|
-
}
|
|
69
|
-
return text;
|
|
81
|
+
return blockSdt.properties.dropdownLastValue;
|
|
70
82
|
}
|
|
71
83
|
function extractDateFullDate(blockSdt) {
|
|
72
84
|
if (blockSdt.properties.sdtType !== "date") return;
|
|
@@ -18,7 +18,7 @@ type ExhaustiveBorder = document_d_exports.ExhaustiveFields<document_d_exports.B
|
|
|
18
18
|
*
|
|
19
19
|
* `style` and the color values come straight from the parsed DOCX (the parser
|
|
20
20
|
* casts `w:val`/`w:color` without validating the enum), so they are
|
|
21
|
-
* untrusted and are `
|
|
21
|
+
* untrusted and are `escapeXmlAttribute`'d before re-entering XML attributes; for valid
|
|
22
22
|
* documents these are enum/hex values, so escaping is a no-op.
|
|
23
23
|
*/
|
|
24
24
|
declare function serializeBorder(input: ExhaustiveBorder | undefined, elementName: string): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { intAttr } from "./xmlUtils.js";
|
|
2
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
2
3
|
//#region src/docx/serializer/borderSerializer.ts
|
|
3
4
|
/**
|
|
4
5
|
* Serialize a single border element (`<w:top .../>`, `<w:left .../>`, ...).
|
|
@@ -16,30 +17,30 @@ import { escapeXml, intAttr } from "./xmlUtils.js";
|
|
|
16
17
|
*
|
|
17
18
|
* `style` and the color values come straight from the parsed DOCX (the parser
|
|
18
19
|
* casts `w:val`/`w:color` without validating the enum), so they are
|
|
19
|
-
* untrusted and are `
|
|
20
|
+
* untrusted and are `escapeXmlAttribute`'d before re-entering XML attributes; for valid
|
|
20
21
|
* documents these are enum/hex values, so escaping is a no-op.
|
|
21
22
|
*/
|
|
22
23
|
function serializeBorder(input, elementName) {
|
|
23
24
|
if (!input) return "";
|
|
24
25
|
const { style, color, size, space, shadow, frame, artRelationshipId, topLeftArtRelationshipId, topRightArtRelationshipId, bottomLeftArtRelationshipId, bottomRightArtRelationshipId } = input;
|
|
25
|
-
const attrs = [`w:val="${
|
|
26
|
+
const attrs = [`w:val="${escapeXmlAttribute(style)}"`];
|
|
26
27
|
if (size !== void 0) attrs.push(`w:sz="${intAttr(size)}"`);
|
|
27
28
|
if (space !== void 0) attrs.push(`w:space="${intAttr(space)}"`);
|
|
28
29
|
if (color) {
|
|
29
30
|
const { auto, rgb, themeColor, themeTint, themeShade } = color;
|
|
30
31
|
if (auto) attrs.push("w:color=\"auto\"");
|
|
31
|
-
else if (rgb) attrs.push(`w:color="${
|
|
32
|
-
if (themeColor) attrs.push(`w:themeColor="${
|
|
33
|
-
if (themeTint) attrs.push(`w:themeTint="${
|
|
34
|
-
if (themeShade) attrs.push(`w:themeShade="${
|
|
32
|
+
else if (rgb) attrs.push(`w:color="${escapeXmlAttribute(rgb)}"`);
|
|
33
|
+
if (themeColor) attrs.push(`w:themeColor="${escapeXmlAttribute(themeColor)}"`);
|
|
34
|
+
if (themeTint) attrs.push(`w:themeTint="${escapeXmlAttribute(themeTint)}"`);
|
|
35
|
+
if (themeShade) attrs.push(`w:themeShade="${escapeXmlAttribute(themeShade)}"`);
|
|
35
36
|
}
|
|
36
37
|
if (shadow) attrs.push("w:shadow=\"true\"");
|
|
37
38
|
if (frame) attrs.push("w:frame=\"true\"");
|
|
38
|
-
if (artRelationshipId) attrs.push(`
|
|
39
|
-
if (topLeftArtRelationshipId) attrs.push(`
|
|
40
|
-
if (topRightArtRelationshipId) attrs.push(`
|
|
41
|
-
if (bottomLeftArtRelationshipId) attrs.push(`
|
|
42
|
-
if (bottomRightArtRelationshipId) attrs.push(`
|
|
39
|
+
if (artRelationshipId) attrs.push(`r:id="${escapeXmlAttribute(artRelationshipId)}"`);
|
|
40
|
+
if (topLeftArtRelationshipId) attrs.push(`r:topLeft="${escapeXmlAttribute(topLeftArtRelationshipId)}"`);
|
|
41
|
+
if (topRightArtRelationshipId) attrs.push(`r:topRight="${escapeXmlAttribute(topRightArtRelationshipId)}"`);
|
|
42
|
+
if (bottomLeftArtRelationshipId) attrs.push(`r:bottomLeft="${escapeXmlAttribute(bottomLeftArtRelationshipId)}"`);
|
|
43
|
+
if (bottomRightArtRelationshipId) attrs.push(`r:bottomRight="${escapeXmlAttribute(bottomRightArtRelationshipId)}"`);
|
|
43
44
|
return `<w:${elementName} ${attrs.join(" ")}/>`;
|
|
44
45
|
}
|
|
45
46
|
//#endregion
|
|
@@ -1,30 +1,55 @@
|
|
|
1
1
|
import { Comment } from "../../types/content.js";
|
|
2
2
|
//#region src/docx/serializer/commentSerializer.d.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* What every comment part is written from: one ordered list of comment ids, and
|
|
5
|
+
* the facts each id carries.
|
|
6
|
+
*
|
|
7
|
+
* `word/comments.xml` and `word/commentsExtended.xml` describe the same
|
|
8
|
+
* comments and have to agree about which is which. They agreed by each walking
|
|
9
|
+
* the `Comment[]` they were handed and pairing entries up by position, so any
|
|
10
|
+
* disagreement about order — one part reordered, the other not — silently moved
|
|
11
|
+
* a comment's thread link, resolved state and paraId onto a different comment.
|
|
12
|
+
* Positions cannot disagree if neither part has one: both walk `order` and look
|
|
13
|
+
* every fact up by `w:id`, the only identity the package itself has.
|
|
14
|
+
*/
|
|
15
|
+
type CommentPartPlan = {
|
|
16
|
+
/** The `w:id`s to write, in order, each exactly once. */
|
|
17
|
+
readonly order: readonly number[];
|
|
18
|
+
readonly byId: ReadonlyMap<number, Comment>;
|
|
19
|
+
/** The `w14:paraId` each comment is threaded by, minted where the model had none. */
|
|
20
|
+
readonly threadParaIdById: ReadonlyMap<number, string>;
|
|
21
|
+
/** The comments needing a `commentsExtended.xml` entry: replies, their parents, resolved comments. */
|
|
22
|
+
readonly threadedIds: ReadonlySet<number>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Plan both comment parts from the model, in the model's own order.
|
|
26
|
+
*
|
|
27
|
+
* The order is the document's: the order `word/comments.xml` listed the
|
|
28
|
+
* comments in is the order it is written back in, so a save neither reshuffles
|
|
29
|
+
* a reviewer's threads nor hands the next parse a different `comments[]` than
|
|
30
|
+
* the one it read.
|
|
31
|
+
*
|
|
32
|
+
* Planning also mints the `w14:paraId` a thread needs when the model has none —
|
|
33
|
+
* a comment written in the editor has no Word-authored id, and without one the
|
|
34
|
+
* `commentsExtended.xml` link would be dropped. Minting is deterministic
|
|
35
|
+
* (comment id and text derived) so repeated saves mint the same id, and it
|
|
36
|
+
* happens here rather than at each call site because both parts must see it.
|
|
37
|
+
*/
|
|
38
|
+
declare const planCommentParts: (comments: readonly Comment[]) => CommentPartPlan;
|
|
39
|
+
/**
|
|
40
|
+
* Serialize a comment plan to comments.xml content. Returns a valid empty
|
|
41
|
+
* `<w:comments/>` document for an empty plan so callers can overwrite an
|
|
6
42
|
* existing `word/comments.xml` part when the editor has removed the last
|
|
7
43
|
* comment — leaving the previous file in place would otherwise re-emit
|
|
8
44
|
* the orphaned comment threads on every save.
|
|
9
45
|
*/
|
|
10
|
-
declare function serializeComments(
|
|
11
|
-
/**
|
|
12
|
-
* Assign a deterministic `w14:paraId` to the LAST paragraph of every comment
|
|
13
|
-
* that needs a commentsExtended entry (a reply, a reply's parent, or a resolved
|
|
14
|
-
* comment) but has none. A document authored or loaded without comment paraIds
|
|
15
|
-
* would otherwise have no stable key to thread through commentsExtended.xml, and
|
|
16
|
-
* the thread link would be silently dropped. Word-authored ids are preserved;
|
|
17
|
-
* only threaded, id-less paragraphs are filled. Deterministic (content- and
|
|
18
|
-
* id-derived) so repeated saves mint the SAME id. MUST run before serializing
|
|
19
|
-
* BOTH comments.xml and commentsExtended.xml so the two reference the same id.
|
|
20
|
-
*/
|
|
21
|
-
declare function ensureThreadedCommentParaIds(comments: readonly Comment[]): void;
|
|
46
|
+
declare function serializeComments({ order, byId }: CommentPartPlan, sourceBindings?: ReadonlyMap<string, string>): string;
|
|
22
47
|
/**
|
|
23
48
|
* Serialize `commentsExtended.xml` (`w15:commentsEx`) for reply threading and
|
|
24
49
|
* resolved state, or `null` when no comment needs an entry (see
|
|
25
50
|
* {@link buildCommentExtendedEntries}). This is the part that makes Word render
|
|
26
51
|
* a comment as a REPLY rather than a separate top-level thread.
|
|
27
52
|
*/
|
|
28
|
-
declare function serializeCommentsExtended(
|
|
53
|
+
declare function serializeCommentsExtended(plan: CommentPartPlan): string | null;
|
|
29
54
|
//#endregion
|
|
30
|
-
export {
|
|
55
|
+
export { CommentPartPlan, planCommentParts, serializeComments, serializeCommentsExtended };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { deterministicHexId } from "../../utils/hexId.js";
|
|
2
|
+
import { commentThreadParaId } from "../commentThreadKey.js";
|
|
2
3
|
import { serializeParagraph } from "./paragraphSerializer.js";
|
|
3
4
|
import { serializePartElement } from "./partNamespaces.js";
|
|
4
5
|
import { serializeTextFormatting } from "./textFormattingSerializer.js";
|
|
5
|
-
import {
|
|
6
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
6
7
|
//#region src/docx/serializer/commentSerializer.ts
|
|
7
8
|
/**
|
|
8
9
|
* Comment Serializer
|
|
@@ -26,9 +27,9 @@ function serializeParagraphWithAnnotationRef(paragraph, formatting) {
|
|
|
26
27
|
return xml.replace(/^<w:p(?=[\s>])[^>]*>/u, (openingTag) => `${openingTag}${annotationReference}`);
|
|
27
28
|
}
|
|
28
29
|
function serializeComment(comment) {
|
|
29
|
-
const attrs = [`w:id="${comment.id}"`, `w:author="${
|
|
30
|
-
if (comment.initials !== void 0) attrs.push(`w:initials="${
|
|
31
|
-
if (comment.date) attrs.push(`w:date="${
|
|
30
|
+
const attrs = [`w:id="${comment.id}"`, `w:author="${escapeXmlAttribute(comment.author)}"`];
|
|
31
|
+
if (comment.initials !== void 0) attrs.push(`w:initials="${escapeXmlAttribute(comment.initials)}"`);
|
|
32
|
+
if (comment.date) attrs.push(`w:date="${escapeXmlAttribute(comment.date)}"`);
|
|
32
33
|
let xml = `<w:comment ${attrs.join(" ")}>`;
|
|
33
34
|
if (comment.content.length > 0) {
|
|
34
35
|
xml += serializeParagraphWithAnnotationRef(comment.content[0], comment.annotationReferenceFormatting);
|
|
@@ -61,46 +62,6 @@ const COMMENTS_EXTENDED_BASELINE_PREFIXES = [
|
|
|
61
62
|
];
|
|
62
63
|
const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
|
|
63
64
|
/**
|
|
64
|
-
* The order every comment part is written in: top-level comments, then replies.
|
|
65
|
-
*
|
|
66
|
-
* `word/comments.xml` has always been written this way, and the order matters
|
|
67
|
-
* beyond that one part: the save rewrites comments.xml, so the next parse hands
|
|
68
|
-
* the model back in exactly this order. Any other part built by walking the
|
|
69
|
-
* same array — `word/commentsExtended.xml`, and the paraId minting both parts
|
|
70
|
-
* key on — has to walk it in the same order, or the first save writes a part
|
|
71
|
-
* whose order the second save cannot reproduce and saving is not a fixed point.
|
|
72
|
-
*/
|
|
73
|
-
const commentPartOrder = (comments) => {
|
|
74
|
-
const topLevel = [];
|
|
75
|
-
const replies = [];
|
|
76
|
-
for (const comment of comments) {
|
|
77
|
-
const { parentId } = comment;
|
|
78
|
-
(parentId === null || parentId === void 0 ? topLevel : replies).push(comment);
|
|
79
|
-
}
|
|
80
|
-
return [...topLevel, ...replies];
|
|
81
|
-
};
|
|
82
|
-
/**
|
|
83
|
-
* Serialize comments array to comments.xml content. Returns a valid empty
|
|
84
|
-
* `<w:comments/>` document for an empty array so callers can overwrite an
|
|
85
|
-
* existing `word/comments.xml` part when the editor has removed the last
|
|
86
|
-
* comment — leaving the previous file in place would otherwise re-emit
|
|
87
|
-
* the orphaned comment threads on every save.
|
|
88
|
-
*/
|
|
89
|
-
function serializeComments(comments, sourceBindings) {
|
|
90
|
-
const body = commentPartOrder(comments).map((comment) => serializeComment(comment)).join("");
|
|
91
|
-
return XML_DECLARATION + serializePartElement({
|
|
92
|
-
partPath: "word/comments.xml",
|
|
93
|
-
rootName: "w:comments",
|
|
94
|
-
baselinePrefixes: COMMENTS_BASELINE_PREFIXES,
|
|
95
|
-
sourceBindings,
|
|
96
|
-
body
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
/** The `w14:paraId` Word threads a comment by: its LAST paragraph's paraId. */
|
|
100
|
-
function commentThreadParaId(comment) {
|
|
101
|
-
return comment.content?.at(-1)?.paraId;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
65
|
* The ids of comments that need a `commentsExtended.xml` entry: a reply, a
|
|
105
66
|
* reply's parent (the thread root), or any comment carrying a resolved state.
|
|
106
67
|
* A plain top-level comment set yields an empty set, so no part is written.
|
|
@@ -121,29 +82,71 @@ const commentPlainText = (comment) => {
|
|
|
121
82
|
return text;
|
|
122
83
|
};
|
|
123
84
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
85
|
+
* Plan both comment parts from the model, in the model's own order.
|
|
86
|
+
*
|
|
87
|
+
* The order is the document's: the order `word/comments.xml` listed the
|
|
88
|
+
* comments in is the order it is written back in, so a save neither reshuffles
|
|
89
|
+
* a reviewer's threads nor hands the next parse a different `comments[]` than
|
|
90
|
+
* the one it read.
|
|
91
|
+
*
|
|
92
|
+
* Planning also mints the `w14:paraId` a thread needs when the model has none —
|
|
93
|
+
* a comment written in the editor has no Word-authored id, and without one the
|
|
94
|
+
* `commentsExtended.xml` link would be dropped. Minting is deterministic
|
|
95
|
+
* (comment id and text derived) so repeated saves mint the same id, and it
|
|
96
|
+
* happens here rather than at each call site because both parts must see it.
|
|
132
97
|
*/
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
98
|
+
const planCommentParts = (comments) => {
|
|
99
|
+
const byId = /* @__PURE__ */ new Map();
|
|
100
|
+
const order = [];
|
|
101
|
+
for (const comment of comments) {
|
|
102
|
+
if (byId.has(comment.id)) continue;
|
|
103
|
+
byId.set(comment.id, comment);
|
|
104
|
+
order.push(comment.id);
|
|
105
|
+
}
|
|
106
|
+
const planned = [...byId.values()];
|
|
107
|
+
const threadedIds = threadedCommentIds(planned);
|
|
136
108
|
const used = /* @__PURE__ */ new Set();
|
|
137
|
-
for (const comment of
|
|
138
|
-
|
|
139
|
-
|
|
109
|
+
for (const comment of planned) for (const paragraph of comment.content ?? []) if (paragraph.paraId) used.add(paragraph.paraId.toUpperCase());
|
|
110
|
+
const threadParaIdById = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const id of order) {
|
|
112
|
+
const comment = byId.get(id);
|
|
113
|
+
const existing = commentThreadParaId((comment.content ?? []).map(({ paraId }) => paraId));
|
|
114
|
+
if (existing) {
|
|
115
|
+
threadParaIdById.set(id, existing);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
140
118
|
const last = (comment.content ?? []).at(-1);
|
|
141
|
-
if (!
|
|
142
|
-
let
|
|
143
|
-
for (let salt = 1; used.has(
|
|
144
|
-
used.add(
|
|
145
|
-
last.paraId =
|
|
119
|
+
if (!threadedIds.has(id) || !last) continue;
|
|
120
|
+
let minted = deterministicHexId(`comment:${id}:${commentPlainText(comment)}`);
|
|
121
|
+
for (let salt = 1; used.has(minted.toUpperCase()); salt++) minted = deterministicHexId(`comment:${id}:${salt}`);
|
|
122
|
+
used.add(minted.toUpperCase());
|
|
123
|
+
last.paraId = minted;
|
|
124
|
+
threadParaIdById.set(id, minted);
|
|
146
125
|
}
|
|
126
|
+
return {
|
|
127
|
+
order,
|
|
128
|
+
byId,
|
|
129
|
+
threadParaIdById,
|
|
130
|
+
threadedIds
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Serialize a comment plan to comments.xml content. Returns a valid empty
|
|
135
|
+
* `<w:comments/>` document for an empty plan so callers can overwrite an
|
|
136
|
+
* existing `word/comments.xml` part when the editor has removed the last
|
|
137
|
+
* comment — leaving the previous file in place would otherwise re-emit
|
|
138
|
+
* the orphaned comment threads on every save.
|
|
139
|
+
*/
|
|
140
|
+
function serializeComments({ order, byId }, sourceBindings) {
|
|
141
|
+
let body = "";
|
|
142
|
+
for (const id of order) body += serializeComment(byId.get(id));
|
|
143
|
+
return XML_DECLARATION + serializePartElement({
|
|
144
|
+
partPath: "word/comments.xml",
|
|
145
|
+
rootName: "w:comments",
|
|
146
|
+
baselinePrefixes: COMMENTS_BASELINE_PREFIXES,
|
|
147
|
+
sourceBindings,
|
|
148
|
+
body
|
|
149
|
+
});
|
|
147
150
|
}
|
|
148
151
|
/**
|
|
149
152
|
* Build the `commentsExtended.xml` entries for the comments that participate in
|
|
@@ -152,29 +155,23 @@ function ensureThreadedCommentParaIds(comments) {
|
|
|
152
155
|
* leave any existing part untouched — a plain top-level comment set gets no
|
|
153
156
|
* `commentsExtended.xml`, matching how Word omits it.
|
|
154
157
|
*
|
|
155
|
-
* A `w15:commentEx` keys on the comment's
|
|
156
|
-
* `w15:paraIdParent`
|
|
157
|
-
*
|
|
158
|
-
* skip is a last-ditch safety for a malformed model.
|
|
158
|
+
* A `w15:commentEx` keys on the comment's thread paraId; a reply's
|
|
159
|
+
* `w15:paraIdParent` on its parent's. Both come from the plan, so a reply and
|
|
160
|
+
* its parent name each other by `w:id` right up to the attribute.
|
|
159
161
|
*/
|
|
160
|
-
function buildCommentExtendedEntries(
|
|
161
|
-
|
|
162
|
-
if (threaded.size === 0) return null;
|
|
163
|
-
const paraIdByCommentId = /* @__PURE__ */ new Map();
|
|
164
|
-
for (const comment of comments) {
|
|
165
|
-
const paraId = commentThreadParaId(comment);
|
|
166
|
-
if (paraId) paraIdByCommentId.set(comment.id, paraId);
|
|
167
|
-
}
|
|
162
|
+
function buildCommentExtendedEntries({ order, byId, threadParaIdById, threadedIds }) {
|
|
163
|
+
if (threadedIds.size === 0) return null;
|
|
168
164
|
const entries = [];
|
|
169
|
-
for (const
|
|
170
|
-
if (!
|
|
171
|
-
const paraId =
|
|
165
|
+
for (const id of order) {
|
|
166
|
+
if (!threadedIds.has(id)) continue;
|
|
167
|
+
const paraId = threadParaIdById.get(id);
|
|
172
168
|
if (!paraId) continue;
|
|
173
|
-
const
|
|
169
|
+
const { parentId, done } = byId.get(id);
|
|
170
|
+
const parentParaId = parentId !== void 0 ? threadParaIdById.get(parentId) : void 0;
|
|
174
171
|
entries.push({
|
|
175
172
|
paraId,
|
|
176
173
|
...parentParaId !== void 0 ? { paraIdParent: parentParaId } : {},
|
|
177
|
-
done:
|
|
174
|
+
done: done ?? false
|
|
178
175
|
});
|
|
179
176
|
}
|
|
180
177
|
return entries.length > 0 ? entries : null;
|
|
@@ -185,12 +182,12 @@ function buildCommentExtendedEntries(comments) {
|
|
|
185
182
|
* {@link buildCommentExtendedEntries}). This is the part that makes Word render
|
|
186
183
|
* a comment as a REPLY rather than a separate top-level thread.
|
|
187
184
|
*/
|
|
188
|
-
function serializeCommentsExtended(
|
|
189
|
-
const entries = buildCommentExtendedEntries(
|
|
185
|
+
function serializeCommentsExtended(plan) {
|
|
186
|
+
const entries = buildCommentExtendedEntries(plan);
|
|
190
187
|
if (!entries) return null;
|
|
191
188
|
const body = entries.map((entry) => {
|
|
192
|
-
const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${
|
|
193
|
-
return `<w15:commentEx w15:paraId="${
|
|
189
|
+
const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${escapeXmlAttribute(entry.paraIdParent)}"` : "";
|
|
190
|
+
return `<w15:commentEx w15:paraId="${escapeXmlAttribute(entry.paraId)}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
|
|
194
191
|
}).join("");
|
|
195
192
|
return XML_DECLARATION + serializePartElement({
|
|
196
193
|
partPath: "word/commentsExtended.xml",
|
|
@@ -201,4 +198,4 @@ function serializeCommentsExtended(comments) {
|
|
|
201
198
|
});
|
|
202
199
|
}
|
|
203
200
|
//#endregion
|
|
204
|
-
export {
|
|
201
|
+
export { planCommentParts, serializeComments, serializeCommentsExtended };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { serializePartElement } from "./partNamespaces.js";
|
|
2
|
-
import {
|
|
2
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
3
3
|
//#region src/docx/serializer/fontTableSerializer.ts
|
|
4
4
|
const serializeFontTableXml = (fontTable) => "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
|
|
5
5
|
partPath: "word/fontTable.xml",
|
|
@@ -10,20 +10,20 @@ const serializeFontTableXml = (fontTable) => "<?xml version=\"1.0\" encoding=\"U
|
|
|
10
10
|
});
|
|
11
11
|
const serializeFont = (font) => {
|
|
12
12
|
const parts = [];
|
|
13
|
-
if (font.altName) parts.push(`<w:altName w:val="${
|
|
14
|
-
if (font.panose1) parts.push(`<w:panose1 w:val="${
|
|
15
|
-
if (font.charset) parts.push(`<w:charset w:val="${
|
|
13
|
+
if (font.altName) parts.push(`<w:altName w:val="${escapeXmlAttribute(font.altName)}"/>`);
|
|
14
|
+
if (font.panose1) parts.push(`<w:panose1 w:val="${escapeXmlAttribute(font.panose1)}"/>`);
|
|
15
|
+
if (font.charset) parts.push(`<w:charset w:val="${escapeXmlAttribute(font.charset)}"/>`);
|
|
16
16
|
if (font.family) parts.push(`<w:family w:val="${font.family}"/>`);
|
|
17
17
|
if (font.pitch) parts.push(`<w:pitch w:val="${font.pitch}"/>`);
|
|
18
18
|
if (font.sig) {
|
|
19
19
|
const attrs = [];
|
|
20
20
|
for (const [key, value] of Object.entries(font.sig)) {
|
|
21
21
|
if (value === void 0) continue;
|
|
22
|
-
attrs.push(`w:${key}="${
|
|
22
|
+
attrs.push(`w:${key}="${escapeXmlAttribute(value)}"`);
|
|
23
23
|
}
|
|
24
24
|
if (attrs.length > 0) parts.push(`<w:sig ${attrs.join(" ")}/>`);
|
|
25
25
|
}
|
|
26
|
-
return `<w:font w:name="${
|
|
26
|
+
return `<w:font w:name="${escapeXmlAttribute(font.name)}">${parts.join("")}</w:font>`;
|
|
27
27
|
};
|
|
28
28
|
//#endregion
|
|
29
29
|
export { serializeFontTableXml };
|
|
@@ -7,7 +7,7 @@ import { serializeBlockSdt } from "./blockSdtSerializer.js";
|
|
|
7
7
|
import { serializeParagraph } from "./paragraphSerializer.js";
|
|
8
8
|
import { serializePartElement } from "./partNamespaces.js";
|
|
9
9
|
import { serializeTable } from "./tableSerializer.js";
|
|
10
|
-
import {
|
|
10
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
11
11
|
//#region src/docx/serializer/headerFooterSerializer.ts
|
|
12
12
|
const HEADER_FOOTER_BASELINE_PREFIXES = [
|
|
13
13
|
"wpc",
|
|
@@ -85,7 +85,7 @@ function serializeRawWatermarkIntoHost({ hostXml, rawWatermarkXml }) {
|
|
|
85
85
|
if (!rawHost || getLocalName(rawHost.name ?? "") !== "p") return rawWatermarkXml;
|
|
86
86
|
const watermarkContent = (rawHost.elements ?? []).filter((child) => child.type === "element" && getLocalName(child.name ?? "") !== "pPr").map((child) => captureVerbatimXml(child)).join("");
|
|
87
87
|
if (!watermarkContent) return rawWatermarkXml;
|
|
88
|
-
const namespaceAttributes = Object.entries(rawHost.attributes ?? {}).filter(([name, value]) => value !== void 0 && (name === "xmlns" || name.startsWith("xmlns:"))).map(([name, value]) => ` ${name}="${
|
|
88
|
+
const namespaceAttributes = Object.entries(rawHost.attributes ?? {}).filter(([name, value]) => value !== void 0 && (name === "xmlns" || name.startsWith("xmlns:"))).map(([name, value]) => ` ${name}="${escapeXmlAttribute(String(value))}"`).join("");
|
|
89
89
|
return hostXml.replace(/^<w:p(?=[\s>])/u, `<w:p${namespaceAttributes}`).replace("</w:p>", `${watermarkContent}</w:p>`);
|
|
90
90
|
}
|
|
91
91
|
function serializeWatermarkParagraph(hf) {
|
|
@@ -101,15 +101,15 @@ function synthesizeTextWatermark(watermark) {
|
|
|
101
101
|
const rotation = watermark.diagonal === false ? 0 : 315;
|
|
102
102
|
const fillcolor = watermark.color && watermark.color !== "auto" ? `#${watermark.color}` : "#C0C0C0";
|
|
103
103
|
const fontFamily = watermark.font ?? "Calibri";
|
|
104
|
-
const text =
|
|
105
|
-
return `<w:p><w:r><w:pict><v:shape id="PowerPlusWaterMarkObject1" type="#_x0000_t136" style="position:absolute;margin-left:0;margin-top:0;width:415pt;height:207pt;rotation:${rotation};z-index:-251658240;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin" fillcolor="${fillcolor}" stroked="f">${watermark.opacity !== void 0 ? `<v:fill opacity="${watermark.opacity}"/>` : ""}<v:textpath style="font-family:"${
|
|
104
|
+
const text = escapeXmlAttribute(watermark.text);
|
|
105
|
+
return `<w:p><w:r><w:pict><v:shape id="PowerPlusWaterMarkObject1" type="#_x0000_t136" style="position:absolute;margin-left:0;margin-top:0;width:415pt;height:207pt;rotation:${rotation};z-index:-251658240;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin" fillcolor="${fillcolor}" stroked="f">${watermark.opacity !== void 0 ? `<v:fill opacity="${watermark.opacity}"/>` : ""}<v:textpath style="font-family:"${escapeXmlAttribute(fontFamily)}";font-size:1pt" string="${text}"/></v:shape></w:pict></w:r></w:p>`;
|
|
106
106
|
}
|
|
107
107
|
const PICTURE_WATERMARK_DEFAULT_WIDTH_PT = 415;
|
|
108
108
|
const PICTURE_WATERMARK_DEFAULT_HEIGHT_PT = 207;
|
|
109
109
|
const PICTURE_WATERMARK_WASHOUT_GAIN = "19661f";
|
|
110
110
|
const PICTURE_WATERMARK_WASHOUT_BLACKLEVEL = "22938f";
|
|
111
111
|
function synthesizePictureWatermark(watermark) {
|
|
112
|
-
const rId =
|
|
112
|
+
const rId = escapeXmlAttribute(watermark.imageRId);
|
|
113
113
|
return `<w:p><w:r><w:pict><v:shape id="WordPictureWatermark1" type="#_x0000_t75" style="position:absolute;margin-left:0;margin-top:0;width:${watermark.scale === void 0 ? watermark.widthPt ?? PICTURE_WATERMARK_DEFAULT_WIDTH_PT : PICTURE_WATERMARK_DEFAULT_WIDTH_PT * watermark.scale}pt;height:${watermark.scale === void 0 ? watermark.heightPt ?? PICTURE_WATERMARK_DEFAULT_HEIGHT_PT : PICTURE_WATERMARK_DEFAULT_HEIGHT_PT * watermark.scale}pt;z-index:-251658240;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin"><v:imagedata r:id="${rId}" o:title=""${watermark.washout === false ? "" : ` gain="${PICTURE_WATERMARK_WASHOUT_GAIN}" blacklevel="${PICTURE_WATERMARK_WASHOUT_BLACKLEVEL}"`}/></v:shape></w:pict></w:r></w:p>`;
|
|
114
114
|
}
|
|
115
115
|
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
2
2
|
//#region src/docx/serializer/markupRangeAttributes.ts
|
|
3
|
-
const attribute = (name, value) => `${name}="${
|
|
3
|
+
const attribute = (name, value) => `${name}="${escapeXmlAttribute(String(value))}"`;
|
|
4
4
|
const markupRangeAttributes = (marker) => {
|
|
5
5
|
const attributes = [attribute("w:id", marker.id)];
|
|
6
6
|
if (marker.displacedByCustomXml !== void 0) attributes.push(attribute("w:displacedByCustomXml", marker.displacedByCustomXml));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serializePartElement } from "./partNamespaces.js";
|
|
2
2
|
import { serializeTextFormatting } from "./textFormattingSerializer.js";
|
|
3
|
-
import {
|
|
3
|
+
import { intAttr } from "./xmlUtils.js";
|
|
4
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
4
5
|
//#region src/docx/serializer/numberingSerializer.ts
|
|
5
6
|
/**
|
|
6
7
|
* Serialize a level's paragraph properties — the modeled subset is indentation
|
|
@@ -36,11 +37,11 @@ function serializeLevelParagraphProps(pPr) {
|
|
|
36
37
|
function serializeLevel(level) {
|
|
37
38
|
const parts = [];
|
|
38
39
|
if (level.start !== void 0) parts.push(`<w:start w:val="${intAttr(level.start)}"/>`);
|
|
39
|
-
parts.push(`<w:numFmt w:val="${
|
|
40
|
+
parts.push(`<w:numFmt w:val="${escapeXmlAttribute(level.numFmt)}"/>`);
|
|
40
41
|
if (level.lvlRestart !== void 0) parts.push(`<w:lvlRestart w:val="${intAttr(level.lvlRestart)}"/>`);
|
|
41
42
|
if (level.isLgl) parts.push("<w:isLgl/>");
|
|
42
43
|
if (level.suffix) parts.push(`<w:suff w:val="${level.suffix}"/>`);
|
|
43
|
-
parts.push(`<w:lvlText w:val="${
|
|
44
|
+
parts.push(`<w:lvlText w:val="${escapeXmlAttribute(level.lvlText)}"/>`);
|
|
44
45
|
if (level.legacy) {
|
|
45
46
|
const legacyAttrs = [`w:legacy="${level.legacy.legacy ? 1 : 0}"`];
|
|
46
47
|
if (level.legacy.legacySpace !== void 0) legacyAttrs.push(`w:legacySpace="${intAttr(level.legacy.legacySpace)}"`);
|
|
@@ -60,9 +61,9 @@ function serializeLevel(level) {
|
|
|
60
61
|
function serializeAbstractNum(abstractNum) {
|
|
61
62
|
const parts = [];
|
|
62
63
|
if (abstractNum.multiLevelType) parts.push(`<w:multiLevelType w:val="${abstractNum.multiLevelType}"/>`);
|
|
63
|
-
if (abstractNum.name !== void 0) parts.push(`<w:name w:val="${
|
|
64
|
-
if (abstractNum.styleLink !== void 0) parts.push(`<w:styleLink w:val="${
|
|
65
|
-
if (abstractNum.numStyleLink !== void 0) parts.push(`<w:numStyleLink w:val="${
|
|
64
|
+
if (abstractNum.name !== void 0) parts.push(`<w:name w:val="${escapeXmlAttribute(abstractNum.name)}"/>`);
|
|
65
|
+
if (abstractNum.styleLink !== void 0) parts.push(`<w:styleLink w:val="${escapeXmlAttribute(abstractNum.styleLink)}"/>`);
|
|
66
|
+
if (abstractNum.numStyleLink !== void 0) parts.push(`<w:numStyleLink w:val="${escapeXmlAttribute(abstractNum.numStyleLink)}"/>`);
|
|
66
67
|
const levels = [...abstractNum.levels].sort((a, b) => a.ilvl - b.ilvl);
|
|
67
68
|
for (const level of levels) parts.push(serializeLevel(level));
|
|
68
69
|
return `<w:abstractNum w:abstractNumId="${intAttr(abstractNum.abstractNumId)}">${parts.join("")}</w:abstractNum>`;
|