@stll/folio-core 0.49.0 → 0.51.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/headless.d.ts +37 -1
- package/dist/ai-edits/headless.js +47 -16
- package/dist/ai-edits/word-diff.js +134 -1
- package/dist/compare/content-alignment.js +220 -5
- package/dist/compare/content.d.ts +1 -0
- package/dist/compare/content.js +51 -20
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
- package/dist/display-list/dom/renderDisplayListToDom.js +8 -7
- package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
- package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
- package/dist/display-list/selectDisplayPages.d.ts +6 -0
- package/dist/display-list/selectDisplayPages.js +44 -0
- package/dist/docx/selectiveSave.d.ts +1 -1
- package/dist/docx/selectiveSave.js +16 -6
- package/dist/docx/structuralXmlPatch.d.ts +14 -0
- package/dist/docx/structuralXmlPatch.js +209 -0
- package/dist/export-pdf.d.ts +35 -2
- package/dist/export-pdf.js +51 -20
- package/dist/fonts/fontsourceFaces.d.ts +47 -0
- package/dist/fonts/fontsourceFaces.js +230 -0
- package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
- package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
- package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
- package/dist/layout-bridge/convert/flowBorders.js +31 -0
- package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
- package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
- package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
- package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
- package/dist/layout-bridge/convert/imageConversion.js +118 -0
- package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
- package/dist/layout-bridge/convert/listMarkers.js +98 -0
- package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
- package/dist/layout-bridge/convert/noteReferences.js +29 -0
- package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
- package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
- package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
- package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
- package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
- package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
- package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
- package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
- package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
- package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
- package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
- package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
- package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
- package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
- package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
- package/dist/layout-bridge/convert/tableConversion.js +303 -0
- package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
- package/dist/layout-bridge/convert/textBoxFill.js +42 -0
- package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
- package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
- package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2082
- package/dist/layout-bridge/engine/hitTest.js +1 -1
- package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
- package/dist/layout-bridge/engine/measuring/index.js +2 -1
- package/dist/layout-bridge/engine/selectionRects.js +1 -1
- package/dist/layout-engine/imageLayout.d.ts +9 -0
- package/dist/layout-engine/imageLayout.js +48 -0
- package/dist/layout-engine/index.d.ts +3 -18
- package/dist/layout-engine/index.js +12 -937
- package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
- package/dist/layout-engine/layoutFlowShared.js +8 -0
- package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
- package/dist/layout-engine/measure/crossRunWords.js +172 -0
- package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
- package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
- package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
- package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
- package/dist/layout-engine/measure/justification.d.ts +32 -0
- package/dist/layout-engine/measure/justification.js +83 -0
- package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
- package/dist/layout-engine/measure/lineFitting.js +145 -0
- package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
- package/dist/layout-engine/measure/lineTypography.js +124 -0
- package/dist/layout-engine/measure/measureBlocks.js +2 -1
- package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
- package/dist/layout-engine/measure/measureParagraph.js +32 -734
- package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
- package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
- package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
- package/dist/layout-engine/measure/paragraphTabs.js +78 -0
- package/dist/layout-engine/paragraphLayout.d.ts +23 -0
- package/dist/layout-engine/paragraphLayout.js +204 -0
- package/dist/layout-engine/sectionLayout.d.ts +45 -0
- package/dist/layout-engine/sectionLayout.js +216 -0
- package/dist/layout-engine/tableLayout.d.ts +17 -0
- package/dist/layout-engine/tableLayout.js +369 -0
- package/dist/layout-engine/textBoxLayout.d.ts +15 -0
- package/dist/layout-engine/textBoxLayout.js +137 -0
- package/dist/layout-painter/renderPage.js +1 -0
- package/dist/managers/editorShortcuts.d.ts +16 -3
- package/dist/managers/editorShortcuts.js +6 -4
- package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
- package/dist/paged-layout/sectionBlockWidths.js +1 -1
- package/dist/prosemirror/extensions/StarterKit.d.ts +3 -0
- package/dist/prosemirror/extensions/StarterKit.js +2 -1
- package/dist/prosemirror/extensions/core/HistoryExtension.d.ts +9 -1
- package/dist/prosemirror/extensions/core/HistoryExtension.js +4 -3
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +12 -5
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +91 -33
- package/dist/server.d.ts +3 -2
- package/package.json +2 -2
package/dist/compare/content.js
CHANGED
|
@@ -431,30 +431,61 @@ const separatorBetween = (whole, head, tail) => {
|
|
|
431
431
|
const separator = whole.slice(head.length, whole.length - tail.length);
|
|
432
432
|
return separator.length === 0 || /^\s+$/u.test(separator) ? separator : null;
|
|
433
433
|
};
|
|
434
|
+
const splitPlan = ({ baseBlock, head, tail }) => {
|
|
435
|
+
const separator = separatorBetween(baseBlock.text, head.text, tail.text);
|
|
436
|
+
return separator !== null && contentBlocksShareContainer(head, tail) ? {
|
|
437
|
+
type: "split",
|
|
438
|
+
baseBlock,
|
|
439
|
+
revisedBlocks: [head, tail],
|
|
440
|
+
offset: head.text.length,
|
|
441
|
+
separator
|
|
442
|
+
} : null;
|
|
443
|
+
};
|
|
444
|
+
const mergePlan = ({ revisedBlock, head, tail }) => {
|
|
445
|
+
const separator = separatorBetween(revisedBlock.text, head.text, tail.text);
|
|
446
|
+
return separator !== null && contentBlocksShareContainer(head, tail) ? {
|
|
447
|
+
type: "merge",
|
|
448
|
+
baseBlocks: [head, tail],
|
|
449
|
+
revisedBlock,
|
|
450
|
+
separator
|
|
451
|
+
} : null;
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* A pair next to a one-sided block its text spells together with the pair's
|
|
455
|
+
* other side. Alignment pairs a split paragraph with whichever half reads more
|
|
456
|
+
* like it, so the unpaired half may stand before the pair or after it.
|
|
457
|
+
*/
|
|
458
|
+
const paragraphMarkPlan = (step, next) => {
|
|
459
|
+
if (step.type === "pair" && next.type === "revisedOnly") return splitPlan({
|
|
460
|
+
baseBlock: step.baseBlock,
|
|
461
|
+
head: step.revisedBlock,
|
|
462
|
+
tail: next.block
|
|
463
|
+
});
|
|
464
|
+
if (step.type === "pair" && next.type === "baseOnly") return mergePlan({
|
|
465
|
+
revisedBlock: step.revisedBlock,
|
|
466
|
+
head: step.baseBlock,
|
|
467
|
+
tail: next.block
|
|
468
|
+
});
|
|
469
|
+
if (step.type === "revisedOnly" && next.type === "pair") return splitPlan({
|
|
470
|
+
baseBlock: next.baseBlock,
|
|
471
|
+
head: step.block,
|
|
472
|
+
tail: next.revisedBlock
|
|
473
|
+
});
|
|
474
|
+
if (step.type === "baseOnly" && next.type === "pair") return mergePlan({
|
|
475
|
+
revisedBlock: next.revisedBlock,
|
|
476
|
+
head: step.block,
|
|
477
|
+
tail: next.baseBlock
|
|
478
|
+
});
|
|
479
|
+
return null;
|
|
480
|
+
};
|
|
481
|
+
/** Plans keyed by their first step; each consumes the step after it. */
|
|
434
482
|
const detectFolioContentParagraphMarkPlans = (steps) => {
|
|
435
483
|
const plans = /* @__PURE__ */ new Map();
|
|
436
484
|
for (const [index, step] of steps.entries()) {
|
|
437
485
|
const next = steps[index + 1];
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (separator !== null && contentBlocksShareContainer(step.revisedBlock, next.block)) plans.set(index, {
|
|
442
|
-
type: "split",
|
|
443
|
-
baseBlock: step.baseBlock,
|
|
444
|
-
revisedBlocks: [step.revisedBlock, next.block],
|
|
445
|
-
offset: step.revisedBlock.text.length,
|
|
446
|
-
separator
|
|
447
|
-
});
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
if (next.type !== "baseOnly") continue;
|
|
451
|
-
const separator = separatorBetween(step.revisedBlock.text, step.baseBlock.text, next.block.text);
|
|
452
|
-
if (separator !== null && contentBlocksShareContainer(step.baseBlock, next.block)) plans.set(index, {
|
|
453
|
-
type: "merge",
|
|
454
|
-
baseBlocks: [step.baseBlock, next.block],
|
|
455
|
-
revisedBlock: step.revisedBlock,
|
|
456
|
-
separator
|
|
457
|
-
});
|
|
486
|
+
if (next === void 0 || plans.has(index - 1)) continue;
|
|
487
|
+
const plan = paragraphMarkPlan(step, next);
|
|
488
|
+
if (plan !== null) plans.set(index, plan);
|
|
458
489
|
}
|
|
459
490
|
return plans;
|
|
460
491
|
};
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { DisplayColor, DisplayFontFace, DisplayImageSource, DisplayList, DisplayPage } from "../types.js";
|
|
2
2
|
//#region src/display-list/dom/renderDisplayListToDom.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* How the backend addresses image and embedded-font bytes: `objectUrl` mints
|
|
5
|
+
* a `blob:` URL where the runtime can (a live page, which revokes them), and
|
|
6
|
+
* `dataUrl` always inlines the bytes (markup that leaves the process, such as
|
|
7
|
+
* a serialized page, where a `blob:` URL would resolve to nothing).
|
|
8
|
+
*/
|
|
9
|
+
type DisplayBinaryUrls = "objectUrl" | "dataUrl";
|
|
3
10
|
type RenderDisplayListOptions = {
|
|
4
11
|
/** Document to create elements in. */
|
|
5
12
|
readonly doc: Document;
|
|
6
13
|
/** Page background, painted before any primitive. */
|
|
7
14
|
readonly pageBackground?: DisplayColor;
|
|
15
|
+
/** Defaults to `objectUrl`. */
|
|
16
|
+
readonly binaryUrls?: DisplayBinaryUrls;
|
|
8
17
|
};
|
|
9
18
|
/**
|
|
10
19
|
* A page's glyph runs and images index tables that live on the list, and an
|
|
@@ -23,4 +32,4 @@ declare const renderDisplayPageToDom: (page: DisplayPage, options: RenderDisplay
|
|
|
23
32
|
/** One `div.layout-page` per display page, in page order. */
|
|
24
33
|
declare const renderDisplayListToDom: (list: DisplayList, options: RenderDisplayListOptions) => HTMLElement[];
|
|
25
34
|
//#endregion
|
|
26
|
-
export { RenderDisplayListOptions, RenderDisplayPageOptions, renderDisplayListToDom, renderDisplayPageToDom };
|
|
35
|
+
export { DisplayBinaryUrls, RenderDisplayListOptions, RenderDisplayPageOptions, renderDisplayListToDom, renderDisplayPageToDom };
|
|
@@ -103,8 +103,8 @@ const resolveImage = (ref, images) => {
|
|
|
103
103
|
};
|
|
104
104
|
/** A `Blob` part cannot be backed by shared memory, so prove it is not. */
|
|
105
105
|
const isBlobPart = (bytes) => bytes.buffer instanceof ArrayBuffer;
|
|
106
|
-
const binarySrc = (bytes, mimeType) => {
|
|
107
|
-
if (isBlobPart(bytes) && typeof Blob === "function" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function") return URL.createObjectURL(new Blob([bytes], { type: mimeType }));
|
|
106
|
+
const binarySrc = (bytes, mimeType, urls) => {
|
|
107
|
+
if (urls === "objectUrl" && isBlobPart(bytes) && typeof Blob === "function" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function") return URL.createObjectURL(new Blob([bytes], { type: mimeType }));
|
|
108
108
|
return bytesToDataUrl(bytes, mimeType);
|
|
109
109
|
};
|
|
110
110
|
const fontFaceRule = (face, embedded, src) => `@font-face { font-family: ${quoteFontFamily(embedded.id)}; src: url("${src}"); font-weight: ${face.weight}; font-style: ${face.italic ? "italic" : "normal"}; }`;
|
|
@@ -113,14 +113,14 @@ const fontFaceRule = (face, embedded, src) => `@font-face { font-family: ${quote
|
|
|
113
113
|
* once per render and shared by every page: an id maps to one blob, so the
|
|
114
114
|
* bytes are held once and one revoke releases them.
|
|
115
115
|
*/
|
|
116
|
-
const embeddedFontFaceCss = (fonts) => {
|
|
116
|
+
const embeddedFontFaceCss = (fonts, urls) => {
|
|
117
117
|
const srcById = /* @__PURE__ */ new Map();
|
|
118
118
|
const rules = [];
|
|
119
119
|
for (const face of fonts) {
|
|
120
120
|
const embedded = face.embedded;
|
|
121
121
|
if (embedded === void 0) continue;
|
|
122
122
|
const cached = srcById.get(embedded.id);
|
|
123
|
-
const src = cached ?? binarySrc(embedded.bytes, FONT_MIME_TYPE);
|
|
123
|
+
const src = cached ?? binarySrc(embedded.bytes, FONT_MIME_TYPE, urls);
|
|
124
124
|
if (cached === void 0) srcById.set(embedded.id, src);
|
|
125
125
|
rules.push(fontFaceRule(face, embedded, src));
|
|
126
126
|
}
|
|
@@ -399,7 +399,7 @@ const paintImage = ({ image, rect, crop, opacity, brightness, contrast }, contex
|
|
|
399
399
|
element.style.height = px(fullHeight);
|
|
400
400
|
}
|
|
401
401
|
element.alt = "";
|
|
402
|
-
element.src = binarySrc(source.bytes, IMAGE_MIME_TYPES[source.format]);
|
|
402
|
+
element.src = binarySrc(source.bytes, IMAGE_MIME_TYPES[source.format], context.binaryUrls);
|
|
403
403
|
clip.append(element);
|
|
404
404
|
context.parent.append(clip);
|
|
405
405
|
};
|
|
@@ -497,6 +497,7 @@ const renderPage = (page, options) => {
|
|
|
497
497
|
}
|
|
498
498
|
const context = {
|
|
499
499
|
doc: options.doc,
|
|
500
|
+
binaryUrls: options.binaryUrls ?? "objectUrl",
|
|
500
501
|
fonts: options.fonts,
|
|
501
502
|
images: options.images,
|
|
502
503
|
parent: element,
|
|
@@ -595,11 +596,11 @@ const paintRegionTree = ({ primitives, regions }, context) => {
|
|
|
595
596
|
};
|
|
596
597
|
const renderDisplayPageToDom = (page, options) => renderPage(page, {
|
|
597
598
|
...options,
|
|
598
|
-
fontFaceCss: embeddedFontFaceCss(options.fonts)
|
|
599
|
+
fontFaceCss: embeddedFontFaceCss(options.fonts, options.binaryUrls ?? "objectUrl")
|
|
599
600
|
});
|
|
600
601
|
/** One `div.layout-page` per display page, in page order. */
|
|
601
602
|
const renderDisplayListToDom = (list, options) => {
|
|
602
|
-
const fontFaceCss = embeddedFontFaceCss(list.fonts);
|
|
603
|
+
const fontFaceCss = embeddedFontFaceCss(list.fonts, options.binaryUrls ?? "objectUrl");
|
|
603
604
|
return list.pages.map((page, pageIndex) => renderPage(page, {
|
|
604
605
|
...options,
|
|
605
606
|
fonts: list.fonts,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DisplayColor, DisplayList } from "../types.js";
|
|
2
|
+
//#region src/display-list/html/renderDisplayListToHtml.d.ts
|
|
3
|
+
declare const escapeHtmlText: (value: string) => string;
|
|
4
|
+
type RenderDisplayListToHtmlOptions = {
|
|
5
|
+
/** `@font-face` rules for the families the list names, placed in the head. */
|
|
6
|
+
readonly fontFaceCss?: string;
|
|
7
|
+
/** The document title. */
|
|
8
|
+
readonly title?: string;
|
|
9
|
+
/** Space between pages. Defaults to 0: pages stack flush. */
|
|
10
|
+
readonly pageGapPx?: number;
|
|
11
|
+
/** CSS color behind the pages. Defaults to white. */
|
|
12
|
+
readonly canvasColor?: string;
|
|
13
|
+
/** Page background, painted before any primitive. */
|
|
14
|
+
readonly pageBackground?: DisplayColor;
|
|
15
|
+
};
|
|
16
|
+
/** The pages' markup, one slot per display page, in page order. */
|
|
17
|
+
declare const renderDisplayListPagesToHtml: (list: DisplayList, options?: Pick<RenderDisplayListToHtmlOptions, "pageBackground">) => string[];
|
|
18
|
+
/** A complete HTML document showing every page of the list. */
|
|
19
|
+
declare const renderDisplayListToHtml: (list: DisplayList, options?: RenderDisplayListToHtmlOptions) => string;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { RenderDisplayListToHtmlOptions, escapeHtmlText, renderDisplayListPagesToHtml, renderDisplayListToHtml };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { renderDisplayListToDom } from "../dom/renderDisplayListToDom.js";
|
|
2
|
+
//#region src/display-list/html/renderDisplayListToHtml.ts
|
|
3
|
+
/**
|
|
4
|
+
* A display list as one HTML document, with no browser and no DOM.
|
|
5
|
+
*
|
|
6
|
+
* The DOM backend paints into whatever `Document` it is handed. This module
|
|
7
|
+
* hands it a document with only what the backend reaches for and serializes
|
|
8
|
+
* the result, so a server, a CLI or a harness can produce the page a browser
|
|
9
|
+
* would show from the same painter the editor uses. Binary data (images,
|
|
10
|
+
* embedded faces) is inlined as `data:` URLs, because a `blob:` URL minted in
|
|
11
|
+
* this process resolves to nothing once the markup leaves it.
|
|
12
|
+
*/
|
|
13
|
+
const VOID_TAGS = /* @__PURE__ */ new Set([
|
|
14
|
+
"img",
|
|
15
|
+
"br",
|
|
16
|
+
"hr"
|
|
17
|
+
]);
|
|
18
|
+
const createStubElement = (tagName) => {
|
|
19
|
+
const children = [];
|
|
20
|
+
const attributes = {};
|
|
21
|
+
return {
|
|
22
|
+
tagName,
|
|
23
|
+
style: {},
|
|
24
|
+
dataset: {},
|
|
25
|
+
attributes,
|
|
26
|
+
children,
|
|
27
|
+
className: "",
|
|
28
|
+
id: "",
|
|
29
|
+
textContent: "",
|
|
30
|
+
alt: "",
|
|
31
|
+
src: "",
|
|
32
|
+
href: "",
|
|
33
|
+
title: "",
|
|
34
|
+
append: (...nodes) => {
|
|
35
|
+
children.push(...nodes);
|
|
36
|
+
},
|
|
37
|
+
appendChild: (node) => {
|
|
38
|
+
children.push(node);
|
|
39
|
+
return node;
|
|
40
|
+
},
|
|
41
|
+
setAttribute: (name, value) => {
|
|
42
|
+
attributes[name] = value;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A `Document` with only what the DOM backend reaches for. A backend that
|
|
48
|
+
* grows a new requirement throws on the missing property here rather than
|
|
49
|
+
* silently serializing less than it painted.
|
|
50
|
+
*/
|
|
51
|
+
const stubDocument = () => {
|
|
52
|
+
return { createElement: createStubElement };
|
|
53
|
+
};
|
|
54
|
+
const asStub = (element) => element;
|
|
55
|
+
/** A vendor property is camelCase with no leading capital, so its leading dash is explicit. */
|
|
56
|
+
const VENDOR_PREFIXES = [
|
|
57
|
+
"webkit-",
|
|
58
|
+
"moz-",
|
|
59
|
+
"ms-",
|
|
60
|
+
"o-"
|
|
61
|
+
];
|
|
62
|
+
const toKebabCase = (name) => {
|
|
63
|
+
const hyphenated = name.replaceAll(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);
|
|
64
|
+
return VENDOR_PREFIXES.some((prefix) => hyphenated.startsWith(prefix)) ? `-${hyphenated}` : hyphenated;
|
|
65
|
+
};
|
|
66
|
+
const escapeHtmlText = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
67
|
+
const escapeAttribute = (value) => escapeHtmlText(value).replaceAll("\"", """);
|
|
68
|
+
const serializeStyle = (style) => Object.entries(style).map(([property, value]) => `${toKebabCase(property)}: ${value}`).join("; ");
|
|
69
|
+
/**
|
|
70
|
+
* Serialized without one character of added whitespace: the backend sets
|
|
71
|
+
* `white-space: pre` on every run, so an indentation newline would be painted.
|
|
72
|
+
*/
|
|
73
|
+
const RAW_TEXT_TAGS = /* @__PURE__ */ new Set(["style", "script"]);
|
|
74
|
+
/** Text placed inside a `<style>` or `<script>`: a `</` cannot close the element early. */
|
|
75
|
+
const rawElementText = (text) => text.replaceAll("</", "<\\/");
|
|
76
|
+
const serializeElement = (element) => {
|
|
77
|
+
const attributes = [];
|
|
78
|
+
if (element.className !== "") attributes.push(`class="${escapeAttribute(element.className)}"`);
|
|
79
|
+
if (element.id !== "") attributes.push(`id="${escapeAttribute(element.id)}"`);
|
|
80
|
+
if (element.href !== "") attributes.push(`href="${escapeAttribute(element.href)}"`);
|
|
81
|
+
if (element.src !== "") attributes.push(`src="${escapeAttribute(element.src)}"`);
|
|
82
|
+
if (element.title !== "") attributes.push(`title="${escapeAttribute(element.title)}"`);
|
|
83
|
+
if (element.tagName === "img") attributes.push(`alt="${escapeAttribute(element.alt)}"`);
|
|
84
|
+
for (const [name, value] of Object.entries(element.attributes)) attributes.push(`${name}="${escapeAttribute(value)}"`);
|
|
85
|
+
for (const [key, value] of Object.entries(element.dataset)) attributes.push(`data-${toKebabCase(key)}="${escapeAttribute(value)}"`);
|
|
86
|
+
const style = serializeStyle(element.style);
|
|
87
|
+
if (style !== "") attributes.push(`style="${escapeAttribute(style)}"`);
|
|
88
|
+
const open = [element.tagName, ...attributes].join(" ");
|
|
89
|
+
if (VOID_TAGS.has(element.tagName)) return `<${open} />`;
|
|
90
|
+
if (element.children.length > 0) return `<${open}>${element.children.map(serializeElement).join("")}</${element.tagName}>`;
|
|
91
|
+
return `<${open}>${RAW_TEXT_TAGS.has(element.tagName) ? rawElementText(element.textContent) : escapeHtmlText(element.textContent)}</${element.tagName}>`;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Each page sits in a slot of whole pixels. A page height is fractional (A4 is
|
|
95
|
+
* 1122.52 px), so pages stacked in normal flow would start at fractional
|
|
96
|
+
* offsets; the slot rounds the offset while the page keeps its exact size.
|
|
97
|
+
*/
|
|
98
|
+
const pageSlot = (markup, page) => `<div class="layout-page-slot" style="position: relative; overflow: hidden; width: ${String(Math.ceil(page.widthPx))}px; height: ${String(Math.ceil(page.heightPx))}px">${markup}</div>`;
|
|
99
|
+
/** The pages' markup, one slot per display page, in page order. */
|
|
100
|
+
const renderDisplayListPagesToHtml = (list, options = {}) => renderDisplayListToDom(list, {
|
|
101
|
+
doc: stubDocument(),
|
|
102
|
+
binaryUrls: "dataUrl",
|
|
103
|
+
...options.pageBackground !== void 0 && { pageBackground: options.pageBackground }
|
|
104
|
+
}).map((element, index) => {
|
|
105
|
+
const markup = serializeElement(asStub(element));
|
|
106
|
+
const page = list.pages.at(index);
|
|
107
|
+
return page === void 0 ? markup : pageSlot(markup, page);
|
|
108
|
+
});
|
|
109
|
+
/** A complete HTML document showing every page of the list. */
|
|
110
|
+
const renderDisplayListToHtml = (list, options = {}) => {
|
|
111
|
+
const gap = options.pageGapPx ?? 0;
|
|
112
|
+
const canvas = rawElementText(options.canvasColor ?? "#fff");
|
|
113
|
+
const layout = gap > 0 ? `body { display: flex; flex-direction: column; align-items: center; gap: ${String(gap)}px; padding: ${String(gap)}px 0; }` : "";
|
|
114
|
+
return [
|
|
115
|
+
"<!doctype html>",
|
|
116
|
+
`<html><head><meta charset="utf-8"><title>${escapeHtmlText(options.title ?? "")}</title><style>`,
|
|
117
|
+
rawElementText(options.fontFaceCss ?? ""),
|
|
118
|
+
`html, body { margin: 0; padding: 0; background: ${canvas}; }`,
|
|
119
|
+
layout,
|
|
120
|
+
"</style></head><body>",
|
|
121
|
+
renderDisplayListPagesToHtml(list, options).join(""),
|
|
122
|
+
"</body></html>"
|
|
123
|
+
].join("\n");
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
export { escapeHtmlText, renderDisplayListPagesToHtml, renderDisplayListToHtml };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DisplayList } from "./types.js";
|
|
2
|
+
//#region src/display-list/selectDisplayPages.d.ts
|
|
3
|
+
/** Keep `pageIndices` (0-based, in the order given; duplicates are dropped). */
|
|
4
|
+
declare const selectDisplayPages: (list: DisplayList, pageIndices: readonly number[]) => DisplayList;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { selectDisplayPages };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/display-list/selectDisplayPages.ts
|
|
2
|
+
/** Keep `pageIndices` (0-based, in the order given; duplicates are dropped). */
|
|
3
|
+
const selectDisplayPages = (list, pageIndices) => {
|
|
4
|
+
const kept = [...new Set(pageIndices)].filter((index) => Number.isInteger(index) && index >= 0 && index < list.pages.length);
|
|
5
|
+
const remap = new Map(kept.map((original, next) => [original, next]));
|
|
6
|
+
const relink = (link) => {
|
|
7
|
+
if (link.target.kind !== "page") return [link];
|
|
8
|
+
const pageIndex = remap.get(link.target.pageIndex);
|
|
9
|
+
return pageIndex === void 0 ? [] : [{
|
|
10
|
+
...link,
|
|
11
|
+
target: {
|
|
12
|
+
...link.target,
|
|
13
|
+
pageIndex
|
|
14
|
+
}
|
|
15
|
+
}];
|
|
16
|
+
};
|
|
17
|
+
const pages = kept.flatMap((index) => {
|
|
18
|
+
const page = list.pages[index];
|
|
19
|
+
return page === void 0 ? [] : [{
|
|
20
|
+
...page,
|
|
21
|
+
links: page.links.flatMap(relink)
|
|
22
|
+
}];
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
...list,
|
|
26
|
+
pages,
|
|
27
|
+
outline: list.outline.flatMap((entry) => {
|
|
28
|
+
const pageIndex = remap.get(entry.pageIndex);
|
|
29
|
+
return pageIndex === void 0 ? [] : [{
|
|
30
|
+
...entry,
|
|
31
|
+
pageIndex
|
|
32
|
+
}];
|
|
33
|
+
}),
|
|
34
|
+
unsupported: list.unsupported.flatMap((entry) => {
|
|
35
|
+
const pageIndex = remap.get(entry.pageIndex);
|
|
36
|
+
return pageIndex === void 0 ? [] : [{
|
|
37
|
+
...entry,
|
|
38
|
+
pageIndex
|
|
39
|
+
}];
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { selectDisplayPages };
|
|
@@ -3,7 +3,7 @@ import { document_d_exports } from "../types/document.js";
|
|
|
3
3
|
type SelectiveSaveOptions = {
|
|
4
4
|
/** Changed paragraph IDs to selectively patch */
|
|
5
5
|
changedParaIds: Set<string>;
|
|
6
|
-
/** Whether
|
|
6
|
+
/** Whether paragraph membership, order, or block structure changed. */
|
|
7
7
|
structuralChange: boolean;
|
|
8
8
|
/** Whether any changes affected paragraphs without paraId */
|
|
9
9
|
hasUntrackedChanges: boolean;
|
|
@@ -8,12 +8,13 @@ import { isUnsafePackagePath } from "./packageParts.js";
|
|
|
8
8
|
import { RELATIONSHIP_TYPES } from "./relsParser.js";
|
|
9
9
|
import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedInlineResources, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
|
|
10
10
|
import "./selectiveSaveFlags.js";
|
|
11
|
-
import { buildPatchedDocumentXml, buildPatchedNoteXml, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
|
|
11
|
+
import { buildPatchedDocumentXml, buildPatchedNoteXml, collectAddedNumberingDefs, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
|
|
12
12
|
import { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
|
|
13
13
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
14
14
|
import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
|
|
15
15
|
import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
|
|
16
16
|
import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
|
|
17
|
+
import { buildStructuralDocumentPatch } from "./structuralXmlPatch.js";
|
|
17
18
|
import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
|
|
18
19
|
//#region src/docx/selectiveSave.ts
|
|
19
20
|
/**
|
|
@@ -174,7 +175,6 @@ const queueSettingsUpdates = async (zip, updates) => {
|
|
|
174
175
|
async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
175
176
|
const { changedParaIds, structuralChange, hasUntrackedChanges } = options;
|
|
176
177
|
const maxBytes = options.maxBytes ?? 104857600;
|
|
177
|
-
if (structuralChange) return null;
|
|
178
178
|
if (hasUntrackedChanges) return null;
|
|
179
179
|
if (originalBuffer.byteLength > maxBytes) return null;
|
|
180
180
|
const content = doc.package.document.content;
|
|
@@ -197,22 +197,32 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
197
197
|
const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
|
|
198
198
|
for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
|
|
199
199
|
const updates = /* @__PURE__ */ new Map();
|
|
200
|
-
if (changedParaIds.size > 0) {
|
|
200
|
+
if (changedParaIds.size > 0 || structuralChange) {
|
|
201
201
|
const docXmlFile = zip.file("word/document.xml");
|
|
202
202
|
if (!docXmlFile) return null;
|
|
203
203
|
const originalDocXml = await docXmlFile.async("text");
|
|
204
204
|
const serializedDocXml = serializeDocument(doc, readRootNamespaceBindings(originalDocXml));
|
|
205
205
|
const bodyParaIds = collectParaIds(serializedDocXml);
|
|
206
|
+
const originalBodyParaIds = structuralChange ? collectParaIds(originalDocXml) : void 0;
|
|
207
|
+
if (structuralChange && doc.package.numbering) {
|
|
208
|
+
const numberingFile = findZipEntryCaseInsensitive(zip, "word/numbering.xml");
|
|
209
|
+
const added = collectAddedNumberingDefs(numberingFile ? serializeNumberingXml(parseNumbering(await numberingFile.async("text")).definitions) : "", serializeNumberingXml(doc.package.numbering));
|
|
210
|
+
if (added.abstractNums.size > 0 || added.nums.size > 0) return null;
|
|
211
|
+
}
|
|
206
212
|
const bodyChangedIds = /* @__PURE__ */ new Set();
|
|
207
213
|
const noteCandidateIds = /* @__PURE__ */ new Set();
|
|
208
|
-
for (const id of changedParaIds) if (bodyParaIds.has(id)) bodyChangedIds.add(id);
|
|
214
|
+
for (const id of changedParaIds) if (bodyParaIds.has(id) || originalBodyParaIds?.has(id)) bodyChangedIds.add(id);
|
|
209
215
|
else noteCandidateIds.add(id);
|
|
210
216
|
if (noteCandidateIds.size > 0) {
|
|
211
217
|
const unrouted = await patchNoteParts(zip, doc, noteCandidateIds, updates);
|
|
212
218
|
if (unrouted === null || unrouted.size > 0) return null;
|
|
213
219
|
}
|
|
214
|
-
if (bodyChangedIds.size > 0) {
|
|
215
|
-
const patchedDocXml =
|
|
220
|
+
if (bodyChangedIds.size > 0 || structuralChange) {
|
|
221
|
+
const patchedDocXml = structuralChange ? buildStructuralDocumentPatch({
|
|
222
|
+
originalXml: originalDocXml,
|
|
223
|
+
serializedXml: serializedDocXml,
|
|
224
|
+
changedIds: bodyChangedIds
|
|
225
|
+
}) : buildPatchedDocumentXml(originalDocXml, serializedDocXml, bodyChangedIds);
|
|
216
226
|
if (!patchedDocXml) return null;
|
|
217
227
|
updates.set("word/document.xml", patchedDocXml);
|
|
218
228
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/docx/structuralXmlPatch.d.ts
|
|
2
|
+
type StructuralPatchOptions = {
|
|
3
|
+
originalXml: string;
|
|
4
|
+
serializedXml: string;
|
|
5
|
+
changedIds: ReadonlySet<string>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Insert/delete direct body paragraphs between surviving paragraph/table anchors.
|
|
9
|
+
* Tables are opaque barriers: their modeled content and order must stay identical.
|
|
10
|
+
* Id-less sources need an explicit ensureParaIds ingest before structural editing.
|
|
11
|
+
*/
|
|
12
|
+
declare const buildStructuralDocumentPatch: ({ originalXml, serializedXml, changedIds }: StructuralPatchOptions) => string | null;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { buildStructuralDocumentPatch };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { canonicalJson } from "../utils/canonicalJson.js";
|
|
2
|
+
import { parseDocumentBody } from "./documentParser.js";
|
|
3
|
+
import { paraIdAttribute } from "./paraIdAttribute.js";
|
|
4
|
+
import { spliceXml } from "./selectiveXmlPatch.js";
|
|
5
|
+
import { readRootNamespaceBindings, serializePartElement } from "./serializer/partNamespaces.js";
|
|
6
|
+
import { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, getChildElements, getLocalName, getNamespaceUri, parseXmlDocument } from "./xmlParser.js";
|
|
7
|
+
//#region src/docx/structuralXmlPatch.ts
|
|
8
|
+
/** Conservative body-paragraph splices; source offsets never come from reserialization. */
|
|
9
|
+
const isWordElement = (element, name) => getLocalName(element.name) === name && WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "");
|
|
10
|
+
/** Match XML tokens, skipping quoted delimiters, comments, CDATA and processing instructions. */
|
|
11
|
+
const XML_TOKEN = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<\?[\s\S]*?\?>|<(?:"[^"]*"|'[^']*'|[^'">])*>/gu;
|
|
12
|
+
const readBody = (xml) => {
|
|
13
|
+
const root = parseXmlDocument(xml);
|
|
14
|
+
if (!root || !isWordElement(root, "document")) return null;
|
|
15
|
+
if (getNamespaceUri(root) !== NAMESPACES.w) return null;
|
|
16
|
+
const body = getChildElements(root).find((child) => isWordElement(child, "body"));
|
|
17
|
+
if (!body) return null;
|
|
18
|
+
const children = getChildElements(body);
|
|
19
|
+
const blocks = [];
|
|
20
|
+
const stack = [];
|
|
21
|
+
let bodyDepth = -1;
|
|
22
|
+
let bodyEnd = -1;
|
|
23
|
+
let start = -1;
|
|
24
|
+
for (const token of xml.matchAll(XML_TOKEN)) {
|
|
25
|
+
const tag = token[0];
|
|
26
|
+
if (tag.startsWith("<!") || tag.startsWith("<?")) continue;
|
|
27
|
+
const closing = tag.startsWith("</");
|
|
28
|
+
const name = tag.slice(closing ? 2 : 1).split(/[\s/>]/u).at(0);
|
|
29
|
+
if (!name) return null;
|
|
30
|
+
if (closing) {
|
|
31
|
+
if (stack.pop() !== name) return null;
|
|
32
|
+
if (stack.length === bodyDepth) {
|
|
33
|
+
const element = children[blocks.length];
|
|
34
|
+
if (!element || start < 0) return null;
|
|
35
|
+
blocks.push({
|
|
36
|
+
element,
|
|
37
|
+
start,
|
|
38
|
+
end: token.index + tag.length
|
|
39
|
+
});
|
|
40
|
+
start = -1;
|
|
41
|
+
}
|
|
42
|
+
if (name === body.name && stack.length === bodyDepth - 1) bodyEnd = token.index;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (name === body.name && stack.length === 1) bodyDepth = 2;
|
|
46
|
+
if (stack.length === bodyDepth) {
|
|
47
|
+
if (children[blocks.length]?.name !== name) return null;
|
|
48
|
+
start = token.index;
|
|
49
|
+
if (tag.endsWith("/>")) {
|
|
50
|
+
const element = children[blocks.length];
|
|
51
|
+
if (!element) return null;
|
|
52
|
+
blocks.push({
|
|
53
|
+
element,
|
|
54
|
+
start,
|
|
55
|
+
end: token.index + tag.length
|
|
56
|
+
});
|
|
57
|
+
start = -1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!tag.endsWith("/>")) stack.push(name);
|
|
61
|
+
}
|
|
62
|
+
if (stack.length > 0 || blocks.length !== children.length || bodyEnd < 0) return null;
|
|
63
|
+
return {
|
|
64
|
+
root,
|
|
65
|
+
blocks,
|
|
66
|
+
bodyEnd
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
function* descendants(element) {
|
|
70
|
+
yield element;
|
|
71
|
+
for (const child of getChildElements(element)) yield* descendants(child);
|
|
72
|
+
}
|
|
73
|
+
/** Cross-paragraph ranges require a wider edit contract than paragraph identity. */
|
|
74
|
+
const RANGE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
75
|
+
"commentRangeStart",
|
|
76
|
+
"commentRangeEnd",
|
|
77
|
+
"commentReference",
|
|
78
|
+
"bookmarkStart",
|
|
79
|
+
"bookmarkEnd",
|
|
80
|
+
"permStart",
|
|
81
|
+
"permEnd",
|
|
82
|
+
"fldChar",
|
|
83
|
+
"moveFromRangeStart",
|
|
84
|
+
"moveFromRangeEnd",
|
|
85
|
+
"moveToRangeStart",
|
|
86
|
+
"moveToRangeEnd"
|
|
87
|
+
]);
|
|
88
|
+
const DEPENDENT_ELEMENTS = /* @__PURE__ */ new Set([
|
|
89
|
+
"sectPr",
|
|
90
|
+
"footnoteReference",
|
|
91
|
+
"endnoteReference",
|
|
92
|
+
"drawing",
|
|
93
|
+
"pict",
|
|
94
|
+
"object"
|
|
95
|
+
]);
|
|
96
|
+
const paragraphIsSafe = (element, touched) => {
|
|
97
|
+
for (const child of descendants(element)) {
|
|
98
|
+
if (!WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(child) ?? "")) continue;
|
|
99
|
+
const name = getLocalName(child.name);
|
|
100
|
+
if (touched && DEPENDENT_ELEMENTS.has(name)) return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
};
|
|
104
|
+
const paragraphIds = (root) => {
|
|
105
|
+
const ids = /* @__PURE__ */ new Set();
|
|
106
|
+
for (const element of descendants(root)) {
|
|
107
|
+
if (!isWordElement(element, "p")) continue;
|
|
108
|
+
const id = paraIdAttribute(element);
|
|
109
|
+
if (!id || !/^[0-9a-f]{8}$/iu.test(id) || /^0+$/u.test(id) || ids.has(id.toUpperCase())) return null;
|
|
110
|
+
ids.add(id.toUpperCase());
|
|
111
|
+
}
|
|
112
|
+
return ids;
|
|
113
|
+
};
|
|
114
|
+
/** Bind fragment prefixes locally: the source root may use entirely different aliases. */
|
|
115
|
+
const paragraphFragment = ({ xml, block, bindings }) => {
|
|
116
|
+
const fragment = xml.slice(block.start, block.end);
|
|
117
|
+
const openEnd = fragment.indexOf(">");
|
|
118
|
+
const name = block.element.name ?? "w:p";
|
|
119
|
+
const selfClosing = fragment[openEnd - 1] === "/";
|
|
120
|
+
return serializePartElement({
|
|
121
|
+
partPath: "word/document.xml",
|
|
122
|
+
rootName: name,
|
|
123
|
+
rootAttributes: fragment.slice(name.length + 1, selfClosing ? openEnd - 1 : openEnd).trim(),
|
|
124
|
+
baselinePrefixes: [],
|
|
125
|
+
sourceBindings: bindings,
|
|
126
|
+
body: selfClosing ? "" : fragment.slice(openEnd + 1, fragment.lastIndexOf("</"))
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Insert/delete direct body paragraphs between surviving paragraph/table anchors.
|
|
131
|
+
* Tables are opaque barriers: their modeled content and order must stay identical.
|
|
132
|
+
* Id-less sources need an explicit ensureParaIds ingest before structural editing.
|
|
133
|
+
*/
|
|
134
|
+
const buildStructuralDocumentPatch = ({ originalXml, serializedXml, changedIds }) => {
|
|
135
|
+
const source = readBody(originalXml);
|
|
136
|
+
const current = readBody(serializedXml);
|
|
137
|
+
if (!source || !current) return null;
|
|
138
|
+
for (const root of [source.root, current.root]) for (const element of descendants(root)) if (WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "") && RANGE_ELEMENTS.has(getLocalName(element.name))) return null;
|
|
139
|
+
const allSourceIds = paragraphIds(source.root);
|
|
140
|
+
const allCurrentIds = paragraphIds(current.root);
|
|
141
|
+
if (!allSourceIds || !allCurrentIds) return null;
|
|
142
|
+
const changed = new Set([...changedIds].map((id) => id.toUpperCase()));
|
|
143
|
+
for (const { element } of [...source.blocks, ...current.blocks]) if (isWordElement(element, "p")) {
|
|
144
|
+
const id = paraIdAttribute(element)?.toUpperCase();
|
|
145
|
+
if (!id) return null;
|
|
146
|
+
const touched = changed.has(id) || !allSourceIds.has(id) || !allCurrentIds.has(id);
|
|
147
|
+
if (!paragraphIsSafe(element, touched)) return null;
|
|
148
|
+
} else if (!isWordElement(element, "tbl") && !isWordElement(element, "sectPr")) return null;
|
|
149
|
+
const sourceBody = parseDocumentBody(originalXml);
|
|
150
|
+
const currentBody = parseDocumentBody(serializedXml);
|
|
151
|
+
const barriers = (body) => ({
|
|
152
|
+
blocks: body.content.filter((block) => block.type !== "paragraph"),
|
|
153
|
+
finalSectionProperties: body.finalSectionProperties,
|
|
154
|
+
background: body.background
|
|
155
|
+
});
|
|
156
|
+
if (canonicalJson(barriers(sourceBody)) !== canonicalJson(barriers(currentBody))) return null;
|
|
157
|
+
const indexBlocks = (blocks) => {
|
|
158
|
+
let barrier = 0;
|
|
159
|
+
const indexed = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const block of blocks) {
|
|
161
|
+
const key = isWordElement(block.element, "p") ? `p:${paraIdAttribute(block.element)?.toUpperCase()}` : `barrier:${barrier++}`;
|
|
162
|
+
indexed.set(key, block);
|
|
163
|
+
}
|
|
164
|
+
return indexed;
|
|
165
|
+
};
|
|
166
|
+
const before = indexBlocks(source.blocks);
|
|
167
|
+
const after = indexBlocks(current.blocks);
|
|
168
|
+
const survivingBefore = [...before.keys()].filter((key) => after.has(key));
|
|
169
|
+
const survivingAfter = [...after.keys()].filter((key) => before.has(key));
|
|
170
|
+
if (canonicalJson(survivingBefore) !== canonicalJson(survivingAfter)) return null;
|
|
171
|
+
const bindings = readRootNamespaceBindings(serializedXml);
|
|
172
|
+
const fragmentFor = (block) => paragraphFragment({
|
|
173
|
+
xml: serializedXml,
|
|
174
|
+
block,
|
|
175
|
+
bindings
|
|
176
|
+
});
|
|
177
|
+
const splices = [];
|
|
178
|
+
for (const [key, block] of before) if (!after.has(key)) splices.push({
|
|
179
|
+
start: block.start,
|
|
180
|
+
end: block.end,
|
|
181
|
+
newXml: ""
|
|
182
|
+
});
|
|
183
|
+
let pending = [];
|
|
184
|
+
for (const [key, block] of after) {
|
|
185
|
+
const original = before.get(key);
|
|
186
|
+
if (!original) {
|
|
187
|
+
const id = paraIdAttribute(block.element)?.toUpperCase();
|
|
188
|
+
if (!id || allSourceIds.has(id)) return null;
|
|
189
|
+
pending.push(fragmentFor(block));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const id = paraIdAttribute(block.element)?.toUpperCase();
|
|
193
|
+
const replacement = id !== void 0 && changed.has(id) ? fragmentFor(block) : originalXml.slice(original.start, original.end);
|
|
194
|
+
if (pending.length > 0 || replacement !== originalXml.slice(original.start, original.end)) splices.push({
|
|
195
|
+
start: original.start,
|
|
196
|
+
end: original.end,
|
|
197
|
+
newXml: pending.join("") + replacement
|
|
198
|
+
});
|
|
199
|
+
pending = [];
|
|
200
|
+
}
|
|
201
|
+
if (pending.length > 0) splices.push({
|
|
202
|
+
start: source.bodyEnd,
|
|
203
|
+
end: source.bodyEnd,
|
|
204
|
+
newXml: pending.join("")
|
|
205
|
+
});
|
|
206
|
+
return spliceXml(originalXml, splices);
|
|
207
|
+
};
|
|
208
|
+
//#endregion
|
|
209
|
+
export { buildStructuralDocumentPatch };
|