@stll/folio-core 0.33.2 → 0.35.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 +137 -23
- package/dist/ai-edits/snapshot.d.ts +3 -1
- package/dist/ai-edits/snapshot.js +44 -4
- package/dist/ai-edits/types.d.ts +6 -6
- package/dist/compare/compare.js +15 -3
- package/dist/compare/formatting.d.ts +1 -1
- package/dist/compare/formatting.js +38 -9
- package/dist/compare/verification.js +18 -1
- package/dist/controller/headerFooterEditorManager.js +11 -9
- package/dist/controller/layoutPipeline.js +24 -3
- package/dist/display-list/build/watermarkPrimitives.js +15 -3
- package/dist/document-operations.js +30 -3
- package/dist/docx/headerFooterParser.js +8 -14
- package/dist/docx/paragraphParser.js +45 -0
- package/dist/docx/serializer/headerFooterSerializer.js +21 -2
- package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
- package/dist/docx/serializer/paragraphSerializer.js +19 -9
- package/dist/docx/settingsParser.js +3 -0
- package/dist/docx/watermarkParser.d.ts +2 -4
- package/dist/docx/watermarkParser.js +4 -6
- package/dist/headless-layout.js +14 -2
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
- package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
- package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
- package/dist/layout-engine/index.js +11 -4
- package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
- package/dist/layout-engine/justifiedLineFit.js +4 -4
- package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
- package/dist/layout-engine/measure/measureBlocks.js +1 -1
- package/dist/layout-engine/measure/measureParagraph.js +29 -29
- package/dist/layout-painter/renderPage.js +6 -1
- package/dist/layout-painter/renderParagraph.js +18 -8
- package/dist/layout-painter/renderWatermark.js +11 -4
- package/dist/prosemirror/attrs/index.js +11 -0
- package/dist/prosemirror/commands/comments.js +22 -2
- package/dist/prosemirror/conversion/fromProseDoc.js +32 -5
- package/dist/prosemirror/conversion/toProseDoc.js +49 -11
- package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
- package/dist/prosemirror/plugins/templateDirectives.d.ts +14 -7
- package/dist/prosemirror/plugins/templateDirectives.js +26 -19
- package/dist/prosemirror/plugins/templateSlashMenu.js +2 -2
- package/dist/prosemirror/schema/marks.d.ts +2 -8
- package/dist/utils/fontResolver.js +51 -0
- package/dist/utils/formatToStyle.js +41 -1
- package/dist/watermark/index.js +7 -0
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pxToPt } from "../../layout-engine/measure/measureHelpers.js";
|
|
1
|
+
import { ptToPx, pxToPt } from "../../layout-engine/measure/measureHelpers.js";
|
|
2
2
|
import { getFontMetrics } from "../../layout-engine/measure/measureProvider.js";
|
|
3
3
|
import { parseDisplayColor } from "./colors.js";
|
|
4
4
|
import { buildGlyphs, glyphRunText } from "./glyphs.js";
|
|
@@ -28,7 +28,7 @@ const TEXT_DEFAULT_COLOR = "#C0C0C0";
|
|
|
28
28
|
const TEXT_DEFAULT_OPACITY = .5;
|
|
29
29
|
const TEXT_DIAGONAL_DEGREES = -45;
|
|
30
30
|
const PICTURE_NATIVE_SCALE = 1;
|
|
31
|
-
const PICTURE_WASHOUT_OPACITY = .
|
|
31
|
+
const PICTURE_WASHOUT_OPACITY = .18;
|
|
32
32
|
const paintTextWatermark = (watermark, page, context) => {
|
|
33
33
|
if (watermark.text.length === 0) return [];
|
|
34
34
|
const style = {
|
|
@@ -98,6 +98,18 @@ const containedRect = (page, scale, pixelWidth, pixelHeight) => {
|
|
|
98
98
|
heightPx
|
|
99
99
|
};
|
|
100
100
|
};
|
|
101
|
+
/** The centred VML shape box, when both authored dimensions survived parsing. */
|
|
102
|
+
const authoredPictureRect = (page, widthPt, heightPt) => {
|
|
103
|
+
if (widthPt === void 0 || heightPt === void 0) return;
|
|
104
|
+
const widthPx = ptToPx(widthPt);
|
|
105
|
+
const heightPx = ptToPx(heightPt);
|
|
106
|
+
return {
|
|
107
|
+
xPx: (page.size.w - widthPx) / 2,
|
|
108
|
+
yPx: (page.size.h - heightPx) / 2,
|
|
109
|
+
widthPx,
|
|
110
|
+
heightPx
|
|
111
|
+
};
|
|
112
|
+
};
|
|
101
113
|
const paintPictureWatermark = (watermark, page, imageSrc, context) => {
|
|
102
114
|
if (imageSrc === void 0) {
|
|
103
115
|
context.unsupported.report(UNSUPPORTED_CONSTRUCT.watermark, context.pageIndex, `picture watermark ${watermark.imageRId} has no resolved image source: the relationship id resolves in the package layer, not in the builder`);
|
|
@@ -113,7 +125,7 @@ const paintPictureWatermark = (watermark, page, imageSrc, context) => {
|
|
|
113
125
|
return [{
|
|
114
126
|
kind: "image",
|
|
115
127
|
image: ref,
|
|
116
|
-
rect: containedRect(page, watermark.scale ?? PICTURE_NATIVE_SCALE, source?.pixelWidth ?? 0, source?.pixelHeight ?? 0),
|
|
128
|
+
rect: authoredPictureRect(page, watermark.widthPt, watermark.heightPt) ?? containedRect(page, watermark.scale ?? PICTURE_NATIVE_SCALE, source?.pixelWidth ?? 0, source?.pixelHeight ?? 0),
|
|
117
129
|
opacity: watermark.washout === false ? 1 : PICTURE_WASHOUT_OPACITY
|
|
118
130
|
}];
|
|
119
131
|
};
|
|
@@ -143,6 +143,24 @@ const readOptionalBoolean = (value, key, path) => {
|
|
|
143
143
|
if (typeof candidate === "boolean") return candidate;
|
|
144
144
|
return invalidBatch(`${path}.${key}`, "expected a boolean when provided");
|
|
145
145
|
};
|
|
146
|
+
const readClearableNonEmptyString = (value, key, path) => {
|
|
147
|
+
const candidate = value[key];
|
|
148
|
+
if (candidate === void 0 || candidate === null) return candidate;
|
|
149
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
|
|
150
|
+
return invalidBatch(`${path}.${key}`, "expected a non-empty string or null when provided");
|
|
151
|
+
};
|
|
152
|
+
const readClearableFontSize = (value, key, path) => {
|
|
153
|
+
const candidate = value[key];
|
|
154
|
+
if (candidate === void 0 || candidate === null) return candidate;
|
|
155
|
+
if (typeof candidate === "number" && candidate > 0 && Number.isInteger(candidate * 2)) return candidate;
|
|
156
|
+
return invalidBatch(`${path}.${key}`, "expected a positive half-point value or null");
|
|
157
|
+
};
|
|
158
|
+
const readClearableRgbColor = (value, key, path) => {
|
|
159
|
+
const candidate = value[key];
|
|
160
|
+
if (candidate === void 0 || candidate === null) return candidate;
|
|
161
|
+
if (typeof candidate === "string" && /^#?[0-9a-fA-F]{6}$/u.test(candidate)) return candidate.replace(/^#/u, "").toUpperCase();
|
|
162
|
+
return invalidBatch(`${path}.${key}`, "expected a six-digit RGB color or null");
|
|
163
|
+
};
|
|
146
164
|
const readOptionalStringArray = (value, key, path) => {
|
|
147
165
|
const candidate = value[key];
|
|
148
166
|
if (candidate === void 0) return;
|
|
@@ -238,18 +256,27 @@ const readInlineFormatting = (value, path) => {
|
|
|
238
256
|
"bold",
|
|
239
257
|
"italic",
|
|
240
258
|
"underline",
|
|
241
|
-
"strike"
|
|
259
|
+
"strike",
|
|
260
|
+
"fontFamily",
|
|
261
|
+
"fontSizePt",
|
|
262
|
+
"color"
|
|
242
263
|
]);
|
|
243
264
|
const bold = readOptionalBoolean(candidate, "bold", formattingPath);
|
|
244
265
|
const italic = readOptionalBoolean(candidate, "italic", formattingPath);
|
|
245
266
|
const underline = readOptionalBoolean(candidate, "underline", formattingPath);
|
|
246
267
|
const strike = readOptionalBoolean(candidate, "strike", formattingPath);
|
|
247
|
-
|
|
268
|
+
const fontFamily = readClearableNonEmptyString(candidate, "fontFamily", formattingPath);
|
|
269
|
+
const fontSizePt = readClearableFontSize(candidate, "fontSizePt", formattingPath);
|
|
270
|
+
const color = readClearableRgbColor(candidate, "color", formattingPath);
|
|
271
|
+
if (bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0) return invalidBatch(formattingPath, "expected at least one formatting property");
|
|
248
272
|
return {
|
|
249
273
|
...bold !== void 0 && { bold },
|
|
250
274
|
...italic !== void 0 && { italic },
|
|
251
275
|
...underline !== void 0 && { underline },
|
|
252
|
-
...strike !== void 0 && { strike }
|
|
276
|
+
...strike !== void 0 && { strike },
|
|
277
|
+
...fontFamily !== void 0 && { fontFamily },
|
|
278
|
+
...fontSizePt !== void 0 && { fontSizePt },
|
|
279
|
+
...color !== void 0 && { color }
|
|
253
280
|
};
|
|
254
281
|
};
|
|
255
282
|
const readOptionalComment = (value, path) => {
|
|
@@ -31,27 +31,21 @@ function parseHeader(headerXml, hdrFtrType = "default", styles = null, theme = n
|
|
|
31
31
|
result.rawWatermarkXml = watermarkResult.rawParagraphXml;
|
|
32
32
|
result.watermarkBlockIndex = watermarkResult.blockIndex;
|
|
33
33
|
}
|
|
34
|
-
result.content = parseBlockContent(
|
|
34
|
+
result.content = parseBlockContent(rootElement, styles, theme, numbering, rels, media, {
|
|
35
35
|
inHeaderFooter: true,
|
|
36
36
|
rootXmlns: collectXmlnsDeclarations(rootElement)
|
|
37
37
|
});
|
|
38
|
+
if (watermarkResult) {
|
|
39
|
+
const host = result.content.at(watermarkResult.blockIndex);
|
|
40
|
+
if (host?.type === "paragraph") result.content[watermarkResult.blockIndex] = {
|
|
41
|
+
...host,
|
|
42
|
+
content: []
|
|
43
|
+
};
|
|
44
|
+
}
|
|
38
45
|
assignHeaderFooterVerbatimXml(result, headerXml);
|
|
39
46
|
return result;
|
|
40
47
|
}
|
|
41
48
|
/**
|
|
42
|
-
* Return a shallow copy of `parent` whose `elements` array omits the
|
|
43
|
-
* single child reference `child`. Used to skip the watermark paragraph
|
|
44
|
-
* when feeding the header into `parseBlockContent` — without this the
|
|
45
|
-
* body parser would emit an empty placeholder paragraph where the
|
|
46
|
-
* watermark sits in the source.
|
|
47
|
-
*/
|
|
48
|
-
function withoutChild(parent, child) {
|
|
49
|
-
return {
|
|
50
|
-
...parent,
|
|
51
|
-
elements: (parent.elements ?? []).filter((el) => el !== child)
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
49
|
* Parse a footer XML file (word/footer*.xml)
|
|
56
50
|
*
|
|
57
51
|
* @param footerXml - The raw XML content of the footer file
|
|
@@ -763,6 +763,47 @@ function parseSimpleField(node, styles, theme, rels, media, rootXmlns = {}) {
|
|
|
763
763
|
function hasRunPayloadElement(runElement) {
|
|
764
764
|
return getChildElements(runElement).some((child) => !matchesName(child, "w", "rPr"));
|
|
765
765
|
}
|
|
766
|
+
const LEGACY_FORM_CHECKBOX_GLYPHS = {
|
|
767
|
+
checked: "☒",
|
|
768
|
+
unchecked: "☐"
|
|
769
|
+
};
|
|
770
|
+
const LEGACY_FORM_CHECKBOX_INSTRUCTION = "FORMCHECKBOX";
|
|
771
|
+
function getLegacyFormCheckboxDisplay(runElement) {
|
|
772
|
+
const fieldChar = findChild(runElement, "w", "fldChar");
|
|
773
|
+
const fieldData = fieldChar ? findChild(fieldChar, "w", "ffData") : null;
|
|
774
|
+
const checkBox = fieldData ? findChild(fieldData, "w", "checkBox") : null;
|
|
775
|
+
if (!checkBox) return;
|
|
776
|
+
const checked = findChild(checkBox, "w", "checked");
|
|
777
|
+
const defaultChecked = findChild(checkBox, "w", "default");
|
|
778
|
+
let isChecked = defaultChecked ? parseBooleanElement(defaultChecked) : false;
|
|
779
|
+
if (checked) isChecked = parseBooleanElement(checked);
|
|
780
|
+
const explicitSize = parseNumericAttribute(findChild(checkBox, "w", "size"), "w", "val");
|
|
781
|
+
const text = isChecked ? LEGACY_FORM_CHECKBOX_GLYPHS.checked : LEGACY_FORM_CHECKBOX_GLYPHS.unchecked;
|
|
782
|
+
if (explicitSize === void 0) return { text };
|
|
783
|
+
return {
|
|
784
|
+
text,
|
|
785
|
+
fontSize: explicitSize
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function createLegacyFormCheckboxResultRun(display, inheritedFormatting) {
|
|
789
|
+
const run = {
|
|
790
|
+
type: "run",
|
|
791
|
+
content: [{
|
|
792
|
+
type: "text",
|
|
793
|
+
text: display.text
|
|
794
|
+
}]
|
|
795
|
+
};
|
|
796
|
+
const hasInheritedFormatting = inheritedFormatting !== void 0 && Object.keys(inheritedFormatting).length > 0;
|
|
797
|
+
if (display.fontSize !== void 0) run.formatting = hasInheritedFormatting ? {
|
|
798
|
+
...inheritedFormatting,
|
|
799
|
+
fontSize: display.fontSize
|
|
800
|
+
} : { fontSize: display.fontSize };
|
|
801
|
+
else if (hasInheritedFormatting) run.formatting = inheritedFormatting;
|
|
802
|
+
return run;
|
|
803
|
+
}
|
|
804
|
+
function isLegacyFormCheckboxInstruction(instruction) {
|
|
805
|
+
return instruction.trim().split(/\s+/u).at(0)?.toUpperCase() === LEGACY_FORM_CHECKBOX_INSTRUCTION;
|
|
806
|
+
}
|
|
766
807
|
/**
|
|
767
808
|
* Parse all content within a paragraph
|
|
768
809
|
*
|
|
@@ -779,6 +820,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
|
|
|
779
820
|
let afterSeparator = false;
|
|
780
821
|
let complexFieldLock = false;
|
|
781
822
|
let complexFieldDirty = false;
|
|
823
|
+
let complexFieldFallbackDisplay;
|
|
782
824
|
let complexFieldFormatting;
|
|
783
825
|
for (const child of children) {
|
|
784
826
|
const localName = getLocalName(child.name);
|
|
@@ -790,6 +832,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
|
|
|
790
832
|
let hasFieldBegin = false;
|
|
791
833
|
let beginFldLock = false;
|
|
792
834
|
let beginDirty = false;
|
|
835
|
+
const beginFallbackDisplay = getLegacyFormCheckboxDisplay(runElement);
|
|
793
836
|
let hasFieldSeparate = false;
|
|
794
837
|
let hasFieldEnd = false;
|
|
795
838
|
let endOriginalValue;
|
|
@@ -816,6 +859,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
|
|
|
816
859
|
complexFieldResultRuns = [];
|
|
817
860
|
complexFieldLock = beginFldLock;
|
|
818
861
|
complexFieldDirty = beginDirty;
|
|
862
|
+
complexFieldFallbackDisplay = beginFallbackDisplay;
|
|
819
863
|
complexFieldFormatting = run.formatting;
|
|
820
864
|
}
|
|
821
865
|
if (inComplexField) {
|
|
@@ -840,6 +884,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
|
|
|
840
884
|
}
|
|
841
885
|
if (hasFieldEnd) {
|
|
842
886
|
let resultRuns = complexFieldResultRuns;
|
|
887
|
+
if (resultRuns.length === 0 && complexFieldFallbackDisplay !== void 0 && isLegacyFormCheckboxInstruction(complexFieldInstr)) resultRuns = [createLegacyFormCheckboxResultRun(complexFieldFallbackDisplay, complexFieldFormatting)];
|
|
843
888
|
if (resultRuns.length === 0 && !afterSeparator && endOriginalValue !== void 0) resultRuns = [{
|
|
844
889
|
type: "run",
|
|
845
890
|
content: [{
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { canReplayHeaderFooterVerbatim, getHeaderFooterVerbatimXml } from "../headerFooterVerbatim.js";
|
|
2
|
+
import { isEmptyParagraph } from "../paragraphParser.js";
|
|
3
|
+
import { captureVerbatimXml } from "../verbatimCapture.js";
|
|
4
|
+
import { getLocalName, parseXmlDocument } from "../xmlParser.js";
|
|
2
5
|
import { serializeBlockSdt } from "./blockSdtSerializer.js";
|
|
3
6
|
import { serializeParagraph } from "./paragraphSerializer.js";
|
|
4
7
|
import { serializePartElement } from "./partNamespaces.js";
|
|
@@ -53,8 +56,16 @@ function serializeHeaderFooter(hf, source) {
|
|
|
53
56
|
const watermarkInsertIndex = hf.watermarkBlockIndex !== void 0 ? Math.max(0, Math.min(hf.watermarkBlockIndex, hf.content.length)) : 0;
|
|
54
57
|
const blocksXml = hf.content.map((block) => serializeBlock(block));
|
|
55
58
|
let contentXml;
|
|
56
|
-
if (watermarkXml)
|
|
57
|
-
|
|
59
|
+
if (watermarkXml) {
|
|
60
|
+
const watermarkHost = hf.content.at(watermarkInsertIndex);
|
|
61
|
+
const rawWatermarkXml = hf.rawWatermarkXml;
|
|
62
|
+
const mergesIntoRetainedHost = rawWatermarkXml !== void 0 && watermarkHost?.type === "paragraph" && isEmptyParagraph(watermarkHost);
|
|
63
|
+
if (mergesIntoRetainedHost) blocksXml[watermarkInsertIndex] = serializeRawWatermarkIntoHost({
|
|
64
|
+
hostXml: blocksXml[watermarkInsertIndex] ?? serializeParagraph(watermarkHost),
|
|
65
|
+
rawWatermarkXml
|
|
66
|
+
});
|
|
67
|
+
contentXml = blocksXml.slice(0, watermarkInsertIndex).join("") + (mergesIntoRetainedHost ? "" : watermarkXml) + blocksXml.slice(watermarkInsertIndex).join("");
|
|
68
|
+
} else contentXml = blocksXml.join("");
|
|
58
69
|
if (!contentXml) contentXml = "<w:p><w:pPr/></w:p>";
|
|
59
70
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + serializePartElement({
|
|
60
71
|
partPath: source?.path ?? `word/${hf.type}.xml`,
|
|
@@ -64,6 +75,14 @@ function serializeHeaderFooter(hf, source) {
|
|
|
64
75
|
body: contentXml
|
|
65
76
|
});
|
|
66
77
|
}
|
|
78
|
+
function serializeRawWatermarkIntoHost({ hostXml, rawWatermarkXml }) {
|
|
79
|
+
const rawHost = parseXmlDocument(rawWatermarkXml);
|
|
80
|
+
if (!rawHost || getLocalName(rawHost.name ?? "") !== "p") return rawWatermarkXml;
|
|
81
|
+
const watermarkContent = (rawHost.elements ?? []).filter((child) => child.type === "element" && getLocalName(child.name ?? "") !== "pPr").map((child) => captureVerbatimXml(child)).join("");
|
|
82
|
+
if (!watermarkContent) return rawWatermarkXml;
|
|
83
|
+
const namespaceAttributes = Object.entries(rawHost.attributes ?? {}).filter(([name, value]) => value !== void 0 && (name === "xmlns" || name.startsWith("xmlns:"))).map(([name, value]) => ` ${name}="${escapeXml(String(value))}"`).join("");
|
|
84
|
+
return hostXml.replace(/^<w:p(?=[\s>])/u, `<w:p${namespaceAttributes}`).replace("</w:p>", `${watermarkContent}</w:p>`);
|
|
85
|
+
}
|
|
67
86
|
function serializeWatermarkParagraph(hf) {
|
|
68
87
|
if (hf.rawWatermarkXml) return hf.rawWatermarkXml;
|
|
69
88
|
if (hf.watermark) return synthesizeWatermarkParagraph(hf.watermark);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { document_d_exports } from "../../types/document.js";
|
|
2
2
|
//#region src/docx/serializer/paragraphSerializer.d.ts
|
|
3
|
-
declare function serializeParagraphFormatting(formatting: document_d_exports.ParagraphFormatting | undefined, propertyChanges?: document_d_exports.ParagraphPropertyChange[],
|
|
3
|
+
declare function serializeParagraphFormatting(formatting: document_d_exports.ParagraphFormatting | undefined, propertyChanges?: document_d_exports.ParagraphPropertyChange[], paragraphMarkChange?: document_d_exports.ParagraphMarkChange): string;
|
|
4
4
|
/**
|
|
5
5
|
* Serialize a paragraph to OOXML XML (w:p)
|
|
6
6
|
*
|
|
@@ -140,7 +140,7 @@ function serializeParagraphMarkChange(mark) {
|
|
|
140
140
|
const attrs = serializeTrackedChangeAttrs(mark.info);
|
|
141
141
|
return `<w:${mark.kind} ${attrs}/>`;
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
const serializeParagraphFormattingWithOptions = (formatting, { propertyChanges, paragraphMarkChange, sectionProperties } = {}) => {
|
|
144
144
|
const parts = [];
|
|
145
145
|
const pushToggle = (name, value) => {
|
|
146
146
|
if (value === true) parts.push(`<w:${name}/>`);
|
|
@@ -175,14 +175,22 @@ function serializeParagraphFormatting(formatting, propertyChanges, pPrMark) {
|
|
|
175
175
|
pushToggle("contextualSpacing", formatting.contextualSpacing);
|
|
176
176
|
if (formatting.alignment) parts.push(`<w:jc w:val="${formatting.alignment}"/>`);
|
|
177
177
|
if (formatting.outlineLevel !== void 0) parts.push(`<w:outlineLvl w:val="${formatting.outlineLevel}"/>`);
|
|
178
|
-
if (
|
|
179
|
-
const fullInner = `${
|
|
178
|
+
if (paragraphMarkChange || formatting.runProperties || formatting.runInWithNext) {
|
|
179
|
+
const fullInner = `${paragraphMarkChange ? serializeParagraphMarkChange(paragraphMarkChange) : ""}${formatting.runProperties ? extractRPrInner(serializeTextFormatting(formatting.runProperties)) : ""}${formatting.runInWithNext ? "<w:specVanish/>" : ""}`;
|
|
180
180
|
if (fullInner.length > 0) parts.push(`<w:rPr>${fullInner}</w:rPr>`);
|
|
181
181
|
}
|
|
182
|
-
} else if (
|
|
182
|
+
} else if (paragraphMarkChange) parts.push(`<w:rPr>${serializeParagraphMarkChange(paragraphMarkChange)}</w:rPr>`);
|
|
183
|
+
parts.push(serializeSectionProperties(sectionProperties));
|
|
183
184
|
if (propertyChanges && propertyChanges.length > 0) parts.push(...propertyChanges.map((change) => serializeParagraphPropertyChange(change)));
|
|
184
|
-
|
|
185
|
-
return
|
|
185
|
+
const inner = parts.join("");
|
|
186
|
+
if (inner.length === 0) return "";
|
|
187
|
+
return `<w:pPr>${inner}</w:pPr>`;
|
|
188
|
+
};
|
|
189
|
+
function serializeParagraphFormatting(formatting, propertyChanges, paragraphMarkChange) {
|
|
190
|
+
return serializeParagraphFormattingWithOptions(formatting, {
|
|
191
|
+
propertyChanges,
|
|
192
|
+
paragraphMarkChange
|
|
193
|
+
});
|
|
186
194
|
}
|
|
187
195
|
function extractPPrInner(pPrXml) {
|
|
188
196
|
if (!pPrXml.startsWith("<w:pPr>") || !pPrXml.endsWith("</w:pPr>")) return "";
|
|
@@ -486,9 +494,11 @@ function serializeParagraph(paragraph) {
|
|
|
486
494
|
if (paragraph.paraId) attrs.push(`w14:paraId="${escapeXml(paragraph.paraId)}"`);
|
|
487
495
|
if (paragraph.textId) attrs.push(`w14:textId="${escapeXml(paragraph.textId)}"`);
|
|
488
496
|
const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
497
|
+
parts.push(serializeParagraphFormattingWithOptions(paragraph.formatting, {
|
|
498
|
+
propertyChanges: paragraph.propertyChanges,
|
|
499
|
+
paragraphMarkChange: paragraph.pPrMark,
|
|
500
|
+
sectionProperties: paragraph.sectionProperties
|
|
501
|
+
}));
|
|
492
502
|
const explicitCommentReferenceIds = /* @__PURE__ */ new Set();
|
|
493
503
|
for (const content of paragraph.content) if (content.type === "commentReference") explicitCommentReferenceIds.add(content.id);
|
|
494
504
|
let pendingRenderedPageBreak = paragraph.renderedPageBreakBefore === true;
|
|
@@ -55,6 +55,9 @@ function parseSettings(xml) {
|
|
|
55
55
|
if (compatibilityMode !== void 0) settings.compatibilityMode = compatibilityMode;
|
|
56
56
|
const splitPageBreakAndParagraphMark = compat ? findChild(compat, "w", "splitPgBreakAndParaMark") : null;
|
|
57
57
|
if (splitPageBreakAndParagraphMark && parseBooleanElement(splitPageBreakAndParagraphMark)) settings.splitPageBreakAndParagraphMark = true;
|
|
58
|
+
const wordprocessingCompat = root ? findChildByNamespaceUri(root, WORDPROCESSINGML_NAMESPACE_URIS, "compat") : null;
|
|
59
|
+
const adjustLineHeightInTable = wordprocessingCompat ? findChildByNamespaceUri(wordprocessingCompat, WORDPROCESSINGML_NAMESPACE_URIS, "adjustLineHeightInTable") : null;
|
|
60
|
+
if (adjustLineHeightInTable && parseBooleanElement(adjustLineHeightInTable)) settings.adjustLineHeightInTable = true;
|
|
58
61
|
const applyBreakingRules = compat ? findChild(compat, "w", "applyBreakingRules") : null;
|
|
59
62
|
const useLegacyEthiopicAmharicRules = applyBreakingRules !== null && parseBooleanElement(applyBreakingRules);
|
|
60
63
|
if (noLineBreaksBefore || noLineBreaksAfter || useLegacyEthiopicAmharicRules) settings.lineBreakRules = {
|
|
@@ -14,10 +14,8 @@ type ParsedWatermark = {
|
|
|
14
14
|
hostingParagraph: XmlElement;
|
|
15
15
|
/**
|
|
16
16
|
* Index where the watermark paragraph sat among block-level siblings
|
|
17
|
-
* (`w:p` / `w:tbl`) in the source header.
|
|
18
|
-
*
|
|
19
|
-
* back at this index so a header that originally placed the
|
|
20
|
-
* watermark after visible text rounds-trips with the same flow.
|
|
17
|
+
* (`w:p` / `w:tbl` / `w:sdt`) in the source header. The serializer uses
|
|
18
|
+
* this index to replace the retained host paragraph with watermark XML.
|
|
21
19
|
*/
|
|
22
20
|
blockIndex: number;
|
|
23
21
|
};
|
|
@@ -59,11 +59,9 @@ function parseWatermark(header) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
* Index of `target` among block-level children of `header
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* serializer needs to splice the watermark XML back into so the
|
|
66
|
-
* header rounds-trips with the same flow as the source.
|
|
62
|
+
* Index of `target` among block-level children of `header`. Keep this list
|
|
63
|
+
* aligned with `parseBlockContent`, which emits paragraphs, tables, and
|
|
64
|
+
* block-level content controls as one content item each.
|
|
67
65
|
*/
|
|
68
66
|
function blockIndexOf(header, target) {
|
|
69
67
|
let blockIdx = 0;
|
|
@@ -71,7 +69,7 @@ function blockIndexOf(header, target) {
|
|
|
71
69
|
if (child === target) return blockIdx;
|
|
72
70
|
if (child.type !== "element") continue;
|
|
73
71
|
const local = getLocalName(child.name ?? "");
|
|
74
|
-
if (local === "p" || local === "tbl") blockIdx++;
|
|
72
|
+
if (local === "p" || local === "tbl" || local === "sdt") blockIdx++;
|
|
75
73
|
}
|
|
76
74
|
return 0;
|
|
77
75
|
}
|
package/dist/headless-layout.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveDocumentGridLinePitch } from "./docx/documentGrid.js";
|
|
2
|
+
import { formatOoxmlCounter } from "./docx/ooxmlCounterFormatter.js";
|
|
1
3
|
import { parseDocx } from "./docx/parser.js";
|
|
2
4
|
import { buildHeaderFooterFieldValues } from "./fields/resolveFieldValues.js";
|
|
3
5
|
import { extractEmbeddedFonts } from "./fonts/embeddedFonts.js";
|
|
@@ -50,6 +52,11 @@ import { createHash } from "node:crypto";
|
|
|
50
52
|
* backend in a browser); this module refuses to guess, because a silently
|
|
51
53
|
* wrong provider produces a plausible layout that is wrong everywhere.
|
|
52
54
|
*/
|
|
55
|
+
const formatEndnoteTexts = (numbers, formatNumber) => {
|
|
56
|
+
const texts = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const [id, displayNumber] of numbers) texts.set(id, formatNumber(displayNumber));
|
|
58
|
+
return texts;
|
|
59
|
+
};
|
|
53
60
|
var HeadlessLayoutError = class extends TaggedError("HeadlessLayoutError") {};
|
|
54
61
|
/**
|
|
55
62
|
* Stories this entry still does not lay out, keyed by story so the map is total
|
|
@@ -93,6 +100,8 @@ const buildFlowOptions = (document, pageContentHeight) => {
|
|
|
93
100
|
...settings.consecutiveHyphenLimit === void 0 ? {} : { consecutiveLineLimit: settings.consecutiveHyphenLimit },
|
|
94
101
|
...settings.hyphenationZoneTwips === void 0 ? {} : { hyphenationZoneTwips: settings.hyphenationZoneTwips }
|
|
95
102
|
};
|
|
103
|
+
const finalSectionDocumentGridLinePitchTwips = resolveDocumentGridLinePitch(document.package.document.sections?.at(-1)?.properties.docGrid);
|
|
104
|
+
if (finalSectionDocumentGridLinePitchTwips !== void 0) options.finalSectionDocumentGridLinePitchTwips = finalSectionDocumentGridLinePitchTwips;
|
|
96
105
|
return options;
|
|
97
106
|
};
|
|
98
107
|
/**
|
|
@@ -237,9 +246,12 @@ const layoutDocxHeadless = async (input, options = {}) => {
|
|
|
237
246
|
const footnotes = document.package.footnotes ?? [];
|
|
238
247
|
const endnotes = document.package.endnotes ?? [];
|
|
239
248
|
const footnoteRefs = collectFootnoteRefs(authored);
|
|
249
|
+
const footnoteNumbers = computeNoteDisplayNumbers(footnotes, footnoteRefs.map((ref) => ref.footnoteId));
|
|
250
|
+
const endnoteNumbers = computeNoteDisplayNumbers(endnotes, collectEndnoteRefs(authored).map((ref) => ref.endnoteId));
|
|
251
|
+
const endnoteNumberFormat = finalSection?.endnotePr?.numFmt ?? "lowerRoman";
|
|
240
252
|
const blocks = remapNoteMarkerText(authored, {
|
|
241
|
-
footnoteNumbers
|
|
242
|
-
|
|
253
|
+
footnoteNumbers,
|
|
254
|
+
endnoteTexts: formatEndnoteTexts(endnoteNumbers, (displayNumber) => formatOoxmlCounter(displayNumber, endnoteNumberFormat))
|
|
243
255
|
});
|
|
244
256
|
const measures = measureBlocks(blocks, contentWidth);
|
|
245
257
|
const footnoteContentById = footnoteRefs.length === 0 ? void 0 : buildFootnoteContentMap(footnotes, footnoteRefs, contentWidth, {
|
|
@@ -52,8 +52,8 @@ declare function computeNoteDisplayNumbers(notes: readonly NumberableNote[], ref
|
|
|
52
52
|
type NoteDisplayNumberMaps = {
|
|
53
53
|
/** footnote `w:id` → sequential display number */
|
|
54
54
|
footnoteNumbers?: ReadonlyMap<number, number>;
|
|
55
|
-
/** endnote `w:id` →
|
|
56
|
-
|
|
55
|
+
/** endnote `w:id` → formatted display text */
|
|
56
|
+
endnoteTexts?: ReadonlyMap<number, string>;
|
|
57
57
|
};
|
|
58
58
|
/**
|
|
59
59
|
* Rewrite body reference-marker run text from the raw `w:id` (which the PM
|
|
@@ -80,7 +80,7 @@ function computeNoteDisplayNumbers(notes, refNoteIds) {
|
|
|
80
80
|
* template preview substitution).
|
|
81
81
|
*/
|
|
82
82
|
function remapNoteMarkerText(blocks, maps) {
|
|
83
|
-
if ((maps.footnoteNumbers?.size ?? 0) === 0 && (maps.
|
|
83
|
+
if ((maps.footnoteNumbers?.size ?? 0) === 0 && (maps.endnoteTexts?.size ?? 0) === 0) return blocks;
|
|
84
84
|
let changed = false;
|
|
85
85
|
const next = blocks.map((block) => {
|
|
86
86
|
const remapped = remapNoteMarkerBlock(block, maps);
|
|
@@ -147,17 +147,19 @@ function remapNoteMarkerParagraph(block, maps) {
|
|
|
147
147
|
}
|
|
148
148
|
function remapNoteMarkerRun(run, maps) {
|
|
149
149
|
if (run.kind !== "text") return run;
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
return run.text === text ? run : {
|
|
150
|
+
const displayText = getRunDisplayText(run, maps);
|
|
151
|
+
if (displayText === void 0) return run;
|
|
152
|
+
return run.text === displayText ? run : {
|
|
154
153
|
...run,
|
|
155
|
-
text
|
|
154
|
+
text: displayText
|
|
156
155
|
};
|
|
157
156
|
}
|
|
158
|
-
function
|
|
159
|
-
if (run.footnoteRefId !== void 0)
|
|
160
|
-
|
|
157
|
+
function getRunDisplayText(run, maps) {
|
|
158
|
+
if (run.footnoteRefId !== void 0) {
|
|
159
|
+
const displayNumber = maps.footnoteNumbers?.get(run.footnoteRefId);
|
|
160
|
+
return displayNumber === void 0 ? void 0 : String(displayNumber);
|
|
161
|
+
}
|
|
162
|
+
if (run.endnoteRefId !== void 0) return maps.endnoteTexts?.get(run.endnoteRefId);
|
|
161
163
|
}
|
|
162
164
|
/**
|
|
163
165
|
* After layout, determine which footnotes appear on which pages.
|
|
@@ -202,7 +204,7 @@ function convertFootnoteToContent(footnote, displayNumber, contentWidth, options
|
|
|
202
204
|
if (options.justificationCompatibility) flowOptions.justificationCompatibility = options.justificationCompatibility;
|
|
203
205
|
if (options.tableIndentCompatibility) flowOptions.tableIndentCompatibility = options.tableIndentCompatibility;
|
|
204
206
|
if (options.automaticHyphenation) flowOptions.automaticHyphenation = options.automaticHyphenation;
|
|
205
|
-
const blocks = applyFootnotePresentation(toFlowBlocks(pmDoc, flowOptions), displayNumber);
|
|
207
|
+
const blocks = applyFootnotePresentation(preserveAuthoredFootnoteTerminalParagraph(toFlowBlocks(pmDoc, flowOptions)), displayNumber);
|
|
206
208
|
const measures = options.measureBlocks ? options.measureBlocks(blocks, contentWidth) : measureFootnoteBlocks(blocks, contentWidth);
|
|
207
209
|
let totalHeight = 0;
|
|
208
210
|
for (const measure of measures) if (measure.kind === "paragraph") totalHeight += measure.totalHeight;
|
|
@@ -216,6 +218,17 @@ function convertFootnoteToContent(footnote, displayNumber, contentWidth, options
|
|
|
216
218
|
height: totalHeight
|
|
217
219
|
};
|
|
218
220
|
}
|
|
221
|
+
/** Restore an authored footnote line that the body-only terminal-table policy collapsed. */
|
|
222
|
+
function preserveAuthoredFootnoteTerminalParagraph(blocks) {
|
|
223
|
+
const finalBlock = blocks.at(-1);
|
|
224
|
+
if (blocks.at(-2)?.kind !== "table" || finalBlock?.kind !== "paragraph" || finalBlock.attrs?.suppressEmptyParagraphHeight !== true) return blocks;
|
|
225
|
+
const attrs = { ...finalBlock.attrs };
|
|
226
|
+
delete attrs.suppressEmptyParagraphHeight;
|
|
227
|
+
return [...blocks.slice(0, -1), {
|
|
228
|
+
...finalBlock,
|
|
229
|
+
attrs
|
|
230
|
+
}];
|
|
231
|
+
}
|
|
219
232
|
function measureFootnoteBlocks(blocks, contentWidth) {
|
|
220
233
|
return blocks.map((block) => measureFootnoteBlock(block, contentWidth));
|
|
221
234
|
}
|
|
@@ -3,6 +3,16 @@ import { headerFooterToProseDoc } from "../../prosemirror/conversion/toProseDoc.
|
|
|
3
3
|
import { emuToPixels } from "../../utils/units.js";
|
|
4
4
|
import { toFlowBlocks } from "./toFlowBlocks.js";
|
|
5
5
|
//#region src/layout-bridge/convert/headerFooterLayout.ts
|
|
6
|
+
const DETACHED_WATERMARK_HOST = Symbol.for("stll.detachedWatermarkHost");
|
|
7
|
+
const headerFooterToProseDocWithDetachedWatermarkHost = (headerFooter, options) => {
|
|
8
|
+
return headerFooterToProseDoc(headerFooter.content.map((block, blockIndex) => {
|
|
9
|
+
if (blockIndex !== headerFooter.watermarkBlockIndex || block.type !== "paragraph") return block;
|
|
10
|
+
return {
|
|
11
|
+
...block,
|
|
12
|
+
[DETACHED_WATERMARK_HOST]: true
|
|
13
|
+
};
|
|
14
|
+
}), options);
|
|
15
|
+
};
|
|
6
16
|
function isAnchoredImageRun(run) {
|
|
7
17
|
if (run.kind !== "image") return false;
|
|
8
18
|
if (run.position) return true;
|
|
@@ -241,7 +251,7 @@ function calculateHeaderFooterVisualBounds(blocks, measures, flowHeight, metrics
|
|
|
241
251
|
* these bounds separately describe the in-flow portion for API consumers.
|
|
242
252
|
*/
|
|
243
253
|
function calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, metrics) {
|
|
244
|
-
if (blocks.length > 0 && blocks.every((block) => isPaintlessParagraph(block) && !hasAuthoredVisualContent(block))) return {
|
|
254
|
+
if (blocks.length > 0 && blocks.every((block) => block.kind === "paragraph" && isPaintlessParagraph(block) && !hasAuthoredVisualContent(block) && block.attrs?.suppressEmptyParagraphHeight !== false && !preservesInheritedSpacing(block))) return {
|
|
245
255
|
top: 0,
|
|
246
256
|
bottom: 0
|
|
247
257
|
};
|
|
@@ -330,7 +340,7 @@ function convertHeaderFooterToContent(headerFooter, contentWidth, metrics, optio
|
|
|
330
340
|
const proseDocOptions = {};
|
|
331
341
|
if (options.styles) proseDocOptions.styles = options.styles;
|
|
332
342
|
if (options.theme !== void 0) proseDocOptions.theme = options.theme;
|
|
333
|
-
const pmDoc = headerFooterToProseDoc(headerFooter.content, proseDocOptions);
|
|
343
|
+
const pmDoc = headerFooter.watermarkBlockIndex === void 0 ? headerFooterToProseDoc(headerFooter.content, proseDocOptions) : headerFooterToProseDocWithDetachedWatermarkHost(headerFooter, proseDocOptions);
|
|
334
344
|
const flowOptions = {};
|
|
335
345
|
if (options.theme !== void 0) flowOptions.theme = options.theme;
|
|
336
346
|
if (options.fontAlternates !== void 0) flowOptions.fontAlternates = options.fontAlternates;
|
|
@@ -458,6 +468,7 @@ function serializeParagraphAttrs(attrs) {
|
|
|
458
468
|
const keys = [
|
|
459
469
|
"alignment",
|
|
460
470
|
"bidi",
|
|
471
|
+
"suppressEmptyParagraphHeight",
|
|
461
472
|
"indent",
|
|
462
473
|
"spacing",
|
|
463
474
|
"styleId",
|