@stll/folio-core 0.12.0 → 0.13.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/extractDocxText.js +55 -8
- package/dist/docx/settingsParser.js +15 -2
- package/dist/docx/tableParser.d.ts +8 -1
- package/dist/docx/tableParser.js +13 -3
- 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 +16 -3
- package/dist/layout-engine/measure/cache.js +2 -0
- package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
- 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 +9 -1
- 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 +3 -2
- 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/render-dom/RemoteSelectionOverlay.js +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +3 -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,5 +1,6 @@
|
|
|
1
1
|
import { deterministicHexId } from "../utils/hexId.js";
|
|
2
2
|
import { isXmlNameBoundary } from "./selectiveXmlPatch.js";
|
|
3
|
+
import { loadDocxArchive } from "./server/boundedArchive.js";
|
|
3
4
|
import { TaggedError } from "better-result";
|
|
4
5
|
import JSZip from "jszip";
|
|
5
6
|
//#region src/docx/ensureParaIds.ts
|
|
@@ -350,18 +351,17 @@ const hasDigitalSignatureParts = (zip) => Object.values(zip.files).some(({ dir,
|
|
|
350
351
|
* the buffer is not a WordprocessingML package or a part is malformed.
|
|
351
352
|
*/
|
|
352
353
|
const ensureParaIdsInternal = async (docx, options) => {
|
|
353
|
-
const
|
|
354
|
-
const xmlPartNames =
|
|
355
|
-
zip.forEach((relativePath, entry) => {
|
|
354
|
+
const archive = await loadDocxArchive(docx);
|
|
355
|
+
const xmlPartNames = archive.entries.filter((relativePath) => {
|
|
356
356
|
const lower = relativePath.toLowerCase();
|
|
357
|
-
|
|
357
|
+
return lower.startsWith("word/") && lower.endsWith(".xml");
|
|
358
358
|
});
|
|
359
359
|
const documentPartName = xmlPartNames.find((name) => name.toLowerCase() === DOCUMENT_PART);
|
|
360
360
|
if (documentPartName === void 0) throw createEnsureParaIdsError("word/document.xml not found: not a WordprocessingML package");
|
|
361
361
|
const partTexts = /* @__PURE__ */ new Map();
|
|
362
362
|
for (const name of xmlPartNames) {
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
363
|
+
const text = await archive.readEntryString(name);
|
|
364
|
+
if (text !== null) partTexts.set(name, text);
|
|
365
365
|
}
|
|
366
366
|
const taken = /* @__PURE__ */ new Set();
|
|
367
367
|
for (const text of partTexts.values()) collectExistingParaIds(text, taken);
|
|
@@ -390,6 +390,7 @@ const ensureParaIdsInternal = async (docx, options) => {
|
|
|
390
390
|
deduplicated: 0,
|
|
391
391
|
alreadyComplete: true
|
|
392
392
|
};
|
|
393
|
+
const zip = await JSZip.loadAsync(docx);
|
|
393
394
|
if (hasDigitalSignatureParts(zip) && options.allowSignedPackageMutation !== true) throw createEnsureParaIdsError("Refusing to normalize a digitally signed package because rewriting OOXML invalidates its signatures. Warn the user and pass allowSignedPackageMutation only if invalidation is acceptable.");
|
|
394
395
|
for (const [partPath, content] of updates) zip.file(partPath, content, {
|
|
395
396
|
compression: "DEFLATE",
|
|
@@ -150,12 +150,17 @@ const groupViewBox = (group, width, height) => {
|
|
|
150
150
|
};
|
|
151
151
|
};
|
|
152
152
|
const createSvg = (group, width, height, rels, media) => {
|
|
153
|
-
const
|
|
153
|
+
const children = getChildElements(group).slice(0, MAX_GROUP_SHAPES);
|
|
154
|
+
let content = "";
|
|
155
|
+
for (const [index, child] of children.entries()) {
|
|
154
156
|
const localName = getLocalName(child.name ?? "");
|
|
155
|
-
|
|
156
|
-
if (localName
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
let piece;
|
|
158
|
+
if (localName === "pic") piece = renderPicture(child, index, rels, media);
|
|
159
|
+
else if (localName === "wsp") piece = findChildByLocalName(child, "txbx") ? renderTextBox(child) : renderGeometry(child);
|
|
160
|
+
else continue;
|
|
161
|
+
content += piece;
|
|
162
|
+
if (content.length > MAX_SVG_CHARACTERS) return null;
|
|
163
|
+
}
|
|
159
164
|
if (!content) return null;
|
|
160
165
|
const viewBox = groupViewBox(group, width, height);
|
|
161
166
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}" width="${emuToPixels(width)}" height="${emuToPixels(height)}">${content}</svg>`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAttribute, getChildElements, mergeXmlnsDeclarations, parseNumericAttribute } from "./xmlParser.js";
|
|
2
|
-
import { parseRun } from "./runParser.js";
|
|
3
2
|
import { sanitizeExternalUrl, sanitizeLinkTarget } from "../utils/urlSecurity.js";
|
|
3
|
+
import { parseRun } from "./runParser.js";
|
|
4
4
|
//#region src/docx/hyperlinkParser.ts
|
|
5
5
|
/**
|
|
6
6
|
* Get the local name of an element (without namespace prefix)
|
package/dist/docx/imageParser.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { findByFullName, findChild, getAttribute, getChildElements, parseNumericAttribute } from "./xmlParser.js";
|
|
2
2
|
import { emuToPixels } from "../utils/units.js";
|
|
3
|
+
import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
|
|
4
|
+
import { sanitizeImageSrc } from "../utils/sanitizeImageSrc.js";
|
|
3
5
|
import { WRAP_ELEMENT_NAMES, parsePositionH, parsePositionV, parseWrapElement } from "./drawingUtils.js";
|
|
4
6
|
import { resolveTarget } from "./relsParser.js";
|
|
5
7
|
import { isTextBoxDrawing } from "./textBoxParser.js";
|
|
@@ -326,7 +328,8 @@ function parseInline(inlineEl, rels, media) {
|
|
|
326
328
|
if (props.alt) image.alt = props.alt;
|
|
327
329
|
if (props.title) image.title = props.title;
|
|
328
330
|
if (props.decorative) image.decorative = true;
|
|
329
|
-
|
|
331
|
+
const safeSrc = sanitizeImageSrc(imageData.src);
|
|
332
|
+
if (safeSrc) image.src = safeSrc;
|
|
330
333
|
if (imageData.mimeType) image.mimeType = imageData.mimeType;
|
|
331
334
|
if (imageData.filename) image.filename = imageData.filename;
|
|
332
335
|
if (padding) image.padding = padding;
|
|
@@ -334,8 +337,8 @@ function parseInline(inlineEl, rels, media) {
|
|
|
334
337
|
if (crop) image.crop = crop;
|
|
335
338
|
if (opacity !== void 0) image.opacity = opacity;
|
|
336
339
|
if (props.hlinkRId && rels) {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
340
|
+
const safeHref = sanitizeExternalUrl(resolveTarget(rels, props.hlinkRId));
|
|
341
|
+
if (safeHref) image.hlinkHref = safeHref;
|
|
339
342
|
}
|
|
340
343
|
return image;
|
|
341
344
|
}
|
|
@@ -391,7 +394,8 @@ function parseAnchor(anchorEl, rels, media) {
|
|
|
391
394
|
if (props.alt) image.alt = props.alt;
|
|
392
395
|
if (props.title) image.title = props.title;
|
|
393
396
|
if (props.decorative) image.decorative = true;
|
|
394
|
-
|
|
397
|
+
const safeSrc = sanitizeImageSrc(imageData.src);
|
|
398
|
+
if (safeSrc) image.src = safeSrc;
|
|
395
399
|
if (imageData.mimeType) image.mimeType = imageData.mimeType;
|
|
396
400
|
if (imageData.filename) image.filename = imageData.filename;
|
|
397
401
|
if (position) image.position = position;
|
|
@@ -402,8 +406,8 @@ function parseAnchor(anchorEl, rels, media) {
|
|
|
402
406
|
if (layoutInCell !== void 0) image.layoutInCell = layoutInCell;
|
|
403
407
|
if (allowOverlap !== void 0) image.allowOverlap = allowOverlap;
|
|
404
408
|
if (props.hlinkRId && rels) {
|
|
405
|
-
const
|
|
406
|
-
if (
|
|
409
|
+
const safeHref = sanitizeExternalUrl(resolveTarget(rels, props.hlinkRId));
|
|
410
|
+
if (safeHref) image.hlinkHref = safeHref;
|
|
407
411
|
}
|
|
408
412
|
return image;
|
|
409
413
|
}
|
|
@@ -44,6 +44,20 @@ const XML_DECLARATION_PATTERN = /^\s*<\?xml[^?]*\?>/u;
|
|
|
44
44
|
const MAX_INPUT_BYTES = 50 * 1024 * 1024;
|
|
45
45
|
const MAX_ARCHIVE_ENTRIES = 5e3;
|
|
46
46
|
const MAX_CORE_PROPERTIES_BYTES = 1024 * 1024;
|
|
47
|
+
const CORE_PROPERTIES_PATH_LOWER = "docprops/core.xml";
|
|
48
|
+
/**
|
|
49
|
+
* Look up a package part by its conventional lowercase path, falling back to
|
|
50
|
+
* a case-insensitive scan. OPC part names are case-insensitive, so a
|
|
51
|
+
* producer that wrote `docProps/Core.xml` (or another casing) must still be
|
|
52
|
+
* found — otherwise the privacy rewrite silently no-ops and the unscrubbed
|
|
53
|
+
* buffer round-trips untouched.
|
|
54
|
+
*/
|
|
55
|
+
const findZipEntryCaseInsensitive = (zip, lowerPath) => {
|
|
56
|
+
const direct = zip.file(lowerPath);
|
|
57
|
+
if (direct) return direct;
|
|
58
|
+
for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.toLowerCase() === lowerPath) return file;
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
47
61
|
const loadPrivacyArchive = async (buffer) => {
|
|
48
62
|
if (buffer.byteLength > MAX_INPUT_BYTES) throw new FolioDocumentPrivacyArchiveError({
|
|
49
63
|
message: "Document privacy input exceeded the compressed-size limit",
|
|
@@ -63,7 +77,7 @@ const loadPrivacyArchive = async (buffer) => {
|
|
|
63
77
|
message: "Document privacy input exceeded the package-entry limit",
|
|
64
78
|
reason: "too-many-entries"
|
|
65
79
|
});
|
|
66
|
-
const coreProperties = zip
|
|
80
|
+
const coreProperties = findZipEntryCaseInsensitive(zip, CORE_PROPERTIES_PATH_LOWER);
|
|
67
81
|
if (!coreProperties) return zip;
|
|
68
82
|
const data = "_data" in coreProperties ? coreProperties._data : void 0;
|
|
69
83
|
const declaredBytes = typeof data === "object" && data !== null && "uncompressedSize" in data ? data.uncompressedSize : void 0;
|
|
@@ -99,7 +113,7 @@ const rewriteCorePropertiesPrivacy = (xml, transforms) => {
|
|
|
99
113
|
const rewriteDocxMetadataPrivacy = async (buffer, { transforms }) => {
|
|
100
114
|
const appliedTransforms = resolveFolioDocumentPrivacyTransforms(transforms);
|
|
101
115
|
const zip = await loadPrivacyArchive(buffer);
|
|
102
|
-
const coreProperties = zip
|
|
116
|
+
const coreProperties = findZipEntryCaseInsensitive(zip, CORE_PROPERTIES_PATH_LOWER);
|
|
103
117
|
if (!coreProperties) return {
|
|
104
118
|
buffer,
|
|
105
119
|
privacyReport: {
|
|
@@ -115,7 +129,7 @@ const rewriteDocxMetadataPrivacy = async (buffer, { transforms }) => {
|
|
|
115
129
|
removedMetadataProperties: []
|
|
116
130
|
}
|
|
117
131
|
};
|
|
118
|
-
zip.file(
|
|
132
|
+
zip.file(coreProperties.name, rewritten.xml);
|
|
119
133
|
return {
|
|
120
134
|
buffer: await zip.generateAsync({
|
|
121
135
|
type: "arraybuffer",
|
|
@@ -2,11 +2,13 @@ import { BorderStyleSchema, FrameWrapSchema, FrameXAlignSchema, FrameYAlignSchem
|
|
|
2
2
|
import { elementToXml, findChild, findChildren, getAttribute, getChildElements, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
|
|
3
3
|
import { parseRun, parseRunProperties } from "./runParser.js";
|
|
4
4
|
import { parseFieldType } from "./fieldParser.js";
|
|
5
|
+
import { isValidHexId } from "../utils/hexId.js";
|
|
5
6
|
import { parseBookmarkEnd as parseBookmarkEnd$1, parseBookmarkStart as parseBookmarkStart$1 } from "./bookmarkParser.js";
|
|
6
7
|
import { parseHyperlink as parseHyperlink$1 } from "./hyperlinkParser.js";
|
|
7
8
|
import { consolidateParagraphContent } from "./runConsolidator.js";
|
|
8
9
|
import { parseSdtProperties } from "./sdtProperties.js";
|
|
9
10
|
import { parseSectionProperties } from "./sectionParser.js";
|
|
11
|
+
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
10
12
|
//#region src/docx/paragraphParser.ts
|
|
11
13
|
/**
|
|
12
14
|
* Extract plain text from a math element (recursive text content extraction)
|
|
@@ -486,11 +488,13 @@ function parseTrackedChangeInfo(node) {
|
|
|
486
488
|
const rawDate = getAttribute(node, "w", "date");
|
|
487
489
|
const author = rawAuthor?.trim() ?? "";
|
|
488
490
|
const date = rawDate?.trim() ?? "";
|
|
491
|
+
const initials = (getAttribute(node, "w", "initials") ?? "").trim();
|
|
489
492
|
const info = {
|
|
490
|
-
id:
|
|
493
|
+
id: normalizeRevisionId(parsedId),
|
|
491
494
|
author: author.length > 0 ? author : "Unknown"
|
|
492
495
|
};
|
|
493
496
|
if (date.length > 0) info.date = date;
|
|
497
|
+
if (initials.length > 0) info.initials = initials;
|
|
494
498
|
return info;
|
|
495
499
|
}
|
|
496
500
|
function parsePropertyChangeInfo(node) {
|
|
@@ -939,9 +943,9 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
|
|
|
939
943
|
content: []
|
|
940
944
|
};
|
|
941
945
|
const paraId = getAttribute(node, "w14", "paraId") ?? getAttribute(node, "w", "paraId");
|
|
942
|
-
if (paraId) paragraph.paraId = paraId;
|
|
946
|
+
if (paraId && isValidHexId(paraId)) paragraph.paraId = paraId;
|
|
943
947
|
const textId = getAttribute(node, "w14", "textId") ?? getAttribute(node, "w", "textId");
|
|
944
|
-
if (textId) paragraph.textId = textId;
|
|
948
|
+
if (textId && isValidHexId(textId)) paragraph.textId = textId;
|
|
945
949
|
if (!options?.inHeaderFooter && paragraphStartsWithRenderedPageBreak(node)) paragraph.renderedPageBreakBefore = true;
|
|
946
950
|
const pPr = findChild(node, "w", "pPr");
|
|
947
951
|
if (pPr) {
|
package/dist/docx/rezip.js
CHANGED
|
@@ -4,7 +4,8 @@ import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
|
|
|
4
4
|
import { assertValidFolioDocumentModel } from "./modelValidation.js";
|
|
5
5
|
import { isPreservableDocxEntry } from "./unzip.js";
|
|
6
6
|
import { applyReplyThreadMarkers } from "./commentReplyMarkers.js";
|
|
7
|
-
import {
|
|
7
|
+
import { withoutOrphanCommentRanges } from "./commentRangeIntegrity.js";
|
|
8
|
+
import { buildParagraphOffsetIndex, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds } from "./selectiveXmlPatch.js";
|
|
8
9
|
import { escapeXml } from "./serializer/xmlUtils.js";
|
|
9
10
|
import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
|
|
10
11
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
@@ -15,6 +16,7 @@ import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
|
|
|
15
16
|
import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
|
|
16
17
|
import { serializeStylesXml } from "./serializer/stylesSerializer.js";
|
|
17
18
|
import { serializeThemeXml } from "./serializer/themeSerializer.js";
|
|
19
|
+
import { isAllowedExternalWatermarkImageUrl } from "../watermark/index.js";
|
|
18
20
|
import { panic } from "better-result";
|
|
19
21
|
import JSZip from "jszip";
|
|
20
22
|
//#region src/docx/rezip.ts
|
|
@@ -369,7 +371,7 @@ async function processNewHyperlinks(newHyperlinks, zip, compressionLevel) {
|
|
|
369
371
|
async function repackDocx(doc, options = {}) {
|
|
370
372
|
if (!doc.originalBuffer) panic("Cannot repack document: no original buffer for round-trip. Use createDocx() for new documents.");
|
|
371
373
|
const { compressionLevel = 6, updateModifiedDate = true, modifiedBy } = options;
|
|
372
|
-
const exportDocument = doc;
|
|
374
|
+
const exportDocument = withoutOrphanCommentRanges(doc);
|
|
373
375
|
const originalZip = await JSZip.loadAsync(doc.originalBuffer);
|
|
374
376
|
const newZip = new JSZip();
|
|
375
377
|
for (const [path, file] of Object.entries(originalZip.files)) {
|
|
@@ -431,7 +433,7 @@ async function repackDocx(doc, options = {}) {
|
|
|
431
433
|
*/
|
|
432
434
|
async function repackDocxFromRaw(doc, rawContent, options = {}) {
|
|
433
435
|
const { compressionLevel = 6, updateModifiedDate = true, modifiedBy } = options;
|
|
434
|
-
const exportDocument = doc;
|
|
436
|
+
const exportDocument = withoutOrphanCommentRanges(doc);
|
|
435
437
|
const newZip = new JSZip();
|
|
436
438
|
for (const [path, file] of Object.entries(rawContent.originalZip.files)) {
|
|
437
439
|
if (file.dir) {
|
|
@@ -854,10 +856,11 @@ async function rebindWatermarkRelIds(doc, zip, compressionLevel) {
|
|
|
854
856
|
const changedPaths = /* @__PURE__ */ new Set();
|
|
855
857
|
for (const { watermark, relsPath, partPath } of pending) {
|
|
856
858
|
let canonical;
|
|
857
|
-
if (watermark.imageTarget !== void 0) canonical = watermark.
|
|
859
|
+
if (watermark.imageTarget !== void 0) if (watermark.imageTargetExternal) canonical = isAllowedExternalWatermarkImageUrl(watermark.imageTarget) ? {
|
|
858
860
|
mode: "external",
|
|
859
861
|
url: watermark.imageTarget
|
|
860
|
-
} :
|
|
862
|
+
} : resolveCanonical(watermark.imageRId);
|
|
863
|
+
else canonical = {
|
|
861
864
|
mode: "internal",
|
|
862
865
|
absolute: watermark.imageTarget
|
|
863
866
|
};
|
|
@@ -1000,15 +1003,23 @@ async function patchNotePartIntoZip(conventionalLowerPath, currentXml, baselineF
|
|
|
1000
1003
|
* (re-parsed original) serialization — i.e. the ones actually edited. A
|
|
1001
1004
|
* paragraph is only considered when its `paraId` resolves uniquely in both,
|
|
1002
1005
|
* so it can be spliced safely.
|
|
1006
|
+
*
|
|
1007
|
+
* Builds one {@link buildParagraphOffsetIndex} per side (a single linear scan
|
|
1008
|
+
* each) instead of calling `extractParagraphXml` per candidate id, which
|
|
1009
|
+
* re-scanned the whole XML per id — O(note count * XML size) for a document
|
|
1010
|
+
* with many footnotes/endnotes. The index turns each lookup below into O(1).
|
|
1003
1011
|
*/
|
|
1004
1012
|
function collectChangedNoteParaIds(baselineXml, currentXml) {
|
|
1005
1013
|
const changed = /* @__PURE__ */ new Set();
|
|
1006
1014
|
const baselineIds = collectParaIds(baselineXml);
|
|
1015
|
+
const baselineOffsets = buildParagraphOffsetIndex(baselineXml);
|
|
1016
|
+
const currentOffsets = buildParagraphOffsetIndex(currentXml);
|
|
1007
1017
|
for (const [id, count] of collectParaIds(currentXml)) {
|
|
1008
1018
|
if (count !== 1 || baselineIds.get(id) !== 1) continue;
|
|
1009
|
-
const
|
|
1010
|
-
const
|
|
1011
|
-
if (
|
|
1019
|
+
const beforeRange = baselineOffsets.get(id);
|
|
1020
|
+
const afterRange = currentOffsets.get(id);
|
|
1021
|
+
if (!beforeRange || !afterRange) continue;
|
|
1022
|
+
if (baselineXml.slice(beforeRange.start, beforeRange.end) !== currentXml.slice(afterRange.start, afterRange.end)) changed.add(id);
|
|
1012
1023
|
}
|
|
1013
1024
|
return changed;
|
|
1014
1025
|
}
|
package/dist/docx/runParser.js
CHANGED
|
@@ -5,8 +5,17 @@ import { parseGroupDrawing } from "./groupDrawingParser.js";
|
|
|
5
5
|
import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapeParser.js";
|
|
6
6
|
import { parseVmlImageContent } from "./vmlImageParser.js";
|
|
7
7
|
import { resolveThemeFontRef } from "./themeParser.js";
|
|
8
|
+
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
8
9
|
//#region src/docx/runParser.ts
|
|
9
10
|
/**
|
|
11
|
+
* Sanity cap on `w:lang` `@w:val`/`@w:eastAsia`/`@w:bidi` tag length. BCP-47
|
|
12
|
+
* tags top out well under this; a hostile/corrupt tag here would drive the
|
|
13
|
+
* hyphenation dictionary lookup and segmenter cache keying with an
|
|
14
|
+
* attacker-sized string per run.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_LANGUAGE_TAG_LENGTH = 35;
|
|
17
|
+
const truncateLanguageTag = (value) => value === void 0 ? void 0 : value.slice(0, MAX_LANGUAGE_TAG_LENGTH);
|
|
18
|
+
/**
|
|
10
19
|
* Parse color value from attributes
|
|
11
20
|
*/
|
|
12
21
|
function parseColorValue(rgb, themeColor, themeTint, themeShade) {
|
|
@@ -267,9 +276,9 @@ function parseRunProperties(rPr, theme, _styles) {
|
|
|
267
276
|
}
|
|
268
277
|
const lang = propertyChildren.lang;
|
|
269
278
|
if (lang) {
|
|
270
|
-
const val = getAttribute(lang, "w", "val") || void 0;
|
|
271
|
-
const eastAsia = getAttribute(lang, "w", "eastAsia") || void 0;
|
|
272
|
-
const bidi = getAttribute(lang, "w", "bidi") || void 0;
|
|
279
|
+
const val = truncateLanguageTag(getAttribute(lang, "w", "val") || void 0);
|
|
280
|
+
const eastAsia = truncateLanguageTag(getAttribute(lang, "w", "eastAsia") || void 0);
|
|
281
|
+
const bidi = truncateLanguageTag(getAttribute(lang, "w", "bidi") || void 0);
|
|
273
282
|
if (val || eastAsia || bidi) formatting.language = {
|
|
274
283
|
...val ? { val } : {},
|
|
275
284
|
...eastAsia ? { eastAsia } : {},
|
|
@@ -332,7 +341,7 @@ function parsePropertyChangeInfo(changeElement) {
|
|
|
332
341
|
const date = (getAttribute(changeElement, "w", "date") ?? "").trim();
|
|
333
342
|
const rsid = (getAttribute(changeElement, "w", "rsid") ?? "").trim();
|
|
334
343
|
const info = {
|
|
335
|
-
id:
|
|
344
|
+
id: normalizeRevisionId(parsedId),
|
|
336
345
|
author: author.length > 0 ? author : "Unknown"
|
|
337
346
|
};
|
|
338
347
|
if (date.length > 0) info.date = date;
|
|
@@ -2,7 +2,20 @@ import { BorderStyleSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnu
|
|
|
2
2
|
import { findChild, findChildren, getAttribute, getChildElements, getLocalName, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
|
|
3
3
|
import { parseFooterReference, parseHeaderReference } from "./headerFooterRefParser.js";
|
|
4
4
|
import { parseEndnoteProperties, parseFootnoteProperties } from "./notePropertiesParser.js";
|
|
5
|
+
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
5
6
|
//#region src/docx/sectionParser.ts
|
|
7
|
+
/**
|
|
8
|
+
* Sanity cap on `w:cols/@w:num`. Word's column picker tops out well below
|
|
9
|
+
* this; a hostile/corrupt value here would force the layout engine to
|
|
10
|
+
* generate a proportional band per column.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_SECTION_COLUMNS = 45;
|
|
13
|
+
/**
|
|
14
|
+
* Sanity cap on `w:pgSz` `@w:w`/`@w:h`, in twips (~22in). Matches the
|
|
15
|
+
* existing `w:defaultTabStop` cap (`settingsParser.ts`) and bounds the
|
|
16
|
+
* ruler tick generators, which are sized off the page dimensions.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_PAGE_DIMENSION_TWIPS = 31680;
|
|
6
19
|
const serializedSectionPropertyChildNames = /* @__PURE__ */ new Set([
|
|
7
20
|
"headerReference",
|
|
8
21
|
"footerReference",
|
|
@@ -55,7 +68,7 @@ function parsePropertyChangeInfo(node) {
|
|
|
55
68
|
const date = (getAttribute(node, "w", "date") ?? "").trim();
|
|
56
69
|
const rsid = (getAttribute(node, "w", "rsid") ?? "").trim();
|
|
57
70
|
const info = {
|
|
58
|
-
id:
|
|
71
|
+
id: normalizeRevisionId(parsedId),
|
|
59
72
|
author: author.length > 0 ? author : "Unknown"
|
|
60
73
|
};
|
|
61
74
|
if (date.length > 0) info.date = date;
|
|
@@ -170,9 +183,9 @@ function parseSectionProperties(sectPr) {
|
|
|
170
183
|
const pgSz = findChild(sectPr, "w", "pgSz");
|
|
171
184
|
if (pgSz) {
|
|
172
185
|
const w = parseNumericAttribute(pgSz, "w", "w");
|
|
173
|
-
if (w !== void 0) props.pageWidth = w;
|
|
186
|
+
if (w !== void 0) props.pageWidth = Math.min(w, MAX_PAGE_DIMENSION_TWIPS);
|
|
174
187
|
const h = parseNumericAttribute(pgSz, "w", "h");
|
|
175
|
-
if (h !== void 0) props.pageHeight = h;
|
|
188
|
+
if (h !== void 0) props.pageHeight = Math.min(h, MAX_PAGE_DIMENSION_TWIPS);
|
|
176
189
|
const orientation = parseOrientation(getAttribute(pgSz, "w", "orient"));
|
|
177
190
|
if (orientation) props.orientation = orientation;
|
|
178
191
|
}
|
|
@@ -196,7 +209,7 @@ function parseSectionProperties(sectPr) {
|
|
|
196
209
|
const cols = findChild(sectPr, "w", "cols");
|
|
197
210
|
if (cols) {
|
|
198
211
|
const num = parseNumericAttribute(cols, "w", "num");
|
|
199
|
-
if (num !== void 0) props.columnCount = num;
|
|
212
|
+
if (num !== void 0) props.columnCount = Math.min(num, MAX_SECTION_COLUMNS);
|
|
200
213
|
const space = parseNumericAttribute(cols, "w", "space");
|
|
201
214
|
if (space !== void 0) props.columnSpace = space;
|
|
202
215
|
const equalWidth = getAttribute(cols, "w", "equalWidth");
|
|
@@ -2,7 +2,9 @@ import { parseNumbering } from "./numberingParser.js";
|
|
|
2
2
|
import { RELATIONSHIP_TYPES } from "./relsParser.js";
|
|
3
3
|
import { parseCommentsExtended } from "./commentParser.js";
|
|
4
4
|
import { validateFolioDocumentModel } from "./modelValidation.js";
|
|
5
|
+
import { isPreservableDocxEntry } from "./unzip.js";
|
|
5
6
|
import { hasUnsynthesizedReplyRanges } from "./commentReplyMarkers.js";
|
|
7
|
+
import { withoutOrphanCommentRanges } from "./commentRangeIntegrity.js";
|
|
6
8
|
import { buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds } from "./selectiveXmlPatch.js";
|
|
7
9
|
import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
|
|
8
10
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
@@ -182,6 +184,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
182
184
|
if (hasNewImagesOrHyperlinks(content)) return null;
|
|
183
185
|
if (hasUnmaterializedHeaderFooter(doc)) return null;
|
|
184
186
|
if (hasModelDrivenPictureWatermark(doc)) return null;
|
|
187
|
+
if (withoutOrphanCommentRanges(doc) !== doc) return null;
|
|
185
188
|
if (hasUnsynthesizedReplyRanges(doc)) return null;
|
|
186
189
|
if (!validateFolioDocumentModel(doc).valid) return null;
|
|
187
190
|
const comments = doc.package.document.comments ?? [];
|
|
@@ -189,6 +192,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
189
192
|
const headerFooterUpdates = collectHeaderFooterUpdates(doc);
|
|
190
193
|
try {
|
|
191
194
|
const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
|
|
195
|
+
for (const [path, file] of Object.entries(zip.files)) if (!file.dir && !isPreservableDocxEntry(path)) return null;
|
|
192
196
|
const updates = /* @__PURE__ */ new Map();
|
|
193
197
|
if (changedParaIds.size > 0) {
|
|
194
198
|
const docXmlFile = zip.file("word/document.xml");
|
|
@@ -33,6 +33,27 @@ declare function findParagraphOffsets(xml: string, paraId: string): {
|
|
|
33
33
|
* from a fully serialized document.xml string.
|
|
34
34
|
*/
|
|
35
35
|
declare function extractParagraphXml(serializedXml: string, paraId: string): string | null;
|
|
36
|
+
type ParagraphOffsets = {
|
|
37
|
+
start: number;
|
|
38
|
+
end: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Single linear-scan index of every `<w:p>` element's start/end offsets,
|
|
42
|
+
* keyed by `w14:paraId`. Generalizes {@link findParagraphOffsets} (one
|
|
43
|
+
* regex-scan-plus-depth-walk per lookup) to build every paragraph's offsets
|
|
44
|
+
* in a single pass, so a caller that needs many paragraphs from the same XML
|
|
45
|
+
* (e.g. `collectChangedNoteParaIds` in rezip.ts, walking every note paraId)
|
|
46
|
+
* does O(1) map lookups afterward instead of re-scanning the whole XML once
|
|
47
|
+
* per id — O(ids * XML length) collapses to O(XML length).
|
|
48
|
+
*
|
|
49
|
+
* A `<w:p>` nested inside another (e.g. inside `mc:AlternateContent`) is
|
|
50
|
+
* indexed too via a depth stack, matching every element `findParagraphOffsets`
|
|
51
|
+
* can resolve. A paraId that appears on more than one element is ambiguous —
|
|
52
|
+
* mirroring {@link findParagraphOffsets}'s single-match requirement — and is
|
|
53
|
+
* omitted from the index, so a lookup misses exactly where the per-id
|
|
54
|
+
* function would return null.
|
|
55
|
+
*/
|
|
56
|
+
declare function buildParagraphOffsetIndex(xml: string): Map<string, ParagraphOffsets>;
|
|
36
57
|
/**
|
|
37
58
|
* Count <w:p> elements in an XML string (top-level paragraph count).
|
|
38
59
|
* Counts opening <w:p tags that are NOT self-closing.
|
|
@@ -109,4 +130,4 @@ declare function collectChangedNumberingDefs(baselineXml: string, currentXml: st
|
|
|
109
130
|
*/
|
|
110
131
|
declare function buildPatchedNumberingXml(originalXml: string, currentXml: string, changed: ChangedNumberingDefs): string | null;
|
|
111
132
|
//#endregion
|
|
112
|
-
export { ChangedNumberingDefs, PatchSafetyOptions, PatchValidationResult, buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, validatePatchSafety };
|
|
133
|
+
export { ChangedNumberingDefs, ParagraphOffsets, PatchSafetyOptions, PatchValidationResult, buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, validatePatchSafety };
|
|
@@ -74,6 +74,66 @@ function extractParagraphXml(serializedXml, paraId) {
|
|
|
74
74
|
return serializedXml.slice(offsets.start, offsets.end);
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
+
* Single linear-scan index of every `<w:p>` element's start/end offsets,
|
|
78
|
+
* keyed by `w14:paraId`. Generalizes {@link findParagraphOffsets} (one
|
|
79
|
+
* regex-scan-plus-depth-walk per lookup) to build every paragraph's offsets
|
|
80
|
+
* in a single pass, so a caller that needs many paragraphs from the same XML
|
|
81
|
+
* (e.g. `collectChangedNoteParaIds` in rezip.ts, walking every note paraId)
|
|
82
|
+
* does O(1) map lookups afterward instead of re-scanning the whole XML once
|
|
83
|
+
* per id — O(ids * XML length) collapses to O(XML length).
|
|
84
|
+
*
|
|
85
|
+
* A `<w:p>` nested inside another (e.g. inside `mc:AlternateContent`) is
|
|
86
|
+
* indexed too via a depth stack, matching every element `findParagraphOffsets`
|
|
87
|
+
* can resolve. A paraId that appears on more than one element is ambiguous —
|
|
88
|
+
* mirroring {@link findParagraphOffsets}'s single-match requirement — and is
|
|
89
|
+
* omitted from the index, so a lookup misses exactly where the per-id
|
|
90
|
+
* function would return null.
|
|
91
|
+
*/
|
|
92
|
+
function buildParagraphOffsetIndex(xml) {
|
|
93
|
+
const seenCount = /* @__PURE__ */ new Map();
|
|
94
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
95
|
+
const stack = [];
|
|
96
|
+
let pos = 0;
|
|
97
|
+
const noteOpen = (paraId) => {
|
|
98
|
+
if (paraId) seenCount.set(paraId, (seenCount.get(paraId) ?? 0) + 1);
|
|
99
|
+
};
|
|
100
|
+
const noteRange = (paraId, start, end) => {
|
|
101
|
+
if (paraId) ranges.set(paraId, {
|
|
102
|
+
start,
|
|
103
|
+
end
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
while (pos < xml.length) {
|
|
107
|
+
const tagStart = xml.indexOf("<", pos);
|
|
108
|
+
if (tagStart === -1) break;
|
|
109
|
+
if (xml.startsWith("</w:p>", tagStart)) {
|
|
110
|
+
const frame = stack.pop();
|
|
111
|
+
const end = tagStart + 6;
|
|
112
|
+
if (frame) noteRange(frame.paraId, frame.start, end);
|
|
113
|
+
pos = end;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!xml.startsWith("<w:p", tagStart) || !isXmlNameBoundary(xml[tagStart + 4])) {
|
|
117
|
+
pos = tagStart + 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const tagEnd = xml.indexOf(">", tagStart);
|
|
121
|
+
if (tagEnd === -1) break;
|
|
122
|
+
const openTag = xml.slice(tagStart, tagEnd + 1);
|
|
123
|
+
const paraId = /\bw14:paraId="(?<id>[^"]*)"/u.exec(openTag)?.groups?.["id"];
|
|
124
|
+
noteOpen(paraId);
|
|
125
|
+
if (xml[tagEnd - 1] === "/") noteRange(paraId, tagStart, tagEnd + 1);
|
|
126
|
+
else stack.push({
|
|
127
|
+
start: tagStart,
|
|
128
|
+
paraId
|
|
129
|
+
});
|
|
130
|
+
pos = tagEnd + 1;
|
|
131
|
+
}
|
|
132
|
+
const index = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const [id, range] of ranges) if (seenCount.get(id) === 1) index.set(id, range);
|
|
134
|
+
return index;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
77
137
|
* Count <w:p> elements in an XML string (top-level paragraph count).
|
|
78
138
|
* Counts opening <w:p tags that are NOT self-closing.
|
|
79
139
|
*/
|
|
@@ -484,4 +544,4 @@ function escapeRegExp(str) {
|
|
|
484
544
|
return str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
485
545
|
}
|
|
486
546
|
//#endregion
|
|
487
|
-
export { buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, validatePatchSafety };
|
|
547
|
+
export { buildParagraphOffsetIndex, buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, validatePatchSafety };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSingleWellFormedElement } from "./xmlUtils.js";
|
|
1
2
|
import { reconcileRawSdtPr } from "../sdtPropertiesPatch.js";
|
|
2
3
|
//#region src/docx/serializer/blockSdtSerializer.ts
|
|
3
4
|
function escapeXmlAttr(value) {
|
|
@@ -73,14 +74,14 @@ function extractDateFullDate(blockSdt) {
|
|
|
73
74
|
}
|
|
74
75
|
function serializeBlockSdt(blockSdt, serializeChild) {
|
|
75
76
|
const props = blockSdt.properties;
|
|
76
|
-
const baseSdtPr = props.rawPropertiesXml
|
|
77
|
+
const baseSdtPr = props.rawPropertiesXml && isSingleWellFormedElement(props.rawPropertiesXml, "sdtPr") ? props.rawPropertiesXml : serializeFallbackSdtPr(props);
|
|
77
78
|
const dateFullDate = extractDateFullDate(blockSdt);
|
|
78
79
|
const dropdownLastValue = extractDropdownLastValue(blockSdt);
|
|
79
80
|
const sdtPrXml = reconcileRawSdtPr(baseSdtPr, props, {
|
|
80
81
|
...dateFullDate !== void 0 ? { dateFullDate } : {},
|
|
81
82
|
...dropdownLastValue !== void 0 ? { dropdownLastValue } : {}
|
|
82
83
|
});
|
|
83
|
-
const sdtEndPrXml = props.rawEndPropertiesXml
|
|
84
|
+
const sdtEndPrXml = props.rawEndPropertiesXml && isSingleWellFormedElement(props.rawEndPropertiesXml, "sdtEndPr") ? props.rawEndPropertiesXml : "";
|
|
84
85
|
const contentXml = blockSdt.content.map(serializeChild).join("");
|
|
85
86
|
return `<w:sdt>${sdtPrXml}${sdtEndPrXml}${props.rawSdtChildrenBeforeContent ?? ""}<w:sdtContent>${contentXml}</w:sdtContent>${props.rawSdtChildrenAfterContent ?? ""}</w:sdt>`;
|
|
86
87
|
}
|
|
@@ -27,8 +27,8 @@ function serializeRunContent(run) {
|
|
|
27
27
|
*/
|
|
28
28
|
function commentParagraphOpenTag(p) {
|
|
29
29
|
const attrs = [];
|
|
30
|
-
if (p.paraId) attrs.push(`w14:paraId="${p.paraId}"`);
|
|
31
|
-
if (p.textId) attrs.push(`w14:textId="${p.textId}"`);
|
|
30
|
+
if (p.paraId) attrs.push(`w14:paraId="${escapeXml(p.paraId)}"`);
|
|
31
|
+
if (p.textId) attrs.push(`w14:textId="${escapeXml(p.textId)}"`);
|
|
32
32
|
return attrs.length > 0 ? `<w:p ${attrs.join(" ")}>` : "<w:p>";
|
|
33
33
|
}
|
|
34
34
|
function serializeParagraph(p) {
|
|
@@ -173,8 +173,8 @@ function serializeCommentsExtended(comments) {
|
|
|
173
173
|
if (!entries) return null;
|
|
174
174
|
let xml = COMMENTS_EXTENDED_HEADER;
|
|
175
175
|
for (const entry of entries) {
|
|
176
|
-
const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${entry.paraIdParent}"` : "";
|
|
177
|
-
xml += `<w15:commentEx w15:paraId="${entry.paraId}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
|
|
176
|
+
const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
|
|
177
|
+
xml += `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
|
|
178
178
|
}
|
|
179
179
|
xml += "</w15:commentsEx>";
|
|
180
180
|
return xml;
|