@stll/folio-core 0.28.0 → 0.28.1
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/docx/styleParser.js +15 -14
- package/dist/layout-bridge/convert/toFlowBlocks.js +74 -23
- package/dist/layout-engine/footnoteColumnReflow.d.ts +15 -0
- package/dist/layout-engine/footnoteColumnReflow.js +74 -0
- package/dist/layout-engine/index.d.ts +3 -5
- package/dist/layout-engine/index.js +36 -15
- package/dist/layout-engine/measure/cache.d.ts +1 -1
- package/dist/layout-engine/measure/cache.js +11 -2
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
- package/dist/layout-engine/measure/listMarkerWidth.js +6 -4
- package/dist/layout-engine/paginator.d.ts +6 -0
- package/dist/layout-engine/paginator.js +30 -11
- package/dist/layout-engine/types.d.ts +11 -6
- package/dist/prosemirror/attrs/index.js +3 -2
- package/dist/prosemirror/commands/formatPainter.js +45 -1
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +4 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +59 -44
- package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -1
- package/dist/prosemirror/conversion/toProseDoc.js +156 -33
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +22 -22
- package/dist/prosemirror/schema/marks.d.ts +3 -1
- package/dist/prosemirror/styles/styleResolver.d.ts +0 -1
- package/dist/prosemirror/styles/styleResolver.js +22 -29
- package/dist/prosemirror/styles/styleToggleCascade.d.ts +37 -0
- package/dist/prosemirror/styles/styleToggleCascade.js +52 -0
- package/dist/utils/textFormattingMerge.d.ts +9 -1
- package/dist/utils/textFormattingMerge.js +32 -1
- package/package.json +1 -1
package/dist/docx/styleParser.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { isValidHexColor } from "../utils/colorResolver.js";
|
|
2
2
|
import { mergeParagraphFormatting } from "../utils/paragraphFormattingMerge.js";
|
|
3
|
-
import {
|
|
3
|
+
import { mergeStyleTextFormatting } from "../utils/textFormattingMerge.js";
|
|
4
4
|
import { BorderStyleSchema, ConditionalStyleTypeSchema, EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, StyleTypeSchema, TabLeaderSchema, TabStopAlignmentSchema, TableCellTextDirectionSchema, TableRowHeightRuleSchema, TableWidthTypeSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
|
|
5
5
|
import { resolveThemeFontRef } from "./themeParser.js";
|
|
6
6
|
import { findChild, findChildren, getAttribute, getLocalName, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXmlDocument } from "./xmlParser.js";
|
|
7
7
|
//#region src/docx/styleParser.ts
|
|
8
|
+
const findLastRunToggle = (rPr, localName) => findChildren(rPr, "w", localName).at(-1) ?? null;
|
|
8
9
|
/**
|
|
9
10
|
* Parse text formatting properties (w:rPr)
|
|
10
11
|
*/
|
|
11
12
|
function parseRunProperties(rPr, theme) {
|
|
12
13
|
if (!rPr) return;
|
|
13
14
|
const formatting = {};
|
|
14
|
-
const b =
|
|
15
|
+
const b = findLastRunToggle(rPr, "b");
|
|
15
16
|
if (b) formatting.bold = parseBooleanElement(b);
|
|
16
|
-
const bCs =
|
|
17
|
+
const bCs = findLastRunToggle(rPr, "bCs");
|
|
17
18
|
if (bCs) formatting.boldCs = parseBooleanElement(bCs);
|
|
18
|
-
const i =
|
|
19
|
+
const i = findLastRunToggle(rPr, "i");
|
|
19
20
|
if (i) formatting.italic = parseBooleanElement(i);
|
|
20
|
-
const iCs =
|
|
21
|
+
const iCs = findLastRunToggle(rPr, "iCs");
|
|
21
22
|
if (iCs) formatting.italicCs = parseBooleanElement(iCs);
|
|
22
23
|
const u = findChild(rPr, "w", "u");
|
|
23
24
|
if (u) {
|
|
@@ -29,7 +30,7 @@ function parseRunProperties(rPr, theme) {
|
|
|
29
30
|
if (colorVal || themeColor) formatting.underline.color = parseColorValue(colorVal, themeColor, getAttribute(u, "w", "themeTint"), getAttribute(u, "w", "themeShade"));
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
const strike =
|
|
33
|
+
const strike = findLastRunToggle(rPr, "strike");
|
|
33
34
|
if (strike) formatting.strike = parseBooleanElement(strike);
|
|
34
35
|
const dstrike = findChild(rPr, "w", "dstrike");
|
|
35
36
|
if (dstrike) formatting.doubleStrike = parseBooleanElement(dstrike);
|
|
@@ -38,11 +39,11 @@ function parseRunProperties(rPr, theme) {
|
|
|
38
39
|
const val = getAttribute(vertAlign, "w", "val");
|
|
39
40
|
if (val === "superscript" || val === "subscript" || val === "baseline") formatting.vertAlign = val;
|
|
40
41
|
}
|
|
41
|
-
const smallCaps =
|
|
42
|
+
const smallCaps = findLastRunToggle(rPr, "smallCaps");
|
|
42
43
|
if (smallCaps) formatting.smallCaps = parseBooleanElement(smallCaps);
|
|
43
|
-
const caps =
|
|
44
|
+
const caps = findLastRunToggle(rPr, "caps");
|
|
44
45
|
if (caps) formatting.allCaps = parseBooleanElement(caps);
|
|
45
|
-
const vanish =
|
|
46
|
+
const vanish = findLastRunToggle(rPr, "vanish");
|
|
46
47
|
if (vanish) formatting.hidden = parseBooleanElement(vanish);
|
|
47
48
|
const color = findChild(rPr, "w", "color");
|
|
48
49
|
if (color) formatting.color = parseColorValue(getAttribute(color, "w", "val"), getAttribute(color, "w", "themeColor"), getAttribute(color, "w", "themeTint"), getAttribute(color, "w", "themeShade"));
|
|
@@ -154,13 +155,13 @@ function parseRunProperties(rPr, theme) {
|
|
|
154
155
|
const val = narrowEnum(getAttribute(em, "w", "val"), EmphasisMarkSchema);
|
|
155
156
|
if (val) formatting.emphasisMark = val;
|
|
156
157
|
}
|
|
157
|
-
const emboss =
|
|
158
|
+
const emboss = findLastRunToggle(rPr, "emboss");
|
|
158
159
|
if (emboss) formatting.emboss = parseBooleanElement(emboss);
|
|
159
|
-
const imprint =
|
|
160
|
+
const imprint = findLastRunToggle(rPr, "imprint");
|
|
160
161
|
if (imprint) formatting.imprint = parseBooleanElement(imprint);
|
|
161
|
-
const outline =
|
|
162
|
+
const outline = findLastRunToggle(rPr, "outline");
|
|
162
163
|
if (outline) formatting.outline = parseBooleanElement(outline);
|
|
163
|
-
const shadow =
|
|
164
|
+
const shadow = findLastRunToggle(rPr, "shadow");
|
|
164
165
|
if (shadow) formatting.shadow = parseBooleanElement(shadow);
|
|
165
166
|
const rtl = findChild(rPr, "w", "rtl");
|
|
166
167
|
if (rtl) formatting.rtl = parseBooleanElement(rtl);
|
|
@@ -807,7 +808,7 @@ function resolveStyleInheritance(style, styleMap, visited = /* @__PURE__ */ new
|
|
|
807
808
|
const resolved = { ...style };
|
|
808
809
|
const mergedPPr = mergeParagraphFormatting(resolvedParent.pPr, style.pPr);
|
|
809
810
|
if (mergedPPr) resolved.pPr = mergedPPr;
|
|
810
|
-
const mergedRPr =
|
|
811
|
+
const mergedRPr = mergeStyleTextFormatting(resolvedParent.rPr, style.rPr);
|
|
811
812
|
if (mergedRPr) resolved.rPr = mergedRPr;
|
|
812
813
|
if (style.type === "table") {
|
|
813
814
|
if (resolvedParent.tblPr || style.tblPr) resolved.tblPr = {
|
|
@@ -6,10 +6,11 @@ import { setParagraphFrame } from "../../layout-engine/paragraphFrame.js";
|
|
|
6
6
|
import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js";
|
|
7
7
|
import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
|
|
8
8
|
import { getPageNumbering } from "../../paged-layout/sectionGeometry.js";
|
|
9
|
-
import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectSymbolAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
|
|
9
|
+
import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectSymbolAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
|
|
10
10
|
import { autospacingMatchesBase } from "../../prosemirror/autospacingBase.js";
|
|
11
11
|
import { runShadingAttrsToShading } from "../../prosemirror/conversion/runShadingMark.js";
|
|
12
12
|
import { directionToBidi } from "../../prosemirror/paragraphDirection.js";
|
|
13
|
+
import { cascadeStyleTextFormatting } from "../../prosemirror/styles/styleToggleCascade.js";
|
|
13
14
|
import { assertValidProseMirrorDocument } from "../../prosemirror/validation.js";
|
|
14
15
|
import { resolveColor, resolveHighlightToCss } from "../../utils/colorResolver.js";
|
|
15
16
|
import { resolveThemeFont } from "../../utils/fontResolver.js";
|
|
@@ -348,17 +349,49 @@ function mergeRunFormatting(paraDefaults, formatting) {
|
|
|
348
349
|
if (merged.letterSpacing === 0) delete merged.letterSpacing;
|
|
349
350
|
return merged;
|
|
350
351
|
}
|
|
352
|
+
/** Restore character-style toggle values that plain visual marks cannot represent. */
|
|
353
|
+
function applyCharacterStyleToggleFormatting({ formatting, marks, paraDefaults }) {
|
|
354
|
+
const characterStyleMark = marks.find((mark) => mark.type.name === "characterStyle");
|
|
355
|
+
if (!characterStyleMark) return;
|
|
356
|
+
const styleRPr = expectCharacterStyleMarkAttrs(characterStyleMark)._styleRPr;
|
|
357
|
+
if (!styleRPr) return;
|
|
358
|
+
const effectiveStyleFormatting = cascadeStyleTextFormatting([{
|
|
359
|
+
formatting: {
|
|
360
|
+
bold: paraDefaults.bold ?? false,
|
|
361
|
+
boldCs: paraDefaults.complexScriptBold ?? false,
|
|
362
|
+
italic: paraDefaults.italic ?? false,
|
|
363
|
+
italicCs: paraDefaults.complexScriptItalic ?? false
|
|
364
|
+
},
|
|
365
|
+
type: "direct"
|
|
366
|
+
}, {
|
|
367
|
+
formatting: styleRPr,
|
|
368
|
+
type: "style"
|
|
369
|
+
}]).formatting;
|
|
370
|
+
if (styleRPr.bold !== void 0 && formatting.bold === void 0) formatting.bold = effectiveStyleFormatting?.bold ?? false;
|
|
371
|
+
if (styleRPr.boldCs !== void 0 && formatting.complexScriptBold === void 0) formatting.complexScriptBold = effectiveStyleFormatting?.boldCs ?? false;
|
|
372
|
+
if (styleRPr.italic !== void 0 && formatting.italic === void 0) formatting.italic = effectiveStyleFormatting?.italic ?? false;
|
|
373
|
+
if (styleRPr.italicCs !== void 0 && formatting.complexScriptItalic === void 0) formatting.complexScriptItalic = effectiveStyleFormatting?.italicCs ?? false;
|
|
374
|
+
if (styleRPr.allCaps === true && formatting.allCaps === void 0) formatting.allCaps = false;
|
|
375
|
+
if (styleRPr.emboss === true && formatting.emboss === void 0) formatting.emboss = false;
|
|
376
|
+
if (styleRPr.imprint === true && formatting.imprint === void 0) formatting.imprint = false;
|
|
377
|
+
if (styleRPr.outline === true && formatting.textOutline === void 0) formatting.textOutline = false;
|
|
378
|
+
if (styleRPr.shadow === true && formatting.textShadow === void 0) formatting.textShadow = false;
|
|
379
|
+
if (styleRPr.smallCaps === true && formatting.smallCaps === void 0) formatting.smallCaps = false;
|
|
380
|
+
if (styleRPr.strike === true && formatting.strike === void 0) formatting.strike = false;
|
|
381
|
+
if (styleRPr.hidden === true && formatting.hidden === void 0) formatting.hidden = false;
|
|
382
|
+
}
|
|
351
383
|
function applyRunFormattingOverrides(formatting, attrs) {
|
|
352
|
-
if (attrs.bold
|
|
353
|
-
if (attrs.italic
|
|
384
|
+
if (attrs.bold !== void 0) formatting.bold = attrs.bold;
|
|
385
|
+
if (attrs.italic !== void 0) formatting.italic = attrs.italic;
|
|
354
386
|
if (attrs.underline === "none") formatting.underline = false;
|
|
355
|
-
if (attrs.strike
|
|
356
|
-
if (attrs.allCaps
|
|
357
|
-
if (attrs.smallCaps
|
|
358
|
-
if (attrs.
|
|
359
|
-
if (attrs.
|
|
360
|
-
if (attrs.
|
|
361
|
-
if (attrs.
|
|
387
|
+
if (attrs.strike !== void 0) formatting.strike = attrs.strike;
|
|
388
|
+
if (attrs.allCaps !== void 0) formatting.allCaps = attrs.allCaps;
|
|
389
|
+
if (attrs.smallCaps !== void 0) formatting.smallCaps = attrs.smallCaps;
|
|
390
|
+
if (attrs.hidden !== void 0) formatting.hidden = attrs.hidden;
|
|
391
|
+
if (attrs.emboss !== void 0) formatting.emboss = attrs.emboss;
|
|
392
|
+
if (attrs.imprint !== void 0) formatting.imprint = attrs.imprint;
|
|
393
|
+
if (attrs.shadow !== void 0) formatting.textShadow = attrs.shadow;
|
|
394
|
+
if (attrs.outline !== void 0) formatting.textOutline = attrs.outline;
|
|
362
395
|
if (attrs.rtl === false) formatting.rtl = false;
|
|
363
396
|
if (attrs.boldCs !== void 0) formatting.complexScriptBold = attrs.boldCs;
|
|
364
397
|
if (attrs.italicCs !== void 0) formatting.complexScriptItalic = attrs.italicCs;
|
|
@@ -497,6 +530,11 @@ function paragraphToRuns(node, startPos, _options) {
|
|
|
497
530
|
if (child.type.name !== "sdt") leadingRenderedPageBreakPending = false;
|
|
498
531
|
if (child.isText && child.text) {
|
|
499
532
|
const formatting = extractRunFormatting(child.marks, theme);
|
|
533
|
+
applyCharacterStyleToggleFormatting({
|
|
534
|
+
formatting,
|
|
535
|
+
marks: child.marks,
|
|
536
|
+
paraDefaults
|
|
537
|
+
});
|
|
500
538
|
if (inTocParagraph) stripTocHyperlinkStyle(formatting);
|
|
501
539
|
const run = {
|
|
502
540
|
kind: "text",
|
|
@@ -513,6 +551,11 @@ function paragraphToRuns(node, startPos, _options) {
|
|
|
513
551
|
const text = decodeOoxmlSymbolCharacter(attrs.char);
|
|
514
552
|
if (text === null) return;
|
|
515
553
|
const formatting = extractRunFormatting(child.marks, theme);
|
|
554
|
+
applyCharacterStyleToggleFormatting({
|
|
555
|
+
formatting,
|
|
556
|
+
marks: child.marks,
|
|
557
|
+
paraDefaults
|
|
558
|
+
});
|
|
516
559
|
if (inTocParagraph) stripTocHyperlinkStyle(formatting);
|
|
517
560
|
runs.push({
|
|
518
561
|
kind: "text",
|
|
@@ -534,6 +577,11 @@ function paragraphToRuns(node, startPos, _options) {
|
|
|
534
577
|
}
|
|
535
578
|
if (child.type.name === "tab") {
|
|
536
579
|
const formatting = extractRunFormatting(child.marks, theme);
|
|
580
|
+
applyCharacterStyleToggleFormatting({
|
|
581
|
+
formatting,
|
|
582
|
+
marks: child.marks,
|
|
583
|
+
paraDefaults
|
|
584
|
+
});
|
|
537
585
|
const run = {
|
|
538
586
|
kind: "tab",
|
|
539
587
|
...mergeRunFormatting(paraDefaults, formatting),
|
|
@@ -561,6 +609,11 @@ function paragraphToRuns(node, startPos, _options) {
|
|
|
561
609
|
else if (ft === "DATE") mappedType = "DATE";
|
|
562
610
|
else if (ft === "TIME") mappedType = "TIME";
|
|
563
611
|
const extractedFieldFormatting = extractRunFormatting(child.marks, theme);
|
|
612
|
+
applyCharacterStyleToggleFormatting({
|
|
613
|
+
formatting: extractedFieldFormatting,
|
|
614
|
+
marks: child.marks,
|
|
615
|
+
paraDefaults
|
|
616
|
+
});
|
|
564
617
|
if (inTocParagraph) stripTocHyperlinkStyle(extractedFieldFormatting);
|
|
565
618
|
const fieldFormatting = markDefaultBlackTextColorSource(extractedFieldFormatting, paraDefaults);
|
|
566
619
|
const run = {
|
|
@@ -902,27 +955,25 @@ function convertParagraph(node, startPos, options) {
|
|
|
902
955
|
return block;
|
|
903
956
|
}
|
|
904
957
|
/**
|
|
905
|
-
* Word keeps
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
958
|
+
* Word keeps a final empty body paragraph after a table as an editable anchor,
|
|
959
|
+
* but that final anchor does not create a page of its own. Earlier authored
|
|
960
|
+
* empty paragraphs retain their height and may carry the document onto a blank
|
|
961
|
+
* page. Preserve every block and PM range while collapsing only the final one.
|
|
909
962
|
*/
|
|
910
963
|
function isPaintlessTerminalParagraph(block) {
|
|
911
964
|
if (block?.kind !== "paragraph" || block.runs.length !== 0) return false;
|
|
912
965
|
const attrs = block.attrs;
|
|
913
966
|
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);
|
|
914
967
|
}
|
|
915
|
-
function
|
|
968
|
+
function suppressFinalEmptyParagraphAfterTable(blocks) {
|
|
916
969
|
let suffixStart = blocks.length;
|
|
917
970
|
while (suffixStart > 0 && isPaintlessTerminalParagraph(blocks[suffixStart - 1])) suffixStart -= 1;
|
|
918
971
|
if (suffixStart === blocks.length || suffixStart === 0 || blocks[suffixStart - 1]?.kind !== "table") return;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
};
|
|
925
|
-
}
|
|
972
|
+
const finalBlock = blocks.at(-1);
|
|
973
|
+
if (isPaintlessTerminalParagraph(finalBlock)) finalBlock.attrs = {
|
|
974
|
+
...finalBlock.attrs,
|
|
975
|
+
suppressEmptyParagraphHeight: true
|
|
976
|
+
};
|
|
926
977
|
}
|
|
927
978
|
function suppressFinalParagraphInRepeatedEmptySuffix(blocks) {
|
|
928
979
|
let suffixStart = blocks.length;
|
|
@@ -1477,7 +1528,7 @@ function toFlowBlocks(doc, options = {}) {
|
|
|
1477
1528
|
visit(node, offset + nodeOffset);
|
|
1478
1529
|
});
|
|
1479
1530
|
reserveLeadingEmptyOutlineHeight(blocks);
|
|
1480
|
-
|
|
1531
|
+
suppressFinalEmptyParagraphAfterTable(blocks);
|
|
1481
1532
|
suppressFinalParagraphInRepeatedEmptySuffix(blocks);
|
|
1482
1533
|
return groupParagraphFrames(applySectionDocumentGrid(mergeRunInParagraphs(blocks), opts.finalSectionDocumentGridLinePitchTwips), nextBlockId);
|
|
1483
1534
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Layout } from "./types.js";
|
|
2
|
+
//#region src/layout-engine/footnoteColumnReflow.d.ts
|
|
3
|
+
type ReflowFootnoteColumnsOptions = {
|
|
4
|
+
initialLayout: Layout;
|
|
5
|
+
initialReserveFloors?: ReadonlyMap<number, number>;
|
|
6
|
+
runLayout: (reserveFloors: Map<number, number>) => Layout;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Re-run a multi-column layout until its body flow clears the shared footnote band.
|
|
10
|
+
* Reserve floors follow the current page-to-footnote assignment. Repeated floor states
|
|
11
|
+
* and persistent overlap use a stable fallback layout instead of throwing.
|
|
12
|
+
*/
|
|
13
|
+
declare function reflowFootnoteColumns({ initialLayout, initialReserveFloors, runLayout }: ReflowFootnoteColumnsOptions): Layout;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { reflowFootnoteColumns };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/layout-engine/footnoteColumnReflow.ts
|
|
2
|
+
function isFlowFragment(fragment) {
|
|
3
|
+
switch (fragment.kind) {
|
|
4
|
+
case "paragraph": return true;
|
|
5
|
+
case "table": return !fragment.isFloating;
|
|
6
|
+
case "image": return !fragment.isAnchored;
|
|
7
|
+
case "textBox": return !fragment.isPositioned;
|
|
8
|
+
default: return fragment;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function hasFootnoteColumnOverlap(layout) {
|
|
12
|
+
for (const page of layout.pages) {
|
|
13
|
+
if ((page.columns?.count ?? 1) < 2 || !page.footnoteReservedHeight) continue;
|
|
14
|
+
const bodyBottom = page.size.h - page.margins.bottom - page.footnoteReservedHeight;
|
|
15
|
+
if (page.fragments.some((fragment) => isFlowFragment(fragment) && fragment.y + fragment.height > bodyBottom)) return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function growFootnoteReserveFloors(layout, initialReserveFloors) {
|
|
20
|
+
const next = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const page of layout.pages) {
|
|
22
|
+
if ((page.footnoteIds?.length ?? 0) === 0) continue;
|
|
23
|
+
const observed = page.footnoteReservedHeight ?? 0;
|
|
24
|
+
const callerFloor = initialReserveFloors?.get(page.number) ?? 0;
|
|
25
|
+
const floor = Math.max(callerFloor, observed);
|
|
26
|
+
if (floor > 0) next.set(page.number, floor);
|
|
27
|
+
}
|
|
28
|
+
return next;
|
|
29
|
+
}
|
|
30
|
+
function reserveFloorFingerprint(floors) {
|
|
31
|
+
return JSON.stringify([...floors.entries()].sort(([leftPage], [rightPage]) => leftPage - rightPage));
|
|
32
|
+
}
|
|
33
|
+
function clearOrphanedFootnoteReservations(layout) {
|
|
34
|
+
let pagesChanged = false;
|
|
35
|
+
const pages = layout.pages.map((page) => {
|
|
36
|
+
if ((page.footnoteIds?.length ?? 0) > 0 || page.footnoteReservedHeight === void 0) return page;
|
|
37
|
+
pagesChanged = true;
|
|
38
|
+
const cleanedPage = { ...page };
|
|
39
|
+
delete cleanedPage.footnoteReservedHeight;
|
|
40
|
+
return cleanedPage;
|
|
41
|
+
});
|
|
42
|
+
if (!pagesChanged) return layout;
|
|
43
|
+
return {
|
|
44
|
+
...layout,
|
|
45
|
+
pages
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function getPassBudget(layout) {
|
|
49
|
+
const fragmentCount = layout.pages.reduce((count, page) => count + page.fragments.length, 0);
|
|
50
|
+
const footnoteCount = new Set(layout.pages.flatMap((page) => page.footnoteIds ?? [])).size;
|
|
51
|
+
return layout.pages.length + fragmentCount + footnoteCount + 1;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Re-run a multi-column layout until its body flow clears the shared footnote band.
|
|
55
|
+
* Reserve floors follow the current page-to-footnote assignment. Repeated floor states
|
|
56
|
+
* and persistent overlap use a stable fallback layout instead of throwing.
|
|
57
|
+
*/
|
|
58
|
+
function reflowFootnoteColumns({ initialLayout, initialReserveFloors, runLayout }) {
|
|
59
|
+
let layout = initialLayout;
|
|
60
|
+
if (!hasFootnoteColumnOverlap(layout)) return clearOrphanedFootnoteReservations(layout);
|
|
61
|
+
const seenReserveStates = /* @__PURE__ */ new Set();
|
|
62
|
+
const passBudget = getPassBudget(layout);
|
|
63
|
+
for (let pass = 0; pass < passBudget; pass += 1) {
|
|
64
|
+
const reserveFloors = growFootnoteReserveFloors(layout, initialReserveFloors);
|
|
65
|
+
const reserveState = reserveFloorFingerprint(reserveFloors);
|
|
66
|
+
if (seenReserveStates.has(reserveState)) return clearOrphanedFootnoteReservations(layout);
|
|
67
|
+
seenReserveStates.add(reserveState);
|
|
68
|
+
layout = runLayout(reserveFloors);
|
|
69
|
+
if (!hasFootnoteColumnOverlap(layout)) return clearOrphanedFootnoteReservations(layout);
|
|
70
|
+
}
|
|
71
|
+
return clearOrphanedFootnoteReservations(layout);
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { reflowFootnoteColumns };
|
|
@@ -32,11 +32,9 @@ declare function applyContextualSpacing(blocks: FlowBlock[]): void;
|
|
|
32
32
|
/**
|
|
33
33
|
* Layout a document: convert blocks + measures into pages with positioned fragments.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* 3. Use paginator to manage page/column state
|
|
39
|
-
* 4. Handle page breaks, section breaks, and keepNext chains
|
|
35
|
+
* A reference discovered after the first column has already been placed can shrink the
|
|
36
|
+
* shared body band below earlier-column fragments. Retry those documents with the observed
|
|
37
|
+
* reservations as page floors so every column participates in the same footnote geometry.
|
|
40
38
|
*/
|
|
41
39
|
declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], options: LayoutOptions): Layout;
|
|
42
40
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emuToPixels } from "../utils/units.js";
|
|
2
|
+
import { reflowFootnoteColumns } from "./footnoteColumnReflow.js";
|
|
2
3
|
import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
|
|
3
4
|
import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
|
|
4
5
|
import { measuredLineAdvance } from "./lineFlow.js";
|
|
@@ -140,13 +141,23 @@ function applyContextualSpacing(blocks) {
|
|
|
140
141
|
/**
|
|
141
142
|
* Layout a document: convert blocks + measures into pages with positioned fragments.
|
|
142
143
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* 3. Use paginator to manage page/column state
|
|
147
|
-
* 4. Handle page breaks, section breaks, and keepNext chains
|
|
144
|
+
* A reference discovered after the first column has already been placed can shrink the
|
|
145
|
+
* shared body band below earlier-column fragments. Retry those documents with the observed
|
|
146
|
+
* reservations as page floors so every column participates in the same footnote geometry.
|
|
148
147
|
*/
|
|
149
148
|
function layoutDocument(blocks, measures, options) {
|
|
149
|
+
const layout = layoutDocumentPass(blocks, measures, options);
|
|
150
|
+
if (!options.footnoteHeightById) return layout;
|
|
151
|
+
return reflowFootnoteColumns({
|
|
152
|
+
initialLayout: layout,
|
|
153
|
+
...options.footnoteReservedHeights ? { initialReserveFloors: options.footnoteReservedHeights } : {},
|
|
154
|
+
runLayout: (reserveFloors) => layoutDocumentPass(blocks, measures, {
|
|
155
|
+
...options,
|
|
156
|
+
footnoteReservedHeights: reserveFloors
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function layoutDocumentPass(blocks, measures, options) {
|
|
150
161
|
if (blocks.length !== measures.length) panic(`layoutDocument: expected one measure per block (blocks=${blocks.length}, measures=${measures.length})`);
|
|
151
162
|
const pageSize = options.pageSize;
|
|
152
163
|
const baseMargins = {
|
|
@@ -333,6 +344,10 @@ function getLineFootnoteRefs(block, fromRun, toRun, fnHeights) {
|
|
|
333
344
|
height
|
|
334
345
|
};
|
|
335
346
|
}
|
|
347
|
+
function projectedFootnoteReserveGrowth(state, additionalDemandHeight) {
|
|
348
|
+
const projectedDemand = state.footnoteDemandHeight + additionalDemandHeight;
|
|
349
|
+
return Math.max(state.footnoteHeightFloor, projectedDemand) - state.footnoteHeight;
|
|
350
|
+
}
|
|
336
351
|
function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeightById, suppressSpaceBefore }) {
|
|
337
352
|
const lines = measure.lines;
|
|
338
353
|
if (lines.length === 0) {
|
|
@@ -370,7 +385,7 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
|
|
|
370
385
|
if (currentLineIndex === 0 && state.trailingSpacing > 0 && state.cursorY !== state.topMargin) {
|
|
371
386
|
const firstLine = lines[0];
|
|
372
387
|
const firstLineRefs = getLineFootnoteRefs(block, firstLine.fromRun, firstLine.toRun, footnoteHeightById);
|
|
373
|
-
const firstLineHeight = measuredLineAdvance(firstLine) + firstLineRefs.height;
|
|
388
|
+
const firstLineHeight = measuredLineAdvance(firstLine) + projectedFootnoteReserveGrowth(state, firstLineRefs.height);
|
|
374
389
|
const collapsedLead = collapseParagraphSpacing({
|
|
375
390
|
before: spaceBefore,
|
|
376
391
|
after: state.trailingSpacing
|
|
@@ -399,7 +414,8 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
|
|
|
399
414
|
}
|
|
400
415
|
const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
|
|
401
416
|
const totalWithLine = linesHeight + lineAdvance;
|
|
402
|
-
|
|
417
|
+
const footnoteGrowth = projectedFootnoteReserveGrowth(state, linesFnHeight + lineRefs.height);
|
|
418
|
+
if (totalWithLine + firstFragmentSpaceBefore + footnoteGrowth <= availableHeight || fittingLines === 0) {
|
|
403
419
|
linesHeight = totalWithLine;
|
|
404
420
|
linesFnHeight += lineRefs.height;
|
|
405
421
|
for (const id of lineRefs.ids) linesFnIds.push(id);
|
|
@@ -449,8 +465,10 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
|
|
|
449
465
|
...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
|
|
450
466
|
};
|
|
451
467
|
if (linesFnHeight > 0) {
|
|
452
|
-
paginator.
|
|
453
|
-
|
|
468
|
+
const stateBefore = paginator.getCurrentState();
|
|
469
|
+
paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateBefore, linesFnHeight));
|
|
470
|
+
const stateAfter = paginator.getCurrentState();
|
|
471
|
+
if (stateAfter.footnoteDemandHeight === 0) paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateAfter, linesFnHeight + 12));
|
|
454
472
|
}
|
|
455
473
|
fragment.y = paginator.addFragment(fragment, linesHeight, effectiveSpaceBefore, effectiveSpaceAfter).y;
|
|
456
474
|
if (linesFnHeight > 0) paginator.addFootnoteHeight(linesFnHeight, linesFnIds);
|
|
@@ -650,8 +668,9 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
|
|
|
650
668
|
fragmentFootnoteIds.push(id);
|
|
651
669
|
rowFootnoteHeight += footnoteHeightById?.get(id) ?? 0;
|
|
652
670
|
}
|
|
653
|
-
const
|
|
654
|
-
|
|
671
|
+
const candidateFootnoteHeight = fragmentFootnoteHeight + rowFootnoteHeight;
|
|
672
|
+
const footnoteGrowth = projectedFootnoteReserveGrowth(state, candidateFootnoteHeight + (state.footnoteDemandHeight === 0 && candidateFootnoteHeight > 0 ? 12 : 0));
|
|
673
|
+
if (rowsHeight + rowHeight + normalHeaderOverhead + footnoteGrowth <= availableHeight) {
|
|
655
674
|
rowsHeight += rowHeight;
|
|
656
675
|
fragmentFootnoteHeight += rowFootnoteHeight;
|
|
657
676
|
fittingRows++;
|
|
@@ -780,7 +799,7 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
|
|
|
780
799
|
isFloating: true,
|
|
781
800
|
...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
|
|
782
801
|
};
|
|
783
|
-
|
|
802
|
+
paginator.addUnflowedFragment(fragment);
|
|
784
803
|
}
|
|
785
804
|
/**
|
|
786
805
|
* Layout an image block onto pages.
|
|
@@ -825,7 +844,7 @@ function layoutAnchoredImage(block, measure, paginator) {
|
|
|
825
844
|
isAnchored: true,
|
|
826
845
|
zIndex: anchor.behindDoc ? -1 : 1
|
|
827
846
|
};
|
|
828
|
-
|
|
847
|
+
paginator.addUnflowedFragment(fragment);
|
|
829
848
|
}
|
|
830
849
|
/**
|
|
831
850
|
* Layout a text box block onto pages.
|
|
@@ -853,10 +872,11 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
|
|
|
853
872
|
y: state.topMargin + bandTop,
|
|
854
873
|
width: measure.width,
|
|
855
874
|
height: measure.height,
|
|
875
|
+
isPositioned: true,
|
|
856
876
|
...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
|
|
857
877
|
...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
|
|
858
878
|
};
|
|
859
|
-
|
|
879
|
+
paginator.addUnflowedFragment(fragment);
|
|
860
880
|
return;
|
|
861
881
|
}
|
|
862
882
|
if (block.position !== void 0) {
|
|
@@ -882,10 +902,11 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
|
|
|
882
902
|
y,
|
|
883
903
|
width: measure.width,
|
|
884
904
|
height: measure.height,
|
|
905
|
+
isPositioned: true,
|
|
885
906
|
...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
|
|
886
907
|
...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
|
|
887
908
|
};
|
|
888
|
-
|
|
909
|
+
paginator.addUnflowedFragment(fragment);
|
|
889
910
|
return;
|
|
890
911
|
}
|
|
891
912
|
const state = paginator.ensureFits(measure.height);
|
|
@@ -54,7 +54,7 @@ declare function setFontCacheSize(size: number): void;
|
|
|
54
54
|
* Get current font metrics cache size
|
|
55
55
|
*/
|
|
56
56
|
declare function getFontCacheSize(): number;
|
|
57
|
-
/** Serialize the complete paragraph measurement contract into a cache key. */
|
|
57
|
+
/** Serialize the complete, semantically normalized paragraph measurement contract into a cache key. */
|
|
58
58
|
declare function hashParagraphBlock(block: ParagraphBlock): string;
|
|
59
59
|
/**
|
|
60
60
|
* Get cached paragraph measurement or return undefined
|
|
@@ -188,11 +188,20 @@ const runMeasureCacheInput = (run) => {
|
|
|
188
188
|
default: return run;
|
|
189
189
|
}
|
|
190
190
|
};
|
|
191
|
-
|
|
191
|
+
const paragraphAttrsMeasureCacheInput = (attrs) => {
|
|
192
|
+
if (attrs === void 0) return void 0;
|
|
193
|
+
const { snapToGrid, ...measurementAttrs } = attrs;
|
|
194
|
+
if (measurementAttrs.documentGridLinePitch === void 0) return measurementAttrs;
|
|
195
|
+
return {
|
|
196
|
+
...measurementAttrs,
|
|
197
|
+
snapToGrid: snapToGrid !== false
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
/** Serialize the complete, semantically normalized paragraph measurement contract into a cache key. */
|
|
192
201
|
function hashParagraphBlock(block) {
|
|
193
202
|
return JSON.stringify({
|
|
194
203
|
lineBreakProviderGeneration: getLineBreakProviderGeneration(),
|
|
195
|
-
attrs: block.attrs,
|
|
204
|
+
attrs: paragraphAttrsMeasureCacheInput(block.attrs),
|
|
196
205
|
runs: block.runs.map(runMeasureCacheInput)
|
|
197
206
|
});
|
|
198
207
|
}
|
|
@@ -27,7 +27,9 @@ declare function resolveListMarkerFont(block: ParagraphBlock): {
|
|
|
27
27
|
* - hanging indent — body wraps at `indentLeft`, marker sits at
|
|
28
28
|
* `indentLeft - hanging`. Width is `hanging` so the marker fills the slot.
|
|
29
29
|
* - `w:suff` (§17.9.25): `nothing` → natural width, `space` → natural +
|
|
30
|
-
* one space glyph, `tab` (default) → grow to the next tab stop.
|
|
30
|
+
* one space glyph, `tab` (default) → grow to the next tab stop. A hanging
|
|
31
|
+
* indent remains the minimum footprint so body text cannot enter the
|
|
32
|
+
* marker slot.
|
|
31
33
|
* - `w:tabs` on the paragraph: non-`clear`/non-`bar` stops past the marker.
|
|
32
34
|
* `bar` (§17.3.1.37) is a vertical line and doesn't advance the cursor.
|
|
33
35
|
* - default tab grid: stops at multiples of `DEFAULT_TAB_STOP_TWIPS`,
|
|
@@ -50,7 +50,9 @@ function resolveListMarkerFont(block) {
|
|
|
50
50
|
* - hanging indent — body wraps at `indentLeft`, marker sits at
|
|
51
51
|
* `indentLeft - hanging`. Width is `hanging` so the marker fills the slot.
|
|
52
52
|
* - `w:suff` (§17.9.25): `nothing` → natural width, `space` → natural +
|
|
53
|
-
* one space glyph, `tab` (default) → grow to the next tab stop.
|
|
53
|
+
* one space glyph, `tab` (default) → grow to the next tab stop. A hanging
|
|
54
|
+
* indent remains the minimum footprint so body text cannot enter the
|
|
55
|
+
* marker slot.
|
|
54
56
|
* - `w:tabs` on the paragraph: non-`clear`/non-`bar` stops past the marker.
|
|
55
57
|
* `bar` (§17.3.1.37) is a vertical line and doesn't advance the cursor.
|
|
56
58
|
* - default tab grid: stops at multiples of `DEFAULT_TAB_STOP_TWIPS`,
|
|
@@ -67,12 +69,12 @@ function getListMarkerInlineWidth(block) {
|
|
|
67
69
|
const naturalWidth = measureTextWidth(attrs.listMarker, style);
|
|
68
70
|
const markerEndOffset = getMarkerEndOffset(naturalWidth, attrs.listMarkerAlignment);
|
|
69
71
|
const suffix = attrs.listMarkerSuffix ?? "tab";
|
|
70
|
-
|
|
71
|
-
if (suffix === "
|
|
72
|
+
const hanging = attrs.indent?.hanging ?? 0;
|
|
73
|
+
if (suffix === "nothing") return Math.max(hanging, markerEndOffset);
|
|
74
|
+
if (suffix === "space") return Math.max(hanging, markerEndOffset + measureTextWidth(" ", style));
|
|
72
75
|
const indent = attrs.indent;
|
|
73
76
|
const indentLeft = indent?.left ?? 0;
|
|
74
77
|
const firstLine = indent?.firstLine ?? 0;
|
|
75
|
-
const hanging = indent?.hanging ?? 0;
|
|
76
78
|
const markerStartPx = hanging > 0 ? indentLeft - hanging : indentLeft + firstLine;
|
|
77
79
|
const minBodyStart = markerStartPx + markerEndOffset;
|
|
78
80
|
const customTabs = (attrs.tabs ?? []).filter((t) => t.val !== "clear" && t.val !== "bar").map((t) => t.pos * TWIPS_TO_PX);
|
|
@@ -22,6 +22,10 @@ type PageState = {
|
|
|
22
22
|
rawContentBottom: number;
|
|
23
23
|
/** Total height reserved for footnotes on this page (grows as refs are placed). */
|
|
24
24
|
footnoteHeight: number;
|
|
25
|
+
/** Static reservation supplied by a layout retry. */
|
|
26
|
+
footnoteHeightFloor: number;
|
|
27
|
+
/** Footnote demand discovered while placing reference-bearing content. */
|
|
28
|
+
footnoteDemandHeight: number;
|
|
25
29
|
/** Accumulated trailing spacing (space after previous block). */
|
|
26
30
|
trailingSpacing: number;
|
|
27
31
|
};
|
|
@@ -98,6 +102,8 @@ declare function createPaginator(options: PaginatorOptions): {
|
|
|
98
102
|
x: number;
|
|
99
103
|
y: number;
|
|
100
104
|
};
|
|
105
|
+
/** Add an already-positioned fragment without advancing normal flow. */
|
|
106
|
+
addUnflowedFragment: (fragment: Fragment) => PageState;
|
|
101
107
|
/** Reserve additional footnote area on the current page. */
|
|
102
108
|
addFootnoteHeight: (additionalHeight: number, footnoteIds?: number[]) => void;
|
|
103
109
|
/** Force a page break. */
|