@stll/folio-core 0.48.0 → 0.50.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.js +565 -121
- package/dist/ai-edits/headless.d.ts +37 -1
- package/dist/ai-edits/headless.js +47 -16
- package/dist/ai-edits/minimal-replacement.d.ts +64 -0
- package/dist/ai-edits/minimal-replacement.js +282 -0
- package/dist/controller/layoutPipeline.js +11 -1
- package/dist/display-list/build/paragraphPrimitives.js +63 -7
- package/dist/display-list/build/textBoxPrimitives.js +1 -1
- package/dist/display-list/build/unsupported.d.ts +2 -2
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
- package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
- 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/blockContentParser.js +2 -2
- package/dist/docx/bulletMarkers.d.ts +18 -2
- package/dist/docx/bulletMarkers.js +27 -2
- package/dist/docx/drawingGroupChildren.d.ts +60 -0
- package/dist/docx/drawingGroupChildren.js +176 -0
- package/dist/docx/drawingUtils.js +3 -0
- package/dist/docx/groupDrawingParser.d.ts +7 -1
- package/dist/docx/groupDrawingParser.js +123 -32
- package/dist/docx/packageParts.d.ts +45 -1
- package/dist/docx/packageParts.js +93 -2
- package/dist/docx/paraIdAttribute.d.ts +3 -2
- package/dist/docx/paragraphParser.js +91 -7
- package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
- package/dist/docx/runParser.js +10 -0
- package/dist/docx/selectiveSaveFlags.d.ts +10 -3
- package/dist/docx/selectiveSaveFlags.js +1 -1
- package/dist/docx/selectiveXmlPatch.d.ts +56 -34
- package/dist/docx/selectiveXmlPatch.js +222 -115
- package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
- package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
- package/dist/docx/serializer/paragraphSerializer.js +5 -4
- package/dist/docx/serializer/runSerializer.d.ts +5 -1
- package/dist/docx/serializer/runSerializer.js +4 -3
- package/dist/docx/shapeAlternateContent.d.ts +25 -0
- package/dist/docx/shapeAlternateContent.js +52 -0
- package/dist/docx/textBoxParser.d.ts +10 -2
- package/dist/docx/textBoxParser.js +9 -3
- 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/internal/headlessRevisionResolution.js +50 -13
- 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 -2060
- 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 +4 -19
- package/dist/layout-engine/index.js +14 -910
- package/dist/layout-engine/keep-together.d.ts +5 -2
- package/dist/layout-engine/keep-together.js +5 -1
- package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
- package/dist/layout-engine/layoutFlowShared.js +8 -0
- package/dist/layout-engine/measure/advanceComposition.js +49 -3
- 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 +7 -4
- package/dist/layout-engine/measure/measureContainer.js +46 -3
- package/dist/layout-engine/measure/measureHelpers.js +13 -3
- package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
- package/dist/layout-engine/measure/measureParagraph.js +41 -780
- 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/measure/smallCapsCasing.d.ts +56 -0
- package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
- package/dist/layout-engine/paginator.d.ts +2 -0
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphLayout.d.ts +23 -0
- package/dist/layout-engine/paragraphLayout.js +204 -0
- package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
- package/dist/layout-engine/paragraphSpacing.js +2 -1
- 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/tableRowBreak.d.ts +8 -1
- package/dist/layout-engine/tableRowBreak.js +22 -1
- package/dist/layout-engine/textBoxLayout.d.ts +15 -0
- package/dist/layout-engine/textBoxLayout.js +137 -0
- package/dist/layout-engine/types.d.ts +21 -2
- package/dist/layout-painter/renderPage.js +1 -0
- package/dist/layout-painter/renderParagraph.js +41 -4
- package/dist/layout-painter/renderTextBox.d.ts +12 -2
- package/dist/layout-painter/renderTextBox.js +26 -2
- package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
- package/dist/paged-layout/sectionBlockWidths.js +1 -1
- package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
- package/dist/prosemirror/alternateContentAttrs.js +35 -0
- package/dist/prosemirror/attrs/index.js +90 -0
- package/dist/prosemirror/commands/comments.js +51 -41
- package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
- package/dist/prosemirror/contentControlRevisions.js +84 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
- package/dist/prosemirror/conversion/toProseDoc.js +42 -19
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
- package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
- package/dist/prosemirror/listMarker.js +2 -2
- package/dist/prosemirror/paragraphDirection.d.ts +29 -2
- package/dist/prosemirror/paragraphDirection.js +21 -2
- package/dist/prosemirror/plugins/suggestionMode.js +37 -8
- package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
- package/dist/prosemirror/rejoinRunCarriers.js +66 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
- package/dist/prosemirror/schema/nodes.d.ts +37 -5
- package/dist/server.d.ts +3 -2
- package/dist/types/content.d.ts +2 -2
- package/package.json +2 -2
- package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
- package/dist/layout-engine/justifiedLineFit.js +0 -6
|
@@ -7,8 +7,6 @@ import { DisplayUnsupported } from "../types.js";
|
|
|
7
7
|
declare const UNSUPPORTED_CONSTRUCT: {
|
|
8
8
|
/** `w:effect` animations (blinkBackground, shimmer, …). */
|
|
9
9
|
readonly textEffect: "textEffect";
|
|
10
|
-
/** `w:smallCaps`: advances are correct, the small-cap glyph choice is not expressible. */
|
|
11
|
-
readonly smallCaps: "smallCaps";
|
|
12
10
|
/** `w:w` horizontal scale: folded into advances, but the glyphs are not narrowed. */
|
|
13
11
|
readonly horizontalScale: "horizontalScale";
|
|
14
12
|
/** `w:em` emphasis marks. */
|
|
@@ -39,6 +37,8 @@ declare const UNSUPPORTED_CONSTRUCT: {
|
|
|
39
37
|
readonly headerFooterContent: "headerFooterContent";
|
|
40
38
|
/** `a:bodyPr` middle/bottom anchoring inside a text box. */
|
|
41
39
|
readonly textBoxVerticalAlign: "textBoxVerticalAlign";
|
|
40
|
+
/** A text box's `a:gradFill`: the primitives carry no gradient. */
|
|
41
|
+
readonly textBoxGradientFill: "textBoxGradientFill";
|
|
42
42
|
/** A fragment kind with no builder. */
|
|
43
43
|
readonly fragmentKind: "fragmentKind";
|
|
44
44
|
/** A fragment whose block or measure is missing from the block lookup. */
|
|
Binary file
|
|
@@ -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
|
}
|
|
@@ -297,7 +297,6 @@ const paintGlyphRun = (run, context) => {
|
|
|
297
297
|
if (run.unicodeBidi !== void 0) span.style.unicodeBidi = run.unicodeBidi;
|
|
298
298
|
if (run.stroke !== void 0) span.style.webkitTextStroke = `${px(run.stroke.thicknessPx)} ${cssColor(run.stroke.color)}`;
|
|
299
299
|
span.style.fontKerning = run.kerning ? "normal" : "none";
|
|
300
|
-
if (run.smallCaps) span.style.fontVariant = "small-caps";
|
|
301
300
|
applyAdjustments(span, run.adjustments);
|
|
302
301
|
span.dataset["advanceSum"] = String(advanceSum);
|
|
303
302
|
applyModelRange(span, run.pmRange);
|
|
@@ -400,7 +399,7 @@ const paintImage = ({ image, rect, crop, opacity, brightness, contrast }, contex
|
|
|
400
399
|
element.style.height = px(fullHeight);
|
|
401
400
|
}
|
|
402
401
|
element.alt = "";
|
|
403
|
-
element.src = binarySrc(source.bytes, IMAGE_MIME_TYPES[source.format]);
|
|
402
|
+
element.src = binarySrc(source.bytes, IMAGE_MIME_TYPES[source.format], context.binaryUrls);
|
|
404
403
|
clip.append(element);
|
|
405
404
|
context.parent.append(clip);
|
|
406
405
|
};
|
|
@@ -498,6 +497,7 @@ const renderPage = (page, options) => {
|
|
|
498
497
|
}
|
|
499
498
|
const context = {
|
|
500
499
|
doc: options.doc,
|
|
500
|
+
binaryUrls: options.binaryUrls ?? "objectUrl",
|
|
501
501
|
fonts: options.fonts,
|
|
502
502
|
images: options.images,
|
|
503
503
|
parent: element,
|
|
@@ -596,11 +596,11 @@ const paintRegionTree = ({ primitives, regions }, context) => {
|
|
|
596
596
|
};
|
|
597
597
|
const renderDisplayPageToDom = (page, options) => renderPage(page, {
|
|
598
598
|
...options,
|
|
599
|
-
fontFaceCss: embeddedFontFaceCss(options.fonts)
|
|
599
|
+
fontFaceCss: embeddedFontFaceCss(options.fonts, options.binaryUrls ?? "objectUrl")
|
|
600
600
|
});
|
|
601
601
|
/** One `div.layout-page` per display page, in page order. */
|
|
602
602
|
const renderDisplayListToDom = (list, options) => {
|
|
603
|
-
const fontFaceCss = embeddedFontFaceCss(list.fonts);
|
|
603
|
+
const fontFaceCss = embeddedFontFaceCss(list.fonts, options.binaryUrls ?? "objectUrl");
|
|
604
604
|
return list.pages.map((page, pageIndex) => renderPage(page, {
|
|
605
605
|
...options,
|
|
606
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 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
|
|
2
|
-
import { convertBulletToUnicode } from "./bulletMarkers.js";
|
|
2
|
+
import { bulletMarkerFontName, convertBulletToUnicode } from "./bulletMarkers.js";
|
|
3
3
|
import { CAPTURE, dispatchChildrenWithContext, ownedElsewhere, withPreservedChildren } from "./containerChildren.js";
|
|
4
4
|
import { isNumberingReference } from "./numberingReference.js";
|
|
5
5
|
import { formatOoxmlCounter } from "./ooxmlCounterFormatter.js";
|
|
@@ -62,7 +62,7 @@ const computeListMarker = (paragraph, { numbering, listCounters, abstractCounter
|
|
|
62
62
|
previousList.numId = numId;
|
|
63
63
|
const pattern = listRendering.marker;
|
|
64
64
|
if (listRendering.isBullet) {
|
|
65
|
-
listRendering.marker = convertBulletToUnicode(pattern || "");
|
|
65
|
+
listRendering.marker = convertBulletToUnicode(pattern || "", bulletMarkerFontName(listRendering.markerFormatting));
|
|
66
66
|
previousList.abstractNumId = null;
|
|
67
67
|
previousList.fromStyle = false;
|
|
68
68
|
previousList.numId = null;
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
//#region src/docx/bulletMarkers.d.ts
|
|
2
|
-
|
|
2
|
+
type MarkerFontFamily = {
|
|
3
|
+
ascii?: string | undefined;
|
|
4
|
+
hAnsi?: string | undefined;
|
|
5
|
+
};
|
|
6
|
+
/** The face a bullet level's `w:rFonts` names for its (Latin-range) marker character. */
|
|
7
|
+
declare const bulletMarkerFontName: (formatting: {
|
|
8
|
+
fontFamily?: MarkerFontFamily | undefined;
|
|
9
|
+
} | null | undefined) => string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Map a bullet level's `w:lvlText` to the character painted for it.
|
|
12
|
+
*
|
|
13
|
+
* `fontName` is the level's own `w:rFonts` face. When it is an ordinary text
|
|
14
|
+
* font, a Latin-range character is the letter itself (the common `o` bullet
|
|
15
|
+
* in a monospace face); without a named face the character is read as a
|
|
16
|
+
* symbol-font code.
|
|
17
|
+
*/
|
|
18
|
+
declare const convertBulletToUnicode: (bulletChar: string, fontName?: string) => string;
|
|
3
19
|
//#endregion
|
|
4
|
-
export { convertBulletToUnicode };
|
|
20
|
+
export { bulletMarkerFontName, convertBulletToUnicode };
|
|
@@ -28,10 +28,35 @@ const SYMBOL_BULLET_MAP = {
|
|
|
28
28
|
62: ">",
|
|
29
29
|
45: "-"
|
|
30
30
|
};
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Fonts whose single-byte codes name pictographs rather than Latin letters.
|
|
33
|
+
* A `w:lvlText` character below U+0100 only means a symbol glyph when the
|
|
34
|
+
* numbering level's `w:rFonts` selects one of these faces.
|
|
35
|
+
*/
|
|
36
|
+
const SYMBOL_ENCODED_FONTS = /* @__PURE__ */ new Set([
|
|
37
|
+
"symbol",
|
|
38
|
+
"wingdings",
|
|
39
|
+
"wingdings 2",
|
|
40
|
+
"wingdings 3",
|
|
41
|
+
"webdings"
|
|
42
|
+
]);
|
|
43
|
+
/** The face a bullet level's `w:rFonts` names for its (Latin-range) marker character. */
|
|
44
|
+
const bulletMarkerFontName = (formatting) => formatting?.fontFamily?.ascii ?? formatting?.fontFamily?.hAnsi;
|
|
45
|
+
/** A printable Latin-1 character in a named, non-symbol face paints as itself. */
|
|
46
|
+
const isTextFontCharacter = (charCode, fontName) => fontName !== void 0 && (charCode >= 32 && charCode < 127 || charCode >= 160 && charCode < 256) && !SYMBOL_ENCODED_FONTS.has(fontName.trim().toLowerCase());
|
|
47
|
+
/**
|
|
48
|
+
* Map a bullet level's `w:lvlText` to the character painted for it.
|
|
49
|
+
*
|
|
50
|
+
* `fontName` is the level's own `w:rFonts` face. When it is an ordinary text
|
|
51
|
+
* font, a Latin-range character is the letter itself (the common `o` bullet
|
|
52
|
+
* in a monospace face); without a named face the character is read as a
|
|
53
|
+
* symbol-font code.
|
|
54
|
+
*/
|
|
55
|
+
const convertBulletToUnicode = (bulletChar, fontName) => {
|
|
32
56
|
if (!bulletChar || bulletChar.trim() === "") return "•";
|
|
33
57
|
const charCode = bulletChar.codePointAt(0);
|
|
34
58
|
if (charCode === void 0) return "•";
|
|
59
|
+
if (isTextFontCharacter(charCode, fontName)) return bulletChar;
|
|
35
60
|
const mapped = SYMBOL_BULLET_MAP[charCode];
|
|
36
61
|
if (mapped !== void 0) return mapped;
|
|
37
62
|
if (charCode >= 57344 && charCode <= 63743) return "•";
|
|
@@ -39,4 +64,4 @@ const convertBulletToUnicode = (bulletChar) => {
|
|
|
39
64
|
return bulletChar;
|
|
40
65
|
};
|
|
41
66
|
//#endregion
|
|
42
|
-
export { convertBulletToUnicode };
|
|
67
|
+
export { bulletMarkerFontName, convertBulletToUnicode };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { XmlElement } from "./xmlParser.js";
|
|
2
|
+
//#region src/docx/drawingGroupChildren.d.ts
|
|
3
|
+
type GroupTextBoxFrame = {
|
|
4
|
+
/** Element indices from the `wpg:wgp` down to the `wps:wsp`. */
|
|
5
|
+
path: number[];
|
|
6
|
+
wsp: XmlElement;
|
|
7
|
+
/** EMUs from the group's top-left corner. */
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
/** Degrees clockwise. */
|
|
13
|
+
rotation: number;
|
|
14
|
+
flipH: boolean;
|
|
15
|
+
flipV: boolean;
|
|
16
|
+
};
|
|
17
|
+
type Transform2D = {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
childX: number;
|
|
23
|
+
childY: number;
|
|
24
|
+
childWidth: number;
|
|
25
|
+
childHeight: number;
|
|
26
|
+
rotation: number;
|
|
27
|
+
flipH: boolean;
|
|
28
|
+
flipV: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** A `CT_GroupTransform2D` or `CT_Transform2D`; absent children read as zero. */
|
|
31
|
+
declare const readTransform: (xfrm: XmlElement | null) => Transform2D;
|
|
32
|
+
/**
|
|
33
|
+
* Every `wps:wsp` with a text box in the group, nested groups included, with
|
|
34
|
+
* its frame in EMUs from the group's top-left corner.
|
|
35
|
+
*
|
|
36
|
+
* A nested group that is rotated or flipped turns its children's frames into
|
|
37
|
+
* something other than a rectangle in the drawing's axes, so its text boxes
|
|
38
|
+
* are left out and stay with the preview.
|
|
39
|
+
*/
|
|
40
|
+
declare const collectGroupTextBoxes: (group: XmlElement, width: number, height: number) => GroupTextBoxFrame[];
|
|
41
|
+
/** Identity of a group drawing's authored XML. */
|
|
42
|
+
declare const groupXmlFingerprint: (rawXml: string) => string;
|
|
43
|
+
/** Identity of a text box's content, compared on save to tell an edit. */
|
|
44
|
+
declare const groupTextContentFingerprint: (content: unknown) => string;
|
|
45
|
+
type GroupTextBoxEdit = {
|
|
46
|
+
path: readonly number[];
|
|
47
|
+
/** Serialized `w:txbxContent` children. */
|
|
48
|
+
contentXml: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The captured group XML with each edited text box's content replaced, or
|
|
52
|
+
* undefined when an edit names no text box in it.
|
|
53
|
+
*
|
|
54
|
+
* The replacement is written as a placeholder element and substituted after
|
|
55
|
+
* the tree is serialized, so the new content is never reparsed and the rest
|
|
56
|
+
* of the capture is written exactly as it was read.
|
|
57
|
+
*/
|
|
58
|
+
declare const replaceGroupTextBoxContent: (rawXml: string, edits: readonly GroupTextBoxEdit[]) => string | undefined;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { GroupTextBoxEdit, GroupTextBoxFrame, collectGroupTextBoxes, groupTextContentFingerprint, groupXmlFingerprint, readTransform, replaceGroupTextBoxContent };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { canonicalJson } from "../utils/canonicalJson.js";
|
|
2
|
+
import { captureVerbatimXml } from "./verbatimCapture.js";
|
|
3
|
+
import { findChildByLocalName, findDeep, getAttribute, getChildElements, getLocalName, parseNumericAttribute, parseXml } from "./xmlParser.js";
|
|
4
|
+
//#region src/docx/drawingGroupChildren.ts
|
|
5
|
+
/**
|
|
6
|
+
* The text boxes of a DrawingML group (`wpg:wgp`), read as frames folio can lay
|
|
7
|
+
* out, and written back into the group's authored XML.
|
|
8
|
+
*
|
|
9
|
+
* A group states a child coordinate space (`a:chOff` / `a:chExt`) that its
|
|
10
|
+
* own frame (`a:off` / `a:ext`) maps onto, and every child's `a:xfrm` is in
|
|
11
|
+
* that space; a nested `wpg:grpSp` repeats the mapping one level down
|
|
12
|
+
* (ECMA-376 §20.1.7.5). Composing the mappings gives each `wps:wsp` a frame
|
|
13
|
+
* in EMUs relative to the group's own top-left corner. Only the frame is
|
|
14
|
+
* mapped: the text inside keeps its authored size and the body insets stay in
|
|
15
|
+
* EMUs.
|
|
16
|
+
*/
|
|
17
|
+
/** `a:xfrm@rot` is in 60,000ths of a degree. */
|
|
18
|
+
const ROTATION_UNITS_PER_DEGREE = 6e4;
|
|
19
|
+
/** Deeper nesting than this is not a drawing anyone authored. */
|
|
20
|
+
const MAX_GROUP_DEPTH = 16;
|
|
21
|
+
const MAX_GROUP_TEXT_BOXES = 256;
|
|
22
|
+
const numberAttribute = (element, name) => parseNumericAttribute(element, null, name) ?? 0;
|
|
23
|
+
const onOffAttribute = (element, name) => {
|
|
24
|
+
const value = getAttribute(element, null, name);
|
|
25
|
+
return value === "1" || value === "true" || value === "on";
|
|
26
|
+
};
|
|
27
|
+
/** A `CT_GroupTransform2D` or `CT_Transform2D`; absent children read as zero. */
|
|
28
|
+
const readTransform = (xfrm) => {
|
|
29
|
+
const offset = findChildByLocalName(xfrm, "off");
|
|
30
|
+
const extent = findChildByLocalName(xfrm, "ext");
|
|
31
|
+
const childOffset = findChildByLocalName(xfrm, "chOff");
|
|
32
|
+
const childExtent = findChildByLocalName(xfrm, "chExt");
|
|
33
|
+
return {
|
|
34
|
+
x: numberAttribute(offset, "x"),
|
|
35
|
+
y: numberAttribute(offset, "y"),
|
|
36
|
+
width: numberAttribute(extent, "cx"),
|
|
37
|
+
height: numberAttribute(extent, "cy"),
|
|
38
|
+
childX: numberAttribute(childOffset, "x"),
|
|
39
|
+
childY: numberAttribute(childOffset, "y"),
|
|
40
|
+
childWidth: numberAttribute(childExtent, "cx"),
|
|
41
|
+
childHeight: numberAttribute(childExtent, "cy"),
|
|
42
|
+
rotation: numberAttribute(xfrm, "rot") / ROTATION_UNITS_PER_DEGREE,
|
|
43
|
+
flipH: onOffAttribute(xfrm, "flipH"),
|
|
44
|
+
flipV: onOffAttribute(xfrm, "flipV")
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const groupTransform = (group) => readTransform(findChildByLocalName(findChildByLocalName(group, "grpSpPr"), "xfrm"));
|
|
48
|
+
const shapeTransform = (shape) => readTransform(findChildByLocalName(findChildByLocalName(shape, "spPr"), "xfrm"));
|
|
49
|
+
/**
|
|
50
|
+
* The top-level mapping: the group's child space stretched over the drawing's
|
|
51
|
+
* extent. A missing or empty child extent means the child space is the
|
|
52
|
+
* drawing's own.
|
|
53
|
+
*/
|
|
54
|
+
const rootMap = (group, width, height) => {
|
|
55
|
+
const transform = groupTransform(group);
|
|
56
|
+
const scaleX = transform.childWidth > 0 ? width / transform.childWidth : 1;
|
|
57
|
+
const scaleY = transform.childHeight > 0 ? height / transform.childHeight : 1;
|
|
58
|
+
return {
|
|
59
|
+
scaleX,
|
|
60
|
+
scaleY,
|
|
61
|
+
offsetX: -transform.childX * scaleX,
|
|
62
|
+
offsetY: -transform.childY * scaleY
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/** A nested group's child space, mapped through its frame in the parent's space. */
|
|
66
|
+
const nestedMap = (parent, transform) => {
|
|
67
|
+
const scaleX = transform.childWidth > 0 ? transform.width / transform.childWidth : 1;
|
|
68
|
+
const scaleY = transform.childHeight > 0 ? transform.height / transform.childHeight : 1;
|
|
69
|
+
return {
|
|
70
|
+
scaleX: parent.scaleX * scaleX,
|
|
71
|
+
scaleY: parent.scaleY * scaleY,
|
|
72
|
+
offsetX: parent.scaleX * (transform.x - transform.childX * scaleX) + parent.offsetX,
|
|
73
|
+
offsetY: parent.scaleY * (transform.y - transform.childY * scaleY) + parent.offsetY
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const hasTextBox = (wsp) => findChildByLocalName(findChildByLocalName(wsp, "txbx"), "txbxContent") !== null;
|
|
77
|
+
/**
|
|
78
|
+
* Every `wps:wsp` with a text box in the group, nested groups included, with
|
|
79
|
+
* its frame in EMUs from the group's top-left corner.
|
|
80
|
+
*
|
|
81
|
+
* A nested group that is rotated or flipped turns its children's frames into
|
|
82
|
+
* something other than a rectangle in the drawing's axes, so its text boxes
|
|
83
|
+
* are left out and stay with the preview.
|
|
84
|
+
*/
|
|
85
|
+
const collectGroupTextBoxes = (group, width, height) => {
|
|
86
|
+
const frames = [];
|
|
87
|
+
const visit = (container, map, path, depth) => {
|
|
88
|
+
for (const [index, child] of getChildElements(container).entries()) {
|
|
89
|
+
if (frames.length >= MAX_GROUP_TEXT_BOXES) return;
|
|
90
|
+
const localName = getLocalName(child.name ?? "");
|
|
91
|
+
if (localName === "grpSp" && depth < MAX_GROUP_DEPTH) {
|
|
92
|
+
const transform = groupTransform(child);
|
|
93
|
+
if (transform.rotation === 0 && !transform.flipH && !transform.flipV) visit(child, nestedMap(map, transform), [...path, index], depth + 1);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (localName !== "wsp" || !hasTextBox(child)) continue;
|
|
97
|
+
const transform = shapeTransform(child);
|
|
98
|
+
const frameWidth = transform.width * map.scaleX;
|
|
99
|
+
const frameHeight = transform.height * map.scaleY;
|
|
100
|
+
if (!(frameWidth > 0 && frameHeight > 0)) continue;
|
|
101
|
+
frames.push({
|
|
102
|
+
path: [...path, index],
|
|
103
|
+
wsp: child,
|
|
104
|
+
x: transform.x * map.scaleX + map.offsetX,
|
|
105
|
+
y: transform.y * map.scaleY + map.offsetY,
|
|
106
|
+
width: frameWidth,
|
|
107
|
+
height: frameHeight,
|
|
108
|
+
rotation: transform.rotation,
|
|
109
|
+
flipH: transform.flipH,
|
|
110
|
+
flipV: transform.flipV
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
visit(group, rootMap(group, width, height), [], 0);
|
|
115
|
+
return frames;
|
|
116
|
+
};
|
|
117
|
+
/** Two FNV-1a passes with different offsets: a short, stable identity. */
|
|
118
|
+
const fingerprint = (text) => {
|
|
119
|
+
let first = 2166136261;
|
|
120
|
+
let second = 16777619;
|
|
121
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
122
|
+
const code = text.charCodeAt(index);
|
|
123
|
+
first = Math.imul(first ^ code, 16777619);
|
|
124
|
+
second = Math.imul(second ^ code, 96280213);
|
|
125
|
+
}
|
|
126
|
+
return `${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0).toString(16).padStart(8, "0")}`;
|
|
127
|
+
};
|
|
128
|
+
/** Identity of a group drawing's authored XML. */
|
|
129
|
+
const groupXmlFingerprint = (rawXml) => fingerprint(rawXml);
|
|
130
|
+
/** Identity of a text box's content, compared on save to tell an edit. */
|
|
131
|
+
const groupTextContentFingerprint = (content) => fingerprint(canonicalJson(content));
|
|
132
|
+
/** The group a captured drawing carries; a Fallback branch holds none. */
|
|
133
|
+
const findGroup = (root) => findDeep(root, "wpg", "wgp") ?? void 0;
|
|
134
|
+
const childAtPath = (group, path) => {
|
|
135
|
+
let current = group;
|
|
136
|
+
for (const index of path) current = current ? getChildElements(current)[index] : void 0;
|
|
137
|
+
return current;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The text box content element a path names, or undefined when the path does
|
|
141
|
+
* not lead to a `wps:wsp` holding one.
|
|
142
|
+
*/
|
|
143
|
+
const textBoxContentAt = (group, path) => {
|
|
144
|
+
const wsp = childAtPath(group, path);
|
|
145
|
+
if (!wsp || getLocalName(wsp.name ?? "") !== "wsp") return;
|
|
146
|
+
return findChildByLocalName(findChildByLocalName(wsp, "txbx"), "txbxContent") ?? void 0;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* The captured group XML with each edited text box's content replaced, or
|
|
150
|
+
* undefined when an edit names no text box in it.
|
|
151
|
+
*
|
|
152
|
+
* The replacement is written as a placeholder element and substituted after
|
|
153
|
+
* the tree is serialized, so the new content is never reparsed and the rest
|
|
154
|
+
* of the capture is written exactly as it was read.
|
|
155
|
+
*/
|
|
156
|
+
const replaceGroupTextBoxContent = (rawXml, edits) => {
|
|
157
|
+
const root = parseXml(rawXml);
|
|
158
|
+
const group = findGroup(root);
|
|
159
|
+
if (!group) return;
|
|
160
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
161
|
+
for (const [index, edit] of edits.entries()) {
|
|
162
|
+
const content = textBoxContentAt(group, edit.path);
|
|
163
|
+
if (!content) return;
|
|
164
|
+
const placeholder = `folio-group-text-${index}-${fingerprint(rawXml)}`;
|
|
165
|
+
content.elements = [{
|
|
166
|
+
type: "element",
|
|
167
|
+
name: placeholder
|
|
168
|
+
}];
|
|
169
|
+
replacements.set(`<${placeholder}/>`, edit.contentXml || "<w:p/>");
|
|
170
|
+
}
|
|
171
|
+
let xml = (root.elements ?? []).map((element) => captureVerbatimXml(element)).join("");
|
|
172
|
+
for (const [placeholder, contentXml] of replacements) xml = xml.replace(placeholder, () => contentXml);
|
|
173
|
+
return xml;
|
|
174
|
+
};
|
|
175
|
+
//#endregion
|
|
176
|
+
export { collectGroupTextBoxes, groupTextContentFingerprint, groupXmlFingerprint, readTransform, replaceGroupTextBoxContent };
|
|
@@ -107,6 +107,7 @@ function parseFill(spPr) {
|
|
|
107
107
|
function parseGradientFill(gradientFill) {
|
|
108
108
|
let type = "linear";
|
|
109
109
|
let angle;
|
|
110
|
+
let scaled;
|
|
110
111
|
const linear = findChildByLocalName(gradientFill, "lin");
|
|
111
112
|
if (linear) {
|
|
112
113
|
const authoredAngle = getAttribute(linear, null, "ang");
|
|
@@ -114,6 +115,7 @@ function parseGradientFill(gradientFill) {
|
|
|
114
115
|
const parsedAngle = Number.parseInt(authoredAngle, 10);
|
|
115
116
|
angle = Number.isNaN(parsedAngle) ? void 0 : parsedAngle / 6e4;
|
|
116
117
|
}
|
|
118
|
+
scaled = parseOnOffValue(getAttribute(linear, null, "scaled"), void 0, "a:lin@scaled");
|
|
117
119
|
}
|
|
118
120
|
const path = findChildByLocalName(gradientFill, "path");
|
|
119
121
|
if (path) {
|
|
@@ -139,6 +141,7 @@ function parseGradientFill(gradientFill) {
|
|
|
139
141
|
gradient: {
|
|
140
142
|
type,
|
|
141
143
|
...angle !== void 0 ? { angle } : {},
|
|
144
|
+
...scaled !== void 0 ? { scaled } : {},
|
|
142
145
|
stops
|
|
143
146
|
}
|
|
144
147
|
};
|