@stll/folio-core 0.6.0 → 0.7.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/controller/layoutPipeline.js +33 -14
- package/dist/docx/blockContentParser.js +2 -100
- package/dist/docx/groupDrawingParser.d.ts +1 -1
- package/dist/docx/groupDrawingParser.js +49 -8
- package/dist/docx/paragraphTextBoxEnrichment.d.ts +9 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +104 -0
- package/dist/docx/runParser.js +11 -2
- package/dist/docx/server/boundedArchive.d.ts +24 -0
- package/dist/docx/server/boundedArchive.js +106 -0
- package/dist/docx/server/extractDocxText.d.ts +23 -0
- package/dist/docx/server/extractDocxText.js +154 -0
- package/dist/docx/tableParser.js +2 -0
- package/dist/layout-bridge/convert/toFlowBlocks.js +75 -19
- package/dist/layout-bridge/sectionColumns.js +6 -1
- package/dist/layout-engine/index.js +120 -19
- package/dist/layout-engine/keep-together.d.ts +7 -5
- package/dist/layout-engine/keep-together.js +20 -4
- package/dist/layout-engine/measure/cache.js +2 -0
- package/dist/layout-engine/measure/measureBlocks.js +3 -2
- package/dist/layout-engine/measure/measureParagraph.js +30 -12
- package/dist/layout-engine/paginator.d.ts +2 -0
- package/dist/layout-engine/paginator.js +27 -15
- package/dist/layout-engine/tableRowBreak.js +3 -0
- package/dist/layout-engine/types.d.ts +20 -5
- package/dist/layout-painter/index.js +1 -1
- package/dist/layout-painter/renderPage.js +7 -2
- package/dist/layout-painter/renderParagraph.js +93 -9
- package/dist/layout-painter/renderTable.js +88 -10
- package/dist/paged-layout/sectionBlockWidths.js +11 -3
- package/dist/prosemirror/conversion/fromProseDoc.js +11 -2
- package/dist/prosemirror/conversion/toProseDoc.js +28 -20
- package/dist/prosemirror/extensions/nodes/TableExtension.js +3 -2
- package/dist/prosemirror/schema/nodes.d.ts +2 -1
- package/dist/prosemirror/utils/tabCalculator.js +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +3 -1
- package/dist/utils/formatToStyle.js +3 -3
- package/dist/utils/units.d.ts +6 -6
- package/dist/utils/units.js +8 -8
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { findAllDeep, findChild, findDeep, getAttributeAnyPrefix, getLocalName, getTextContent, parseXml } from "../xmlParser.js";
|
|
2
|
+
import { loadDocxArchive } from "./boundedArchive.js";
|
|
3
|
+
//#region src/docx/server/extractDocxText.ts
|
|
4
|
+
const HEADER_FOOTER_PATH = /^word\/(?:header|footer)\d+\.xml$/u;
|
|
5
|
+
const childElements = (element) => element.elements?.filter((child) => child.type === "element") ?? [];
|
|
6
|
+
const collectText = (element) => {
|
|
7
|
+
let text = "";
|
|
8
|
+
const walk = (node) => {
|
|
9
|
+
const localName = getLocalName(node.name ?? "");
|
|
10
|
+
if (localName === "t") {
|
|
11
|
+
text += getTextContent(node);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (localName === "br") {
|
|
15
|
+
text += "\n";
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (localName === "tab") {
|
|
19
|
+
text += " ";
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (localName === "del" || localName === "delText" || localName === "moveFrom") return;
|
|
23
|
+
for (const child of childElements(node)) walk(child);
|
|
24
|
+
};
|
|
25
|
+
walk(element);
|
|
26
|
+
return text;
|
|
27
|
+
};
|
|
28
|
+
const readParagraphProperties = (paragraph) => {
|
|
29
|
+
const properties = findChild(paragraph, "w", "pPr");
|
|
30
|
+
if (!properties) return {};
|
|
31
|
+
const result = {};
|
|
32
|
+
const styleValue = getAttributeAnyPrefix(findChild(properties, "w", "pStyle"), "val");
|
|
33
|
+
if (styleValue !== null) result.style = styleValue;
|
|
34
|
+
const alignment = getAttributeAnyPrefix(findChild(properties, "w", "jc"), "val");
|
|
35
|
+
if (alignment === "left" || alignment === "center" || alignment === "right" || alignment === "both") result.alignment = alignment;
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
const readRunMetrics = (paragraph) => {
|
|
39
|
+
const metrics = [];
|
|
40
|
+
for (const run of childElements(paragraph)) {
|
|
41
|
+
if (getLocalName(run.name ?? "") !== "r") continue;
|
|
42
|
+
const properties = findChild(run, "w", "rPr");
|
|
43
|
+
const boldProperty = findChild(properties, "w", "b");
|
|
44
|
+
const boldValue = getAttributeAnyPrefix(boldProperty, "val");
|
|
45
|
+
const bold = boldProperty !== null && boldValue !== "0" && boldValue !== "false";
|
|
46
|
+
const sizeValue = getAttributeAnyPrefix(findChild(properties, "w", "sz"), "val");
|
|
47
|
+
const parsedSize = sizeValue === null ? NaN : Number.parseInt(sizeValue, 10);
|
|
48
|
+
const fontSize = Number.isFinite(parsedSize) && parsedSize > 0 ? parsedSize : void 0;
|
|
49
|
+
let chars = 0;
|
|
50
|
+
for (const textNode of findAllDeep(run, "w", "t")) chars += getTextContent(textNode).length;
|
|
51
|
+
if (chars === 0) continue;
|
|
52
|
+
const entry = {
|
|
53
|
+
bold,
|
|
54
|
+
chars
|
|
55
|
+
};
|
|
56
|
+
if (fontSize !== void 0) entry.fontSize = fontSize;
|
|
57
|
+
metrics.push(entry);
|
|
58
|
+
}
|
|
59
|
+
return metrics;
|
|
60
|
+
};
|
|
61
|
+
const extractContainer = ({ container, source, startIndex }) => {
|
|
62
|
+
const paragraphs = [];
|
|
63
|
+
let charCount = 0;
|
|
64
|
+
for (const [offset, paragraph] of findAllDeep(container, "w", "p").entries()) {
|
|
65
|
+
const text = collectText(paragraph);
|
|
66
|
+
const entry = {
|
|
67
|
+
index: startIndex + offset,
|
|
68
|
+
text,
|
|
69
|
+
source
|
|
70
|
+
};
|
|
71
|
+
const { style, alignment } = readParagraphProperties(paragraph);
|
|
72
|
+
if (style !== void 0) entry.style = style;
|
|
73
|
+
if (alignment !== void 0) entry.alignment = alignment;
|
|
74
|
+
const runs = readRunMetrics(paragraph);
|
|
75
|
+
if (runs.length > 0) {
|
|
76
|
+
const totalChars = runs.reduce((sum, run) => sum + run.chars, 0);
|
|
77
|
+
if (runs.reduce((sum, run) => sum + (run.bold ? run.chars : 0), 0) > totalChars / 2) entry.bold = true;
|
|
78
|
+
const firstFontSize = runs.find((run) => run.fontSize !== void 0)?.fontSize;
|
|
79
|
+
if (firstFontSize !== void 0) entry.fontSize = firstFontSize;
|
|
80
|
+
}
|
|
81
|
+
paragraphs.push(entry);
|
|
82
|
+
charCount += text.length;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
paragraphs,
|
|
86
|
+
charCount
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const extractParts = async ({ archive, source, rootName, startIndex }) => {
|
|
90
|
+
const paragraphs = [];
|
|
91
|
+
let charCount = 0;
|
|
92
|
+
let nextIndex = startIndex;
|
|
93
|
+
const prefix = `word/${source}`;
|
|
94
|
+
const paths = archive.entries.filter((path) => HEADER_FOOTER_PATH.test(path) && path.startsWith(prefix)).toSorted();
|
|
95
|
+
for (const path of paths) {
|
|
96
|
+
const xml = await archive.readEntryString(path);
|
|
97
|
+
if (xml === null) continue;
|
|
98
|
+
const container = findDeep(parseXml(xml), "w", rootName);
|
|
99
|
+
if (!container) continue;
|
|
100
|
+
const result = extractContainer({
|
|
101
|
+
container,
|
|
102
|
+
source,
|
|
103
|
+
startIndex: nextIndex
|
|
104
|
+
});
|
|
105
|
+
paragraphs.push(...result.paragraphs);
|
|
106
|
+
charCount += result.charCount;
|
|
107
|
+
nextIndex += result.paragraphs.length;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
paragraphs,
|
|
111
|
+
charCount
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
const createEmptyResult = () => ({
|
|
115
|
+
paragraphs: [],
|
|
116
|
+
charCount: 0,
|
|
117
|
+
view: "accepted"
|
|
118
|
+
});
|
|
119
|
+
/** Extract paragraph text and formatting metadata from a DOCX archive. */
|
|
120
|
+
const extractDocxText = async (bytes) => {
|
|
121
|
+
const archive = await loadDocxArchive(bytes);
|
|
122
|
+
const documentXml = await archive.readEntryString("word/document.xml");
|
|
123
|
+
if (documentXml === null) return createEmptyResult();
|
|
124
|
+
const body = findDeep(parseXml(documentXml), "w", "body");
|
|
125
|
+
if (!body) return createEmptyResult();
|
|
126
|
+
const headers = await extractParts({
|
|
127
|
+
archive,
|
|
128
|
+
source: "header",
|
|
129
|
+
rootName: "hdr",
|
|
130
|
+
startIndex: 0
|
|
131
|
+
});
|
|
132
|
+
const bodyResult = extractContainer({
|
|
133
|
+
container: body,
|
|
134
|
+
source: "body",
|
|
135
|
+
startIndex: headers.paragraphs.length
|
|
136
|
+
});
|
|
137
|
+
const footers = await extractParts({
|
|
138
|
+
archive,
|
|
139
|
+
source: "footer",
|
|
140
|
+
rootName: "ftr",
|
|
141
|
+
startIndex: headers.paragraphs.length + bodyResult.paragraphs.length
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
paragraphs: [
|
|
145
|
+
...headers.paragraphs,
|
|
146
|
+
...bodyResult.paragraphs,
|
|
147
|
+
...footers.paragraphs
|
|
148
|
+
],
|
|
149
|
+
charCount: headers.charCount + bodyResult.charCount + footers.charCount,
|
|
150
|
+
view: "accepted"
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
//#endregion
|
|
154
|
+
export { extractDocxText };
|
package/dist/docx/tableParser.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
|
|
|
3
3
|
import { BorderStyleSchema, FloatingTableXSpecSchema, FloatingTableYSpecSchema, ShadingPatternSchema, TableCellTextDirectionSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
|
|
4
4
|
import { parseParagraph } from "./paragraphParser.js";
|
|
5
5
|
import { appendBookmarkMarkerToLastParagraphInBlocks, appendBookmarkMarkerToLastParagraphInCells, prependBookmarkMarkersToFirstParagraphInBlocks, prependBookmarkMarkersToFirstParagraphInCell } from "./bookmarkPlacement.js";
|
|
6
|
+
import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
|
|
6
7
|
//#region src/docx/tableParser.ts
|
|
7
8
|
/**
|
|
8
9
|
* Parse a table measurement (width, height, etc.)
|
|
@@ -489,6 +490,7 @@ function parseCellContent(tcElement, styles, theme, numbering, rels, media, opti
|
|
|
489
490
|
const localName = getLocalName(child.name);
|
|
490
491
|
if (localName === "p") {
|
|
491
492
|
const para = parseParagraph(child, styles, theme, numbering, rels, media, options);
|
|
493
|
+
enrichParagraphTextBoxes(para, child, styles, theme, numbering, rels, media);
|
|
492
494
|
prependPendingBookmarkMarkers(para, pendingBookmarkMarkers);
|
|
493
495
|
content.push(para);
|
|
494
496
|
} else if (localName === "tbl") {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { NUMBER_FORMAT_VALUES } from "../../types/documentEnumValues.js";
|
|
2
|
-
import { halfPointsToPixels, halfPointsToPoints, pointsToPixels } from "../../utils/units.js";
|
|
2
|
+
import { AUTO_PARAGRAPH_SPACING_PX, halfPointsToPixels, halfPointsToPoints, pointsToPixels } from "../../utils/units.js";
|
|
3
3
|
import { padDecimal } from "../../docx/numberingParser.js";
|
|
4
4
|
import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
|
|
5
5
|
import { resolveColor, resolveHighlightToCss } from "../../utils/colorResolver.js";
|
|
6
6
|
import { resolveShadingFill } from "../../utils/formatToStyle.js";
|
|
7
7
|
import { convertBulletToUnicode } from "../../docx/bulletMarkers.js";
|
|
8
8
|
import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js";
|
|
9
|
+
import { getColumns } from "../sectionColumns.js";
|
|
9
10
|
import { directionIsRtl } from "../../prosemirror/paragraphDirection.js";
|
|
10
|
-
import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
|
|
11
|
+
import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
|
|
11
12
|
import { autospacingMatchesBase } from "../../prosemirror/autospacingBase.js";
|
|
12
13
|
import { runShadingAttrsToShading } from "../../prosemirror/conversion/runShadingMark.js";
|
|
13
14
|
import { assertValidProseMirrorDocument } from "../../prosemirror/validation.js";
|
|
@@ -672,6 +673,7 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
|
|
|
672
673
|
else if (align === "center") attrs.alignment = "center";
|
|
673
674
|
else if (align === "right") attrs.alignment = "right";
|
|
674
675
|
}
|
|
676
|
+
if (typeof pmAttrs.outlineLevel === "number") attrs.outlineLevel = pmAttrs.outlineLevel;
|
|
675
677
|
const spaceBefore = pmAttrs.spaceBefore;
|
|
676
678
|
const spaceAfter = pmAttrs.spaceAfter;
|
|
677
679
|
const lineSpacing = pmAttrs.lineSpacing;
|
|
@@ -679,10 +681,14 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
|
|
|
679
681
|
const autoAfter = autospacingMatchesBase(pmAttrs._autospacingBase, "after", spaceAfter);
|
|
680
682
|
if (autoBefore || autoAfter || typeof spaceBefore === "number" || typeof spaceAfter === "number" || typeof lineSpacing === "number") {
|
|
681
683
|
attrs.spacing = {};
|
|
682
|
-
if (autoBefore) attrs.spacing.before =
|
|
684
|
+
if (autoBefore) attrs.spacing.before = AUTO_PARAGRAPH_SPACING_PX;
|
|
683
685
|
else if (typeof spaceBefore === "number") attrs.spacing.before = twipsToPixels(spaceBefore);
|
|
684
|
-
if (autoAfter) attrs.spacing.after =
|
|
686
|
+
if (autoAfter) attrs.spacing.after = AUTO_PARAGRAPH_SPACING_PX;
|
|
685
687
|
else if (typeof spaceAfter === "number") attrs.spacing.after = twipsToPixels(spaceAfter);
|
|
688
|
+
if (autoBefore || autoAfter) attrs.automaticSpacing = {
|
|
689
|
+
...autoBefore ? { before: true } : {},
|
|
690
|
+
...autoAfter ? { after: true } : {}
|
|
691
|
+
};
|
|
686
692
|
const pmSpacingExplicit = pmAttrs.spacingExplicit;
|
|
687
693
|
const spacingFromDocDefaults = pmAttrs.spacingFromDocDefaults;
|
|
688
694
|
const explicit = {};
|
|
@@ -702,7 +708,7 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
|
|
|
702
708
|
let indentLeft = typeof pmAttrs.indentLeft === "number" ? pmAttrs.indentLeft : void 0;
|
|
703
709
|
let indentFirstLine = typeof pmAttrs.indentFirstLine === "number" ? pmAttrs.indentFirstLine : void 0;
|
|
704
710
|
let hangingIndent = pmAttrs.hangingIndent;
|
|
705
|
-
if (pmAttrs.numPr?.numId && indentLeft === void 0) {
|
|
711
|
+
if (pmAttrs.numPr?.numId && indentLeft === void 0 && indentFirstLine === void 0) {
|
|
706
712
|
indentLeft = ((pmAttrs.numPr.ilvl ?? 0) + 1) * 720;
|
|
707
713
|
if (indentFirstLine === void 0) {
|
|
708
714
|
indentFirstLine = -360;
|
|
@@ -828,6 +834,7 @@ function convertParagraph(node, startPos, options) {
|
|
|
828
834
|
const attrs = convertParagraphAttrs(pmAttrs, options.theme, options.listCounters, options.listAbstractCounters, options.listSeenNumIds, options.defaultTabStopTwips, options.originalListCounters, options.originalListAbstractCounters, options.originalListSeenNumIds);
|
|
829
835
|
const defaultTextFormatting = pmAttrs.defaultTextFormatting;
|
|
830
836
|
if (runs.length === 0) {
|
|
837
|
+
if (pmAttrs._originalFormatting && Object.entries(pmAttrs._originalFormatting).some(([key, value]) => key !== "runProperties" && value !== void 0 && value !== null)) attrs.hasDirectParagraphFormatting = true;
|
|
831
838
|
const paragraphMarkFormatting = pmAttrs._originalFormatting?.runProperties;
|
|
832
839
|
if (paragraphMarkFormatting?.fontSize !== void 0) attrs.defaultFontSize = paragraphMarkFormatting.fontSize / 2;
|
|
833
840
|
const paragraphMarkFontFamily = paragraphMarkFormatting?.fontFamily?.ascii ?? paragraphMarkFormatting?.fontFamily?.hAnsi;
|
|
@@ -850,6 +857,37 @@ function convertParagraph(node, startPos, options) {
|
|
|
850
857
|
};
|
|
851
858
|
}
|
|
852
859
|
/**
|
|
860
|
+
* Word keeps terminal empty body paragraphs after a final table as editable
|
|
861
|
+
* anchors, but they do not create a page of their own. Preserve every block
|
|
862
|
+
* and PM range while collapsing only the contiguous, run-free suffix; empty
|
|
863
|
+
* paragraphs elsewhere still retain their normal line height.
|
|
864
|
+
*/
|
|
865
|
+
function isPaintlessTerminalParagraph(block) {
|
|
866
|
+
if (block?.kind !== "paragraph" || block.runs.length !== 0) return false;
|
|
867
|
+
const attrs = block.attrs;
|
|
868
|
+
return !(attrs?.listMarker !== void 0 && !attrs.listMarkerHidden || attrs?.borders?.top || attrs?.borders?.bottom || attrs?.borders?.left || attrs?.borders?.right || attrs?.borders?.between || attrs?.borders?.bar || attrs?.shading || attrs?.spacingExplicit?.before || attrs?.spacingExplicit?.after || attrs?.pageBreakBefore || attrs?.renderedPageBreakBefore);
|
|
869
|
+
}
|
|
870
|
+
function suppressTerminalEmptyParagraphsAfterTable(blocks) {
|
|
871
|
+
let suffixStart = blocks.length;
|
|
872
|
+
while (suffixStart > 0 && isPaintlessTerminalParagraph(blocks[suffixStart - 1])) suffixStart -= 1;
|
|
873
|
+
if (suffixStart === blocks.length || suffixStart === 0 || blocks[suffixStart - 1]?.kind !== "table") return;
|
|
874
|
+
for (let index = suffixStart; index < blocks.length; index += 1) {
|
|
875
|
+
const block = blocks[index];
|
|
876
|
+
if (isPaintlessTerminalParagraph(block)) block.attrs = {
|
|
877
|
+
...block.attrs,
|
|
878
|
+
suppressEmptyParagraphHeight: true
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function reserveLeadingEmptyOutlineHeight(blocks) {
|
|
883
|
+
const firstBlock = blocks.at(0);
|
|
884
|
+
if (firstBlock?.kind !== "paragraph" || firstBlock.runs.length !== 0 || firstBlock.attrs?.outlineLevel !== 0) return;
|
|
885
|
+
firstBlock.attrs = {
|
|
886
|
+
...firstBlock.attrs,
|
|
887
|
+
reserveEmptyOutlineHeight: true
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
853
891
|
* Convert border width from eighths of a point to pixels.
|
|
854
892
|
* OOXML stores border widths in eighths of a point.
|
|
855
893
|
*/
|
|
@@ -919,6 +957,7 @@ function convertTableCell(node, startPos, options, tableCellMargins) {
|
|
|
919
957
|
const block = convertParagraph(child, offset, options);
|
|
920
958
|
blocks.push(block);
|
|
921
959
|
} else if (child.type.name === "table") blocks.push(convertTable(child, offset, options));
|
|
960
|
+
else if (child.type.name === "textBox") blocks.push(convertTextBoxNode(child, offset, options));
|
|
922
961
|
offset += child.nodeSize;
|
|
923
962
|
});
|
|
924
963
|
const trailingBlock = blocks.at(-1);
|
|
@@ -999,7 +1038,8 @@ function convertTable(node, startPos, options) {
|
|
|
999
1038
|
}
|
|
1000
1039
|
const justification = attrs.justification;
|
|
1001
1040
|
const originalFormatting = attrs._originalFormatting;
|
|
1002
|
-
const
|
|
1041
|
+
const effectiveIndent = attrs._resolvedIndent ?? originalFormatting?.indent;
|
|
1042
|
+
const indentPx = effectiveIndent?.value !== void 0 && effectiveIndent?.type === "dxa" ? twipsToPixels(effectiveIndent.value) : void 0;
|
|
1003
1043
|
const floating = attrs.floating;
|
|
1004
1044
|
let floatingPx;
|
|
1005
1045
|
if (floating) {
|
|
@@ -1213,11 +1253,33 @@ function toFlowBlocks(doc, options = {}) {
|
|
|
1213
1253
|
}
|
|
1214
1254
|
switch (node.type.name) {
|
|
1215
1255
|
case "paragraph": {
|
|
1216
|
-
const block = convertParagraph(node, pos, opts);
|
|
1217
1256
|
const pmAttrs = expectParagraphAttrs(node);
|
|
1218
|
-
trackedPush(block);
|
|
1219
1257
|
const secProps = pmAttrs._sectionProperties;
|
|
1220
|
-
|
|
1258
|
+
const hasSectionBreak = secProps !== void 0 || pmAttrs.sectionBreakType !== null && pmAttrs.sectionBreakType !== void 0;
|
|
1259
|
+
const hasListFormatting = pmAttrs.numPr !== null && pmAttrs.numPr !== void 0 || pmAttrs.listMarker !== null && pmAttrs.listMarker !== void 0;
|
|
1260
|
+
const firstChild = node.firstChild;
|
|
1261
|
+
const startsWithColumnBreak = firstChild?.type.name === "hardBreak" && expectHardBreakAttrs(firstChild).breakType === "column";
|
|
1262
|
+
if (node.childCount === 1 && startsWithColumnBreak) {
|
|
1263
|
+
const columnBreak = {
|
|
1264
|
+
kind: "columnBreak",
|
|
1265
|
+
id: nextBlockId(),
|
|
1266
|
+
pmStart: pos,
|
|
1267
|
+
pmEnd: pos + node.nodeSize
|
|
1268
|
+
};
|
|
1269
|
+
trackedPush(columnBreak);
|
|
1270
|
+
} else if (startsWithColumnBreak && firstChild) {
|
|
1271
|
+
const columnBreak = {
|
|
1272
|
+
kind: "columnBreak",
|
|
1273
|
+
id: nextBlockId(),
|
|
1274
|
+
pmStart: pos + 1,
|
|
1275
|
+
pmEnd: pos + 1 + firstChild.nodeSize
|
|
1276
|
+
};
|
|
1277
|
+
trackedPush(columnBreak);
|
|
1278
|
+
const paragraph = convertParagraph(node, pos, opts);
|
|
1279
|
+
if (paragraph.runs.at(0)?.kind === "lineBreak") paragraph.runs.shift();
|
|
1280
|
+
trackedPush(paragraph);
|
|
1281
|
+
} else if (node.content.size > 0 || hasListFormatting || !hasSectionBreak) trackedPush(convertParagraph(node, pos, opts));
|
|
1282
|
+
if (hasSectionBreak) {
|
|
1221
1283
|
const sectionBreak = {
|
|
1222
1284
|
kind: "sectionBreak",
|
|
1223
1285
|
id: nextBlockId()
|
|
@@ -1245,16 +1307,8 @@ function toFlowBlocks(doc, options = {}) {
|
|
|
1245
1307
|
if (secProps.headerDistance !== void 0) sectionBreak.margins.header = twipsToPixels(secProps.headerDistance);
|
|
1246
1308
|
if (secProps.footerDistance !== void 0) sectionBreak.margins.footer = twipsToPixels(secProps.footerDistance);
|
|
1247
1309
|
}
|
|
1248
|
-
const
|
|
1249
|
-
if (
|
|
1250
|
-
const cols = {
|
|
1251
|
-
count: colCount,
|
|
1252
|
-
gap: twipsToPixels(secProps.columnSpace ?? 720),
|
|
1253
|
-
equalWidth: secProps.equalWidth ?? true
|
|
1254
|
-
};
|
|
1255
|
-
if (secProps.separator !== void 0) cols.separator = secProps.separator;
|
|
1256
|
-
sectionBreak.columns = cols;
|
|
1257
|
-
}
|
|
1310
|
+
const columns = getColumns(secProps);
|
|
1311
|
+
if (columns) sectionBreak.columns = columns;
|
|
1258
1312
|
}
|
|
1259
1313
|
trackedPush(sectionBreak);
|
|
1260
1314
|
}
|
|
@@ -1286,6 +1340,8 @@ function toFlowBlocks(doc, options = {}) {
|
|
|
1286
1340
|
doc.forEach((node, nodeOffset) => {
|
|
1287
1341
|
visit(node, offset + nodeOffset);
|
|
1288
1342
|
});
|
|
1343
|
+
reserveLeadingEmptyOutlineHeight(blocks);
|
|
1344
|
+
suppressTerminalEmptyParagraphsAfterTable(blocks);
|
|
1289
1345
|
return mergeRunInParagraphs(blocks);
|
|
1290
1346
|
}
|
|
1291
1347
|
/**
|
|
@@ -7,13 +7,18 @@ const DEFAULT_COLUMN_SPACE_TWIPS = 720;
|
|
|
7
7
|
* Returns undefined for single-column (default) to avoid unnecessary paginator overhead.
|
|
8
8
|
*/
|
|
9
9
|
function getColumns(sectionProps) {
|
|
10
|
-
const
|
|
10
|
+
const authoredColumns = sectionProps?.columns;
|
|
11
|
+
const count = sectionProps?.columnCount ?? authoredColumns?.length ?? 1;
|
|
11
12
|
if (count <= 1) return void 0;
|
|
12
13
|
const columns = {
|
|
13
14
|
count,
|
|
14
15
|
gap: twipsToPixels(sectionProps?.columnSpace ?? DEFAULT_COLUMN_SPACE_TWIPS),
|
|
15
16
|
equalWidth: sectionProps?.equalWidth ?? true
|
|
16
17
|
};
|
|
18
|
+
if (sectionProps?.equalWidth === false && authoredColumns?.length === count && authoredColumns.every(({ width }) => width !== void 0)) {
|
|
19
|
+
columns.widths = authoredColumns.map(({ width }) => twipsToPixels(width ?? 0));
|
|
20
|
+
columns.gaps = authoredColumns.slice(0, -1).map(({ space }) => twipsToPixels(space ?? sectionProps.columnSpace ?? DEFAULT_COLUMN_SPACE_TWIPS));
|
|
21
|
+
}
|
|
17
22
|
if (sectionProps?.separator !== void 0) columns.separator = sectionProps.separator;
|
|
18
23
|
return columns;
|
|
19
24
|
}
|
|
@@ -44,9 +44,7 @@ function collectSectionConfigs(blocks, initialConfig, finalConfig) {
|
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* Whether a paragraph block has no visible content (no runs, or a single
|
|
47
|
-
* empty text run).
|
|
48
|
-
* paragraphs (only direct `<w:pPr><w:spacing>` formatting survives) — see
|
|
49
|
-
* eigenpal #402.
|
|
47
|
+
* empty text run).
|
|
50
48
|
*/
|
|
51
49
|
function isEmptyParagraph(block) {
|
|
52
50
|
if (block.runs.length === 0) return true;
|
|
@@ -68,16 +66,27 @@ function pageHasVisibleBodyContent(page, blocksById) {
|
|
|
68
66
|
}
|
|
69
67
|
return false;
|
|
70
68
|
}
|
|
69
|
+
function continuesNumberedSequence(previous, current) {
|
|
70
|
+
if (previous?.kind !== "paragraph" || current.kind !== "paragraph") return false;
|
|
71
|
+
const previousNumPr = previous.attrs?.numPr;
|
|
72
|
+
const currentNumPr = current.attrs?.numPr;
|
|
73
|
+
if (previousNumPr?.numId === void 0 || currentNumPr?.numId === void 0) return false;
|
|
74
|
+
return previousNumPr.numId === currentNumPr.numId && previousNumPr.ilvl === currentNumPr.ilvl;
|
|
75
|
+
}
|
|
76
|
+
function pageStartsWithPreviousParagraphContinuation(page, previousBlock) {
|
|
77
|
+
if (previousBlock?.kind !== "paragraph") return false;
|
|
78
|
+
return page.fragments.some((fragment) => fragment.kind === "paragraph" && String(fragment.blockId) === String(previousBlock.id) && fragment.continuesFromPrev === true);
|
|
79
|
+
}
|
|
71
80
|
/**
|
|
72
81
|
* Get spacing before a paragraph block. Empty paragraphs whose
|
|
73
82
|
* `before` came only from the implicit default paragraph style collapse to
|
|
74
|
-
* zero.
|
|
75
|
-
*
|
|
83
|
+
* zero. Reference layout keeps inherited spacing when the empty paragraph itself is
|
|
84
|
+
* authored through direct `w:pPr` formatting or an explicit `w:pStyle`.
|
|
76
85
|
*/
|
|
77
86
|
function getSpacingBefore(block) {
|
|
78
87
|
const value = block.attrs?.spacing?.before ?? 0;
|
|
79
88
|
if (value === 0) return 0;
|
|
80
|
-
if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.before) return 0;
|
|
89
|
+
if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.hasDirectParagraphFormatting && !block.attrs?.spacingExplicit?.before) return 0;
|
|
81
90
|
return value;
|
|
82
91
|
}
|
|
83
92
|
/**
|
|
@@ -87,9 +96,59 @@ function getSpacingBefore(block) {
|
|
|
87
96
|
function getSpacingAfter(block) {
|
|
88
97
|
const value = block.attrs?.spacing?.after ?? 0;
|
|
89
98
|
if (value === 0) return 0;
|
|
90
|
-
if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.after) return 0;
|
|
99
|
+
if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.hasDirectParagraphFormatting && !block.attrs?.spacingExplicit?.after) return 0;
|
|
91
100
|
return value;
|
|
92
101
|
}
|
|
102
|
+
function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex, incomingSpacing, columnCount, availableHeight }) {
|
|
103
|
+
if (columnCount <= 1 || startIndex >= endIndex || availableHeight <= 0) return;
|
|
104
|
+
const lineUnits = [];
|
|
105
|
+
let totalHeight = 0;
|
|
106
|
+
let tallestUnit = 0;
|
|
107
|
+
let trailingSpacing = incomingSpacing;
|
|
108
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
109
|
+
const block = blocks[index];
|
|
110
|
+
const measure = measures[index];
|
|
111
|
+
if (block?.kind !== "paragraph" || measure?.kind !== "paragraph") return;
|
|
112
|
+
if (block.attrs?.keepNext === true || block.attrs?.keepLines === true || block.runs.some((run) => run.kind === "text" && run.footnoteRefId !== void 0)) return;
|
|
113
|
+
const leadingSpacing = Math.max(getSpacingBefore(block), trailingSpacing);
|
|
114
|
+
if (measure.lines.length === 0) {
|
|
115
|
+
if (leadingSpacing > 0) {
|
|
116
|
+
lineUnits.push(leadingSpacing);
|
|
117
|
+
totalHeight += leadingSpacing;
|
|
118
|
+
tallestUnit = Math.max(tallestUnit, leadingSpacing);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (let lineIndex = 0; lineIndex < measure.lines.length; lineIndex++) {
|
|
122
|
+
const line = measure.lines[lineIndex];
|
|
123
|
+
if (!line) continue;
|
|
124
|
+
const unitHeight = measuredLineAdvance(line) + (lineIndex === 0 ? leadingSpacing : 0);
|
|
125
|
+
lineUnits.push(unitHeight);
|
|
126
|
+
totalHeight += unitHeight;
|
|
127
|
+
tallestUnit = Math.max(tallestUnit, unitHeight);
|
|
128
|
+
}
|
|
129
|
+
if (tallestUnit > availableHeight || totalHeight > availableHeight * columnCount) return;
|
|
130
|
+
trailingSpacing = getSpacingAfter(block);
|
|
131
|
+
}
|
|
132
|
+
if (totalHeight <= 0) return;
|
|
133
|
+
const columnsNeeded = (targetHeight) => {
|
|
134
|
+
let usedHeight = 0;
|
|
135
|
+
let usedColumns = 1;
|
|
136
|
+
for (const unitHeight of lineUnits) if (usedHeight > 0 && usedHeight + unitHeight > targetHeight) {
|
|
137
|
+
usedColumns += 1;
|
|
138
|
+
usedHeight = unitHeight;
|
|
139
|
+
} else usedHeight += unitHeight;
|
|
140
|
+
return usedColumns;
|
|
141
|
+
};
|
|
142
|
+
let lower = Math.max(tallestUnit, totalHeight / columnCount);
|
|
143
|
+
let upper = Math.min(totalHeight, availableHeight);
|
|
144
|
+
if (columnsNeeded(upper) > columnCount) return;
|
|
145
|
+
for (let iteration = 0; iteration < 32; iteration++) {
|
|
146
|
+
const middle = (lower + upper) / 2;
|
|
147
|
+
if (columnsNeeded(middle) <= columnCount) upper = middle;
|
|
148
|
+
else lower = middle;
|
|
149
|
+
}
|
|
150
|
+
return Math.ceil(upper * 1e3) / 1e3;
|
|
151
|
+
}
|
|
93
152
|
function hasWidowControl(block) {
|
|
94
153
|
return block.attrs?.widowControl !== false;
|
|
95
154
|
}
|
|
@@ -123,6 +182,24 @@ function applyContextualSpacing(blocks) {
|
|
|
123
182
|
for (const block of blocks) if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) applyContextualSpacing(cell.blocks);
|
|
124
183
|
else if (block.kind === "textBox") applyContextualSpacing(block.content);
|
|
125
184
|
}
|
|
185
|
+
/** Suppress automatic inter-item spacing within one numbered sequence. */
|
|
186
|
+
const applyAutomaticListSpacing = (blocks) => {
|
|
187
|
+
for (let index = 1; index < blocks.length; index++) {
|
|
188
|
+
const previous = blocks[index - 1];
|
|
189
|
+
const current = blocks[index];
|
|
190
|
+
if (previous?.kind !== "paragraph" || current?.kind !== "paragraph" || !continuesNumberedSequence(previous, current)) continue;
|
|
191
|
+
if (previous.attrs?.automaticSpacing?.after && previous.attrs.spacing) previous.attrs.spacing = {
|
|
192
|
+
...previous.attrs.spacing,
|
|
193
|
+
after: 0
|
|
194
|
+
};
|
|
195
|
+
if (current.attrs?.automaticSpacing?.before && current.attrs.spacing) current.attrs.spacing = {
|
|
196
|
+
...current.attrs.spacing,
|
|
197
|
+
before: 0
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
for (const block of blocks) if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) applyAutomaticListSpacing(cell.blocks);
|
|
201
|
+
else if (block.kind === "textBox") applyAutomaticListSpacing(block.content);
|
|
202
|
+
};
|
|
126
203
|
/**
|
|
127
204
|
* Layout a document: convert blocks + measures into pages with positioned fragments.
|
|
128
205
|
*
|
|
@@ -156,7 +233,8 @@ function layoutDocument(blocks, measures, options) {
|
|
|
156
233
|
pageSize: finalPageSize,
|
|
157
234
|
margins: finalMargins
|
|
158
235
|
};
|
|
159
|
-
|
|
236
|
+
const finalColumns = options.finalColumns ?? options.columns;
|
|
237
|
+
if (finalColumns !== void 0) finalConfig.columns = finalColumns;
|
|
160
238
|
const { configs: sectionConfigs, breakIndices } = collectSectionConfigs(blocks, bodyConfig, finalConfig);
|
|
161
239
|
const sectionBreakTypes = [...breakIndices.map((index) => blocks[index].type), options.bodyBreakType];
|
|
162
240
|
const initialConfig = sectionConfigs.at(0) ?? bodyConfig;
|
|
@@ -170,6 +248,7 @@ function layoutDocument(blocks, measures, options) {
|
|
|
170
248
|
...options.sectionHeaderFooterRefs !== void 0 ? { sectionHeaderFooterRefs: options.sectionHeaderFooterRefs } : {}
|
|
171
249
|
});
|
|
172
250
|
applyContextualSpacing(blocks);
|
|
251
|
+
applyAutomaticListSpacing(blocks);
|
|
173
252
|
const keepNextChains = computeKeepNextChains(blocks);
|
|
174
253
|
const midChainIndices = getMidChainIndices(keepNextChains);
|
|
175
254
|
const blocksById = /* @__PURE__ */ new Map();
|
|
@@ -178,7 +257,7 @@ function layoutDocument(blocks, measures, options) {
|
|
|
178
257
|
let activeSectionMarginTop = initialConfig.margins.top;
|
|
179
258
|
let activeSectionPageHeight = initialConfig.pageSize.h;
|
|
180
259
|
let activeSectionMarginBottom = initialConfig.margins.bottom;
|
|
181
|
-
let naturalPageAdvanceSinceRenderedBreak =
|
|
260
|
+
let naturalPageAdvanceSinceRenderedBreak = "none";
|
|
182
261
|
for (let i = 0; i < blocks.length; i++) {
|
|
183
262
|
const block = blocks[i];
|
|
184
263
|
const measure = measures[i];
|
|
@@ -186,18 +265,20 @@ function layoutDocument(blocks, measures, options) {
|
|
|
186
265
|
if (hasPageBreakBefore(block)) paginator.forcePageBreak();
|
|
187
266
|
else if (hasRenderedPageBreak) {
|
|
188
267
|
const state = paginator.getCurrentState();
|
|
189
|
-
if (!(block.kind === "paragraph" && isPaginationEmptyParagraph(block) && naturalPageAdvanceSinceRenderedBreak) && pageHasVisibleBodyContent(state.page, blocksById)) paginator.forcePageBreak();
|
|
268
|
+
if (!(pageStartsWithPreviousParagraphContinuation(state.page, blocks[i - 1]) || naturalPageAdvanceSinceRenderedBreak === "reflowBoundary" || block.kind === "paragraph" && isPaginationEmptyParagraph(block) && naturalPageAdvanceSinceRenderedBreak !== "none" || naturalPageAdvanceSinceRenderedBreak !== "none" && continuesNumberedSequence(blocks[i - 1], block)) && pageHasVisibleBodyContent(state.page, blocksById)) paginator.forcePageBreak();
|
|
190
269
|
}
|
|
191
|
-
if (hasRenderedPageBreak || hasPageBreakBefore(block)) naturalPageAdvanceSinceRenderedBreak =
|
|
270
|
+
if (hasRenderedPageBreak || hasPageBreakBefore(block)) naturalPageAdvanceSinceRenderedBreak = "none";
|
|
192
271
|
const chain = keepNextChains.get(i);
|
|
193
272
|
if (chain && !midChainIndices.has(i)) {
|
|
194
273
|
const chainHeight = calculateChainHeight(chain, blocks, measures);
|
|
274
|
+
const pageBeforeChainLayout = paginator.getCurrentState().page.number;
|
|
195
275
|
paginator.ensureFits(chainHeight);
|
|
276
|
+
if (paginator.getCurrentState().page.number > pageBeforeChainLayout) naturalPageAdvanceSinceRenderedBreak = "reflowBoundary";
|
|
196
277
|
}
|
|
197
278
|
const pageBeforeBlockLayout = paginator.getCurrentState().page.number;
|
|
198
279
|
switch (block.kind) {
|
|
199
280
|
case "paragraph":
|
|
200
|
-
layoutParagraph(block, measure, paginator, paginator.
|
|
281
|
+
layoutParagraph(block, measure, paginator, paginator.columnWidth, options.footnoteHeightById);
|
|
201
282
|
break;
|
|
202
283
|
case "table":
|
|
203
284
|
if (block.floating) layoutFloatingTable(block, measure, paginator, paginator.getContentWidth());
|
|
@@ -223,6 +304,23 @@ function layoutDocument(blocks, measures, options) {
|
|
|
223
304
|
case "sectionBreak": {
|
|
224
305
|
const nextSectionConfig = sectionConfigs[sectionIdx + 1] ?? initialConfig;
|
|
225
306
|
handleSectionBreak(block, paginator, nextSectionConfig, sectionBreakTypes[sectionIdx + 1] ?? sectionBreakTypes[sectionIdx], sectionIdx + 1);
|
|
307
|
+
const nextColumns = nextSectionConfig.columns;
|
|
308
|
+
const nextBreakIndex = breakIndices[sectionIdx + 1] ?? blocks.length;
|
|
309
|
+
const nextBreak = blocks[nextBreakIndex];
|
|
310
|
+
const sectionEndsContinuously = nextBreakIndex === blocks.length || nextBreak?.kind === "sectionBreak" && nextBreak.type === "continuous";
|
|
311
|
+
if (nextColumns && sectionEndsContinuously) {
|
|
312
|
+
const state = paginator.getCurrentState();
|
|
313
|
+
const balancedHeight = balancedParagraphSectionHeight({
|
|
314
|
+
blocks,
|
|
315
|
+
measures,
|
|
316
|
+
startIndex: i + 1,
|
|
317
|
+
endIndex: nextBreakIndex,
|
|
318
|
+
incomingSpacing: state.trailingSpacing,
|
|
319
|
+
columnCount: nextColumns.count,
|
|
320
|
+
availableHeight: paginator.getAvailableHeight()
|
|
321
|
+
});
|
|
322
|
+
if (balancedHeight !== void 0) state.contentBottom = Math.min(state.contentBottom, state.cursorY + balancedHeight);
|
|
323
|
+
}
|
|
226
324
|
activeSectionMarginTop = nextSectionConfig.margins.top;
|
|
227
325
|
activeSectionPageHeight = nextSectionConfig.pageSize.h;
|
|
228
326
|
activeSectionMarginBottom = nextSectionConfig.margins.bottom;
|
|
@@ -232,8 +330,8 @@ function layoutDocument(blocks, measures, options) {
|
|
|
232
330
|
default: break;
|
|
233
331
|
}
|
|
234
332
|
const isVisibleBodyBlock = block.kind === "paragraph" ? !isEmptyParagraph(block) : block.kind !== "pageBreak" && block.kind !== "columnBreak" && block.kind !== "sectionBreak";
|
|
235
|
-
if (block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak") naturalPageAdvanceSinceRenderedBreak =
|
|
236
|
-
else if (isVisibleBodyBlock && paginator.getCurrentState().page.number > pageBeforeBlockLayout) naturalPageAdvanceSinceRenderedBreak =
|
|
333
|
+
if (block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak") naturalPageAdvanceSinceRenderedBreak = "none";
|
|
334
|
+
else if (isVisibleBodyBlock && paginator.getCurrentState().page.number > pageBeforeBlockLayout) naturalPageAdvanceSinceRenderedBreak = paginator.pages[pageBeforeBlockLayout - 1]?.fragments.some(({ blockId }) => blockId === block.id) ? "reflowBoundary" : "ordinary";
|
|
237
335
|
}
|
|
238
336
|
if (paginator.pages.length === 0) paginator.getCurrentState();
|
|
239
337
|
return {
|
|
@@ -428,9 +526,10 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
|
|
|
428
526
|
let x = paginator.getColumnX(columnIndex);
|
|
429
527
|
if (block.justification === "center") x += (paginator.columnWidth - measure.totalWidth) / 2;
|
|
430
528
|
else if (block.justification === "right") x = x + paginator.columnWidth - measure.totalWidth;
|
|
431
|
-
else if (block.indent !== void 0)
|
|
432
|
-
|
|
433
|
-
|
|
529
|
+
else if (block.indent !== void 0) x += block.indent;
|
|
530
|
+
else {
|
|
531
|
+
const leadingCellMargin = block.rows.at(0)?.cells.at(0)?.padding?.left ?? 0;
|
|
532
|
+
x -= leadingCellMargin;
|
|
434
533
|
}
|
|
435
534
|
return x;
|
|
436
535
|
};
|
|
@@ -637,8 +736,10 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
|
|
|
637
736
|
else if (spec === "center") y = baseY + (contentHeight - tableHeight) / 2;
|
|
638
737
|
}
|
|
639
738
|
if (!usedExplicitY) y = paginator.ensureFits(tableHeight).cursorY;
|
|
640
|
-
const
|
|
641
|
-
const
|
|
739
|
+
const usesNumericOffset = floating?.tblpX !== void 0;
|
|
740
|
+
const clampToPage = floating?.horzAnchor === "page" || usesNumericOffset;
|
|
741
|
+
const minX = clampToPage ? 0 : margins.left;
|
|
742
|
+
const maxX = clampToPage ? page.size.w - tableWidth : margins.left + contentWidth - tableWidth;
|
|
642
743
|
if (Number.isFinite(maxX)) x = Math.max(minX, Math.min(x, maxX));
|
|
643
744
|
const fragment = {
|
|
644
745
|
kind: "table",
|
|
@@ -2,8 +2,9 @@ import { FlowBlock, Measure } from "./types.js";
|
|
|
2
2
|
|
|
3
3
|
//#region src/layout-engine/keep-together.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* A chain of
|
|
6
|
-
*
|
|
5
|
+
* A chain of paragraphs that Word keeps with following content. This includes
|
|
6
|
+
* explicit keepNext links and a trailing table separator that would otherwise
|
|
7
|
+
* be stranded at the bottom of a page.
|
|
7
8
|
*/
|
|
8
9
|
type KeepNextChain = {
|
|
9
10
|
/** Index of the first paragraph in the chain. */startIndex: number; /** Index of the last keepNext or pass-through empty member. */
|
|
@@ -14,9 +15,10 @@ type KeepNextChain = {
|
|
|
14
15
|
/**
|
|
15
16
|
* Pre-scan blocks to find all keepNext chains.
|
|
16
17
|
*
|
|
17
|
-
* A
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* A chain starts with a paragraph whose keepNext=true or with an empty
|
|
19
|
+
* separator immediately following a table. It continues through further
|
|
20
|
+
* keepNext paragraphs and structural empty separators. The first visible
|
|
21
|
+
* non-keepNext paragraph is its anchor.
|
|
20
22
|
*
|
|
21
23
|
* Returns a map from chain start index to chain info.
|
|
22
24
|
*/
|