@stll/folio-core 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-edits/headless.d.ts +37 -1
- package/dist/ai-edits/headless.js +47 -16
- package/dist/ai-edits/word-diff.js +134 -1
- package/dist/compare/content-alignment.js +220 -5
- package/dist/compare/content.d.ts +1 -0
- package/dist/compare/content.js +51 -20
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
- package/dist/display-list/dom/renderDisplayListToDom.js +8 -7
- package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
- package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
- package/dist/display-list/selectDisplayPages.d.ts +6 -0
- package/dist/display-list/selectDisplayPages.js +44 -0
- package/dist/docx/selectiveSave.d.ts +1 -1
- package/dist/docx/selectiveSave.js +16 -6
- package/dist/docx/structuralXmlPatch.d.ts +14 -0
- package/dist/docx/structuralXmlPatch.js +209 -0
- package/dist/export-pdf.d.ts +35 -2
- package/dist/export-pdf.js +51 -20
- package/dist/fonts/fontsourceFaces.d.ts +47 -0
- package/dist/fonts/fontsourceFaces.js +230 -0
- package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
- package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
- package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
- package/dist/layout-bridge/convert/flowBorders.js +31 -0
- package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
- package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
- package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
- package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
- package/dist/layout-bridge/convert/imageConversion.js +118 -0
- package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
- package/dist/layout-bridge/convert/listMarkers.js +98 -0
- package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
- package/dist/layout-bridge/convert/noteReferences.js +29 -0
- package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
- package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
- package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
- package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
- package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
- package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
- package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
- package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
- package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
- package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
- package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
- package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
- package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
- package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
- package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
- package/dist/layout-bridge/convert/tableConversion.js +303 -0
- package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
- package/dist/layout-bridge/convert/textBoxFill.js +42 -0
- package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
- package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
- package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2082
- package/dist/layout-bridge/engine/hitTest.js +1 -1
- package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
- package/dist/layout-bridge/engine/measuring/index.js +2 -1
- package/dist/layout-bridge/engine/selectionRects.js +1 -1
- package/dist/layout-engine/imageLayout.d.ts +9 -0
- package/dist/layout-engine/imageLayout.js +48 -0
- package/dist/layout-engine/index.d.ts +3 -18
- package/dist/layout-engine/index.js +12 -937
- package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
- package/dist/layout-engine/layoutFlowShared.js +8 -0
- package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
- package/dist/layout-engine/measure/crossRunWords.js +172 -0
- package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
- package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
- package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
- package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
- package/dist/layout-engine/measure/justification.d.ts +32 -0
- package/dist/layout-engine/measure/justification.js +83 -0
- package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
- package/dist/layout-engine/measure/lineFitting.js +145 -0
- package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
- package/dist/layout-engine/measure/lineTypography.js +124 -0
- package/dist/layout-engine/measure/measureBlocks.js +2 -1
- package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
- package/dist/layout-engine/measure/measureParagraph.js +32 -734
- package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
- package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
- package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
- package/dist/layout-engine/measure/paragraphTabs.js +78 -0
- package/dist/layout-engine/paragraphLayout.d.ts +23 -0
- package/dist/layout-engine/paragraphLayout.js +204 -0
- package/dist/layout-engine/sectionLayout.d.ts +45 -0
- package/dist/layout-engine/sectionLayout.js +216 -0
- package/dist/layout-engine/tableLayout.d.ts +17 -0
- package/dist/layout-engine/tableLayout.js +369 -0
- package/dist/layout-engine/textBoxLayout.d.ts +15 -0
- package/dist/layout-engine/textBoxLayout.js +137 -0
- package/dist/layout-painter/renderPage.js +1 -0
- package/dist/managers/editorShortcuts.d.ts +16 -3
- package/dist/managers/editorShortcuts.js +6 -4
- package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
- package/dist/paged-layout/sectionBlockWidths.js +1 -1
- package/dist/prosemirror/extensions/StarterKit.d.ts +3 -0
- package/dist/prosemirror/extensions/StarterKit.js +2 -1
- package/dist/prosemirror/extensions/core/HistoryExtension.d.ts +9 -1
- package/dist/prosemirror/extensions/core/HistoryExtension.js +4 -3
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +12 -5
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +91 -33
- package/dist/server.d.ts +3 -2
- package/package.json +2 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { FieldRun, ImageRun, LineBreakRun, MathRun, MeasuredLine, Run, TabRun, TextRun } from "../types.js";
|
|
2
|
+
import { FontMetrics, FontStyle } from "./measureTypes.js";
|
|
3
|
+
import { FloatingLineSegmentZone } from "./floatingZones.js";
|
|
4
|
+
//#region src/layout-engine/measure/paragraphMeasureShared.d.ts
|
|
5
|
+
declare const DEFAULT_FONT_SIZE = 11;
|
|
6
|
+
declare const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
|
|
7
|
+
declare const WIDTH_TOLERANCE = 0.5;
|
|
8
|
+
/**
|
|
9
|
+
* Typography metrics for a line
|
|
10
|
+
*/
|
|
11
|
+
type LineTypography = {
|
|
12
|
+
ascent: number;
|
|
13
|
+
descent: number;
|
|
14
|
+
lineHeight: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* State tracking for line accumulation
|
|
18
|
+
*/
|
|
19
|
+
type LineState = {
|
|
20
|
+
fromRun: number;
|
|
21
|
+
fromChar: number;
|
|
22
|
+
toRun: number;
|
|
23
|
+
toChar: number;
|
|
24
|
+
width: number;
|
|
25
|
+
/** Width of collapsible ASCII whitespace at the current line tail. */
|
|
26
|
+
trailingWhitespaceWidth: number;
|
|
27
|
+
/** Spaces the layout may compress while justifying this line. */
|
|
28
|
+
regularSpaceWidth: number;
|
|
29
|
+
/** Paint plan set only when final-text contraction admits a candidate. */
|
|
30
|
+
justificationPaint?: NonNullable<MeasuredLine["justificationPaint"]>;
|
|
31
|
+
maxFontSize: number;
|
|
32
|
+
maxFontMetrics: FontMetrics | null;
|
|
33
|
+
/**
|
|
34
|
+
* Largest tab font on the line. Tabs are whitespace: their font sizes the
|
|
35
|
+
* line only when no other run on it does (a line of nothing but tabs).
|
|
36
|
+
*/
|
|
37
|
+
maxTabFontSize: number;
|
|
38
|
+
maxTabFontMetrics: FontMetrics | null;
|
|
39
|
+
/** Maximum inline image height in pixels (already in px, not points) */
|
|
40
|
+
maxImageHeightPx: number;
|
|
41
|
+
/** Maximum exact-height embedded-object preview on the line. */
|
|
42
|
+
maxExactImageHeightPx: number;
|
|
43
|
+
/** Maximum inline math height in pixels (already in px, not points) */
|
|
44
|
+
maxMathHeightPx: number;
|
|
45
|
+
availableWidth: number;
|
|
46
|
+
/** Left offset from floating images (pixels from content left edge) */
|
|
47
|
+
leftOffset: number;
|
|
48
|
+
/** Right offset from floating images (pixels from content right edge) */
|
|
49
|
+
rightOffset: number;
|
|
50
|
+
/** Optional split segment zones from centered floating exclusions */
|
|
51
|
+
segmentZones?: FloatingLineSegmentZone[];
|
|
52
|
+
discretionaryHyphen?: {
|
|
53
|
+
runIndex: number;
|
|
54
|
+
};
|
|
55
|
+
renderedPageBreakBefore?: boolean;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Extract FontStyle from a run that carries RunFormatting (text, tab, or
|
|
59
|
+
* field). All three share the same formatting shape, so they measure the
|
|
60
|
+
* same way; widening the parameter keeps tab-following measurement
|
|
61
|
+
* (FieldRun page numbers, etc.) consistent with TextRun handling.
|
|
62
|
+
*/
|
|
63
|
+
declare function runToFontStyle(run: TextRun | TabRun | FieldRun | MathRun): FontStyle;
|
|
64
|
+
/**
|
|
65
|
+
* Check if a run is a text run
|
|
66
|
+
*/
|
|
67
|
+
declare function isTextRun(run: Run): run is TextRun;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a run is a tab run
|
|
70
|
+
*/
|
|
71
|
+
declare function isTabRun(run: Run): run is TabRun;
|
|
72
|
+
/**
|
|
73
|
+
* Check if a run is an image run
|
|
74
|
+
*/
|
|
75
|
+
declare function isImageRun(run: Run): run is ImageRun;
|
|
76
|
+
/**
|
|
77
|
+
* Check if a run is a line break run
|
|
78
|
+
*/
|
|
79
|
+
declare function isLineBreakRun(run: Run): run is LineBreakRun;
|
|
80
|
+
/**
|
|
81
|
+
* Check if a run is a field run
|
|
82
|
+
*/
|
|
83
|
+
declare function isFieldRun(run: Run): run is FieldRun;
|
|
84
|
+
/** Text a field run is measured at: its resolved value when known (so the
|
|
85
|
+
* measurer agrees with the painter), else the cached fallback. */
|
|
86
|
+
declare function fieldMeasureText(run: FieldRun, fieldValues: ReadonlyMap<number, string> | undefined): string;
|
|
87
|
+
/**
|
|
88
|
+
* Check if a run is a math equation run
|
|
89
|
+
*/
|
|
90
|
+
declare function isMathRun(run: Run): run is MathRun;
|
|
91
|
+
/**
|
|
92
|
+
* Check if text run is empty (only whitespace or no text)
|
|
93
|
+
*/
|
|
94
|
+
declare function isEmptyTextRun(run: TextRun): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Sum the inline pixel widths of runs after a tab, up to (but not including)
|
|
97
|
+
* the next tab or line break. Measured per-run so widths reserved match what
|
|
98
|
+
* the painter draws even when trailing runs use different fonts/sizes.
|
|
99
|
+
*
|
|
100
|
+
* Floating/anchored images are skipped — the painter lifts them out of the
|
|
101
|
+
* paragraph flow (see `isFloatingImageRun`) and `measureFollowingContentWidth`
|
|
102
|
+
* in the painter already excludes them, so counting their width here would
|
|
103
|
+
* desync measurer and painter on tab advance for paragraphs with a tab
|
|
104
|
+
* preceding a floating image.
|
|
105
|
+
*/
|
|
106
|
+
declare function isBlockLayoutImageRun(run: ImageRun): boolean;
|
|
107
|
+
declare function isSpaceOrTab(char: string | undefined): boolean;
|
|
108
|
+
//#endregion
|
|
109
|
+
export { DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT_MULTIPLIER, LineState, LineTypography, WIDTH_TOLERANCE, fieldMeasureText, isBlockLayoutImageRun, isEmptyTextRun, isFieldRun, isImageRun, isLineBreakRun, isMathRun, isSpaceOrTab, isTabRun, isTextRun, runToFontStyle };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isFloatingImageRun } from "../types.js";
|
|
2
|
+
import { DEFAULT_FONT_FAMILY, buildRunFontStyle } from "./measureHelpers.js";
|
|
3
|
+
//#region src/layout-engine/measure/paragraphMeasureShared.ts
|
|
4
|
+
const DEFAULT_FONT_SIZE = 11;
|
|
5
|
+
const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
|
|
6
|
+
const WIDTH_TOLERANCE = .5;
|
|
7
|
+
/**
|
|
8
|
+
* Extract FontStyle from a run that carries RunFormatting (text, tab, or
|
|
9
|
+
* field). All three share the same formatting shape, so they measure the
|
|
10
|
+
* same way; widening the parameter keeps tab-following measurement
|
|
11
|
+
* (FieldRun page numbers, etc.) consistent with TextRun handling.
|
|
12
|
+
*/
|
|
13
|
+
function runToFontStyle(run) {
|
|
14
|
+
return buildRunFontStyle(run, DEFAULT_FONT_FAMILY, 11);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if a run is a text run
|
|
18
|
+
*/
|
|
19
|
+
function isTextRun(run) {
|
|
20
|
+
return run.kind === "text";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if a run is a tab run
|
|
24
|
+
*/
|
|
25
|
+
function isTabRun(run) {
|
|
26
|
+
return run.kind === "tab";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a run is an image run
|
|
30
|
+
*/
|
|
31
|
+
function isImageRun(run) {
|
|
32
|
+
return run.kind === "image";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if a run is a line break run
|
|
36
|
+
*/
|
|
37
|
+
function isLineBreakRun(run) {
|
|
38
|
+
return run.kind === "lineBreak";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if a run is a field run
|
|
42
|
+
*/
|
|
43
|
+
function isFieldRun(run) {
|
|
44
|
+
return run.kind === "field";
|
|
45
|
+
}
|
|
46
|
+
/** Text a field run is measured at: its resolved value when known (so the
|
|
47
|
+
* measurer agrees with the painter), else the cached fallback. */
|
|
48
|
+
function fieldMeasureText(run, fieldValues) {
|
|
49
|
+
return (run.pmStart === void 0 ? void 0 : fieldValues?.get(run.pmStart)) ?? (run.fallback || "1");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if a run is a math equation run
|
|
53
|
+
*/
|
|
54
|
+
function isMathRun(run) {
|
|
55
|
+
return run.kind === "math";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if text run is empty (only whitespace or no text)
|
|
59
|
+
*/
|
|
60
|
+
function isEmptyTextRun(run) {
|
|
61
|
+
return !run.text || run.text.replace(/\u00a0/gu, " ").trim().length === 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sum the inline pixel widths of runs after a tab, up to (but not including)
|
|
65
|
+
* the next tab or line break. Measured per-run so widths reserved match what
|
|
66
|
+
* the painter draws even when trailing runs use different fonts/sizes.
|
|
67
|
+
*
|
|
68
|
+
* Floating/anchored images are skipped — the painter lifts them out of the
|
|
69
|
+
* paragraph flow (see `isFloatingImageRun`) and `measureFollowingContentWidth`
|
|
70
|
+
* in the painter already excludes them, so counting their width here would
|
|
71
|
+
* desync measurer and painter on tab advance for paragraphs with a tab
|
|
72
|
+
* preceding a floating image.
|
|
73
|
+
*/
|
|
74
|
+
function isBlockLayoutImageRun(run) {
|
|
75
|
+
if (isFloatingImageRun(run)) return false;
|
|
76
|
+
return run.wrapType === "topAndBottom" || run.displayMode === "block";
|
|
77
|
+
}
|
|
78
|
+
function isSpaceOrTab(char) {
|
|
79
|
+
return char === " " || char === " ";
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT_MULTIPLIER, WIDTH_TOLERANCE, fieldMeasureText, isBlockLayoutImageRun, isEmptyTextRun, isFieldRun, isImageRun, isLineBreakRun, isMathRun, isSpaceOrTab, isTabRun, isTextRun, runToFontStyle };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Run } from "../types.js";
|
|
2
|
+
//#region src/layout-engine/measure/paragraphTabs.d.ts
|
|
3
|
+
declare function measureInlineWidthAfterTab(runs: Run[], tabIndex: number, fieldValues?: ReadonlyMap<number, string>): number;
|
|
4
|
+
declare function hasFollowingTabOnLine(runs: Run[], tabIndex: number): boolean;
|
|
5
|
+
declare function hasPriorTabOnLine(runs: Run[], tabIndex: number): boolean;
|
|
6
|
+
declare function canClampTabToRightEdge(alignment: string, currentLineWidth: number, hasPriorTab: boolean, followingWidth: number, availableWidth: number): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Width of the inline content preceding the first `.` in the runs that follow
|
|
9
|
+
* a tab, used to anchor `decimal` tab stops. Mirrors `getTextAfterTab` +
|
|
10
|
+
* decimal-prefix measurement in the painter (`renderParagraph.ts`) so the
|
|
11
|
+
* measurer and painter agree on tab advance for decimal stops.
|
|
12
|
+
*
|
|
13
|
+
* Returns 0 when no decimal separator appears before the next tab / line
|
|
14
|
+
* break — `calculateTabWidth` treats that as "no anchor adjustment".
|
|
15
|
+
*/
|
|
16
|
+
declare function measureDecimalPrefixWidthAfterTab(runs: Run[], tabIndex: number, fieldValues?: ReadonlyMap<number, string>): number;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { canClampTabToRightEdge, hasFollowingTabOnLine, hasPriorTabOnLine, measureDecimalPrefixWidthAfterTab, measureInlineWidthAfterTab };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
|
|
2
|
+
import { isFloatingImageRun } from "../types.js";
|
|
3
|
+
import { measureTextWidth } from "./measureProvider.js";
|
|
4
|
+
import { fieldMeasureText, isBlockLayoutImageRun, isFieldRun, isImageRun, isLineBreakRun, isMathRun, isTabRun, isTextRun, runToFontStyle } from "./paragraphMeasureShared.js";
|
|
5
|
+
//#region src/layout-engine/measure/paragraphTabs.ts
|
|
6
|
+
/**
|
|
7
|
+
* Tab measurement on a line: the width of content following a tab, decimal
|
|
8
|
+
* tab prefixes, neighbouring tabs, and right-edge clamping.
|
|
9
|
+
*/
|
|
10
|
+
function measureInlineWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
11
|
+
let width = 0;
|
|
12
|
+
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
13
|
+
const next = runs[i];
|
|
14
|
+
if (!next || isTabRun(next) || isLineBreakRun(next)) break;
|
|
15
|
+
if (isImageRun(next)) {
|
|
16
|
+
if (isBlockLayoutImageRun(next)) break;
|
|
17
|
+
if (!isFloatingImageRun(next)) width += inlineImageBoundingBox(next).width || 0;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (isTextRun(next)) width += measureTextWidth(next.text || "", runToFontStyle(next));
|
|
21
|
+
else if (isFieldRun(next)) width += measureTextWidth(fieldMeasureText(next, fieldValues), runToFontStyle(next));
|
|
22
|
+
else if (isMathRun(next)) width += measureTextWidth(next.plainText || "[equation]", runToFontStyle(next));
|
|
23
|
+
}
|
|
24
|
+
return width;
|
|
25
|
+
}
|
|
26
|
+
function hasFollowingTabOnLine(runs, tabIndex) {
|
|
27
|
+
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
28
|
+
const next = runs[i];
|
|
29
|
+
if (!next || isLineBreakRun(next)) break;
|
|
30
|
+
if (isImageRun(next) && isBlockLayoutImageRun(next)) break;
|
|
31
|
+
if (isTabRun(next)) return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function hasPriorTabOnLine(runs, tabIndex) {
|
|
36
|
+
for (let i = tabIndex - 1; i >= 0; i--) {
|
|
37
|
+
const prior = runs[i];
|
|
38
|
+
if (!prior || isLineBreakRun(prior) || isImageRun(prior) && isBlockLayoutImageRun(prior)) break;
|
|
39
|
+
if (isTabRun(prior)) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function canClampTabToRightEdge(alignment, currentLineWidth, hasPriorTab, followingWidth, availableWidth) {
|
|
44
|
+
if (alignment === "start" || alignment === "default") return currentLineWidth > .5 && (hasPriorTab || followingWidth <= availableWidth);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Width of the inline content preceding the first `.` in the runs that follow
|
|
49
|
+
* a tab, used to anchor `decimal` tab stops. Mirrors `getTextAfterTab` +
|
|
50
|
+
* decimal-prefix measurement in the painter (`renderParagraph.ts`) so the
|
|
51
|
+
* measurer and painter agree on tab advance for decimal stops.
|
|
52
|
+
*
|
|
53
|
+
* Returns 0 when no decimal separator appears before the next tab / line
|
|
54
|
+
* break — `calculateTabWidth` treats that as "no anchor adjustment".
|
|
55
|
+
*/
|
|
56
|
+
function measureDecimalPrefixWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
57
|
+
let text = "";
|
|
58
|
+
let firstRun;
|
|
59
|
+
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
60
|
+
const next = runs[i];
|
|
61
|
+
if (!next || isTabRun(next) || isLineBreakRun(next)) break;
|
|
62
|
+
if (isTextRun(next)) {
|
|
63
|
+
text += next.text || "";
|
|
64
|
+
firstRun ??= next;
|
|
65
|
+
} else if (isFieldRun(next)) {
|
|
66
|
+
text += fieldMeasureText(next, fieldValues);
|
|
67
|
+
firstRun ??= next;
|
|
68
|
+
} else if (isMathRun(next)) {
|
|
69
|
+
text += next.plainText || "[equation]";
|
|
70
|
+
firstRun ??= next;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const decimalIndex = text.indexOf(".");
|
|
74
|
+
if (decimalIndex === -1 || !firstRun) return 0;
|
|
75
|
+
return measureTextWidth(text.slice(0, decimalIndex), runToFontStyle(firstRun));
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { canClampTabToRightEdge, hasFollowingTabOnLine, hasPriorTabOnLine, measureDecimalPrefixWidthAfterTab, measureInlineWidthAfterTab };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ParagraphBlock, ParagraphMeasure } from "./types.js";
|
|
2
|
+
import { createPaginator } from "./paginator.js";
|
|
3
|
+
//#region src/layout-engine/paragraphLayout.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Layout a paragraph block onto pages.
|
|
6
|
+
*
|
|
7
|
+
* When `footnoteHeightById` is provided, each line carrying a footnote
|
|
8
|
+
* ref additionally reserves space on its host page for the fn's
|
|
9
|
+
* content. Pagination decisions include only the reservation growth
|
|
10
|
+
* beyond any retry floor, so a line cannot land on a page that lacks
|
|
11
|
+
* room for both body and note content.
|
|
12
|
+
*/
|
|
13
|
+
type LayoutParagraphOptions = {
|
|
14
|
+
block: ParagraphBlock;
|
|
15
|
+
measure: ParagraphMeasure;
|
|
16
|
+
paginator: ReturnType<typeof createPaginator>;
|
|
17
|
+
contentWidth: number;
|
|
18
|
+
footnoteHeightById: Map<number, number> | undefined;
|
|
19
|
+
suppressSpaceBefore: boolean;
|
|
20
|
+
};
|
|
21
|
+
declare function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeightById, suppressSpaceBefore }: LayoutParagraphOptions): void;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { layoutParagraph };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { hasKeepLines } from "./keep-together.js";
|
|
2
|
+
import { projectedFootnoteReserveGrowth } from "./layoutFlowShared.js";
|
|
3
|
+
import { measuredLineAdvance, measuredLineRangeHeight } from "./lineFlow.js";
|
|
4
|
+
import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
|
|
5
|
+
import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore } from "./paragraphSpacing.js";
|
|
6
|
+
import "./types.js";
|
|
7
|
+
//#region src/layout-engine/paragraphLayout.ts
|
|
8
|
+
/**
|
|
9
|
+
* Places a measured paragraph's lines onto pages: keep rules, widow/orphan
|
|
10
|
+
* control, rendered-break reconciliation, and per-line footnote reservation.
|
|
11
|
+
*/
|
|
12
|
+
function hasWidowControl(block) {
|
|
13
|
+
return block.attrs?.widowControl !== false;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Footnote refs whose run sits in line `[fromRun..toRun]`, with their
|
|
17
|
+
* pre-measured content heights. Empty when the engine isn't tracking
|
|
18
|
+
* dynamic fn demand. Used by `layoutParagraph` to (a) reserve fn
|
|
19
|
+
* height per line and (b) record the IDs on the host page so the
|
|
20
|
+
* painter renders the fn on the page where the ref-bearing line
|
|
21
|
+
* actually landed — even when the paragraph splits across pages
|
|
22
|
+
* (fragment pmStart/pmEnd is paragraph-wide and cannot disambiguate
|
|
23
|
+
* between split halves).
|
|
24
|
+
*/
|
|
25
|
+
function getLineFootnoteRefs(block, fromRun, toRun, fnHeights) {
|
|
26
|
+
if (!fnHeights) return {
|
|
27
|
+
ids: [],
|
|
28
|
+
height: 0
|
|
29
|
+
};
|
|
30
|
+
const ids = [];
|
|
31
|
+
let height = 0;
|
|
32
|
+
for (let r = fromRun; r <= toRun; r++) {
|
|
33
|
+
const run = block.runs[r];
|
|
34
|
+
if (!run || run.kind !== "text") continue;
|
|
35
|
+
const id = run.footnoteRefId;
|
|
36
|
+
if (id === void 0) continue;
|
|
37
|
+
const h = fnHeights.get(id);
|
|
38
|
+
if (h !== void 0) {
|
|
39
|
+
ids.push(id);
|
|
40
|
+
height += h;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ids,
|
|
45
|
+
height
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeightById, suppressSpaceBefore }) {
|
|
49
|
+
const lines = measure.lines;
|
|
50
|
+
if (lines.length === 0) {
|
|
51
|
+
const spaceBefore = suppressSpaceBefore ? 0 : getParagraphSpacingBefore(block);
|
|
52
|
+
const spaceAfter = getParagraphSpacingAfter(block);
|
|
53
|
+
const state = paginator.getCurrentState();
|
|
54
|
+
const fragment = {
|
|
55
|
+
kind: "paragraph",
|
|
56
|
+
...block.attrs?.suppressEmptyParagraphHeight === true ? { paginationRole: "empty-carrier" } : {},
|
|
57
|
+
blockId: block.id,
|
|
58
|
+
x: paginator.getColumnX(state.columnIndex),
|
|
59
|
+
y: state.cursorY + spaceBefore,
|
|
60
|
+
width: contentWidth,
|
|
61
|
+
height: 0,
|
|
62
|
+
fromLine: 0,
|
|
63
|
+
toLine: 0,
|
|
64
|
+
...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
|
|
65
|
+
...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
|
|
66
|
+
...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
|
|
67
|
+
};
|
|
68
|
+
paginator.addFragment(fragment, 0, spaceBefore, spaceAfter);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const spaceBefore = suppressSpaceBefore ? 0 : getParagraphSpacingBefore(block);
|
|
72
|
+
const spaceAfter = getParagraphSpacingAfter(block);
|
|
73
|
+
let keepLinesDemand;
|
|
74
|
+
if (lines.length > 1 && hasKeepLines(block)) {
|
|
75
|
+
let footnoteHeight = 0;
|
|
76
|
+
for (const line of lines) footnoteHeight += getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById).height;
|
|
77
|
+
keepLinesDemand = {
|
|
78
|
+
linesHeight: measuredLineRangeHeight(lines, 0, lines.length),
|
|
79
|
+
footnoteHeight
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let keepLinesMoveAttempted = false;
|
|
83
|
+
let currentLineIndex = 0;
|
|
84
|
+
while (currentLineIndex < lines.length) {
|
|
85
|
+
const state = paginator.getCurrentState();
|
|
86
|
+
const availableHeight = paginator.getAvailableHeight();
|
|
87
|
+
const currentLine = lines.at(currentLineIndex);
|
|
88
|
+
const markerAlreadySatisfied = state.columnIndex === 0 && state.cursorY === state.topMargin && state.page.fragments.length === 0;
|
|
89
|
+
if (currentLine?.renderedPageBreakBefore === true && !markerAlreadySatisfied && availableHeight <= measuredLineAdvance(currentLine) * 3) {
|
|
90
|
+
paginator.forcePageBreak();
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (currentLineIndex === 0 && state.trailingSpacing > 0 && state.cursorY !== state.topMargin) {
|
|
94
|
+
const firstLine = lines[0];
|
|
95
|
+
const firstLineRefs = getLineFootnoteRefs(block, firstLine.fromRun, firstLine.toRun, footnoteHeightById);
|
|
96
|
+
const firstLineHeight = measuredLineAdvance(firstLine) + projectedFootnoteReserveGrowth(state, firstLineRefs.height);
|
|
97
|
+
const collapsedLead = collapseParagraphSpacing({
|
|
98
|
+
before: spaceBefore,
|
|
99
|
+
after: state.trailingSpacing
|
|
100
|
+
});
|
|
101
|
+
const columnCapacity = state.contentBottom - state.topMargin;
|
|
102
|
+
if (collapsedLead + firstLineHeight > availableHeight && spaceBefore + firstLineHeight <= columnCapacity) {
|
|
103
|
+
paginator.ensureFits(collapsedLead + firstLineHeight);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (keepLinesDemand !== void 0 && !keepLinesMoveAttempted && currentLineIndex === 0 && state.cursorY !== state.topMargin) {
|
|
108
|
+
const wholeHeight = keepLinesDemand.linesHeight + projectedFootnoteReserveGrowth(state, keepLinesDemand.footnoteHeight);
|
|
109
|
+
const collapsedLead = collapseParagraphSpacing({
|
|
110
|
+
before: spaceBefore,
|
|
111
|
+
after: state.trailingSpacing
|
|
112
|
+
});
|
|
113
|
+
const columnCapacity = state.contentBottom - state.topMargin;
|
|
114
|
+
if (collapsedLead + wholeHeight > availableHeight && spaceBefore + wholeHeight <= columnCapacity) {
|
|
115
|
+
keepLinesMoveAttempted = true;
|
|
116
|
+
paginator.ensureFits(collapsedLead + wholeHeight);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let linesHeight = 0;
|
|
121
|
+
let linesFnHeight = 0;
|
|
122
|
+
const linesFnIds = [];
|
|
123
|
+
let fittingLines = 0;
|
|
124
|
+
let forcePageBreakAfterFragment = false;
|
|
125
|
+
const firstFragmentSpaceBefore = currentLineIndex === 0 ? collapseParagraphSpacing({
|
|
126
|
+
before: spaceBefore,
|
|
127
|
+
after: state.trailingSpacing
|
|
128
|
+
}) : 0;
|
|
129
|
+
for (let j = currentLineIndex; j < lines.length; j++) {
|
|
130
|
+
const line = lines[j];
|
|
131
|
+
const lineAdvance = measuredLineAdvance(line);
|
|
132
|
+
if (j > currentLineIndex && line.renderedPageBreakBefore === true && availableHeight - (linesHeight + firstFragmentSpaceBefore + linesFnHeight) <= lineAdvance * 3) {
|
|
133
|
+
forcePageBreakAfterFragment = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
|
|
137
|
+
const totalWithLine = linesHeight + lineAdvance;
|
|
138
|
+
const footnoteGrowth = projectedFootnoteReserveGrowth(state, linesFnHeight + lineRefs.height);
|
|
139
|
+
if (totalWithLine + firstFragmentSpaceBefore + footnoteGrowth <= availableHeight || fittingLines === 0) {
|
|
140
|
+
linesHeight = totalWithLine;
|
|
141
|
+
linesFnHeight += lineRefs.height;
|
|
142
|
+
for (const id of lineRefs.ids) linesFnIds.push(id);
|
|
143
|
+
fittingLines++;
|
|
144
|
+
} else break;
|
|
145
|
+
}
|
|
146
|
+
let forceBreakAfterFragment = false;
|
|
147
|
+
if (hasWidowControl(block)) {
|
|
148
|
+
const remainingAfter = lines.length - (currentLineIndex + fittingLines);
|
|
149
|
+
if (fittingLines > 1 && remainingAfter === 1) {
|
|
150
|
+
if (currentLineIndex === 0 && fittingLines === 2 && state.cursorY !== state.topMargin) {
|
|
151
|
+
paginator.forceColumnBreak();
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
fittingLines -= 1;
|
|
155
|
+
forceBreakAfterFragment = true;
|
|
156
|
+
linesHeight = 0;
|
|
157
|
+
linesFnHeight = 0;
|
|
158
|
+
linesFnIds.length = 0;
|
|
159
|
+
for (let j = currentLineIndex; j < currentLineIndex + fittingLines; j++) {
|
|
160
|
+
const line = lines[j];
|
|
161
|
+
linesHeight += measuredLineAdvance(line);
|
|
162
|
+
const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
|
|
163
|
+
linesFnHeight += lineRefs.height;
|
|
164
|
+
for (const id of lineRefs.ids) linesFnIds.push(id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const isFirstFragment = currentLineIndex === 0;
|
|
169
|
+
const isLastFragment = currentLineIndex + fittingLines >= lines.length;
|
|
170
|
+
const effectiveSpaceBefore = isFirstFragment ? spaceBefore : 0;
|
|
171
|
+
const effectiveSpaceAfter = isLastFragment ? spaceAfter : 0;
|
|
172
|
+
const pmRange = getParagraphFragmentPmRange(block, measure, currentLineIndex, currentLineIndex + fittingLines);
|
|
173
|
+
const fragment = {
|
|
174
|
+
kind: "paragraph",
|
|
175
|
+
...block.attrs?.suppressEmptyParagraphHeight === true ? { paginationRole: "empty-carrier" } : {},
|
|
176
|
+
blockId: block.id,
|
|
177
|
+
x: paginator.getColumnX(state.columnIndex),
|
|
178
|
+
y: 0,
|
|
179
|
+
width: contentWidth,
|
|
180
|
+
height: linesHeight,
|
|
181
|
+
fromLine: currentLineIndex,
|
|
182
|
+
toLine: currentLineIndex + fittingLines,
|
|
183
|
+
...pmRange.pmStart !== void 0 ? { pmStart: pmRange.pmStart } : {},
|
|
184
|
+
...pmRange.pmEnd !== void 0 ? { pmEnd: pmRange.pmEnd } : {},
|
|
185
|
+
...!isFirstFragment ? { continuesFromPrev: true } : {},
|
|
186
|
+
...!isLastFragment ? { continuesOnNext: true } : {},
|
|
187
|
+
...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
|
|
188
|
+
};
|
|
189
|
+
if (linesFnHeight > 0) {
|
|
190
|
+
const stateBefore = paginator.getCurrentState();
|
|
191
|
+
paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateBefore, linesFnHeight));
|
|
192
|
+
const stateAfter = paginator.getCurrentState();
|
|
193
|
+
if (stateAfter.footnoteDemandHeight === 0) paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateAfter, linesFnHeight + 12));
|
|
194
|
+
}
|
|
195
|
+
fragment.y = paginator.addFragment(fragment, linesHeight, effectiveSpaceBefore, effectiveSpaceAfter).y;
|
|
196
|
+
if (linesFnHeight > 0) paginator.addFootnoteHeight(linesFnHeight, linesFnIds);
|
|
197
|
+
currentLineIndex += fittingLines;
|
|
198
|
+
if (currentLineIndex < lines.length) if (forcePageBreakAfterFragment) paginator.forcePageBreak();
|
|
199
|
+
else if (forceBreakAfterFragment) paginator.forceColumnBreak();
|
|
200
|
+
else paginator.ensureFits(measuredLineAdvance(lines[currentLineIndex]));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
export { layoutParagraph };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ColumnLayout, FlowBlock, Measure, PageMargins, SectionBreakBlock, SectionPageNumbering } from "./types.js";
|
|
2
|
+
import { createPaginator } from "./paginator.js";
|
|
3
|
+
import { SectionStart } from "@stll/docx-core/model";
|
|
4
|
+
//#region src/layout-engine/sectionLayout.d.ts
|
|
5
|
+
type SectionLayoutConfig = {
|
|
6
|
+
pageSize: {
|
|
7
|
+
w: number;
|
|
8
|
+
h: number;
|
|
9
|
+
};
|
|
10
|
+
margins: PageMargins;
|
|
11
|
+
pageNumbering: SectionPageNumbering;
|
|
12
|
+
columns?: ColumnLayout;
|
|
13
|
+
};
|
|
14
|
+
declare const DEFAULT_COLUMNS: ColumnLayout;
|
|
15
|
+
declare const CONTINUE_PAGE_NUMBERING: SectionPageNumbering;
|
|
16
|
+
declare function collectSectionConfigs(blocks: FlowBlock[], initialConfig: SectionLayoutConfig, finalConfig: SectionLayoutConfig): {
|
|
17
|
+
configs: SectionLayoutConfig[];
|
|
18
|
+
breakIndices: number[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Estimate the height of a short paragraph-only multi-column section so its
|
|
22
|
+
* final page can use Word-style balanced columns. Sections containing tables,
|
|
23
|
+
* floating blocks, or authored breaks keep the normal bottom-up pagination;
|
|
24
|
+
* those need structure-aware balancing rather than a height estimate.
|
|
25
|
+
*/
|
|
26
|
+
type BalancedParagraphSectionHeightOptions = {
|
|
27
|
+
blocks: FlowBlock[];
|
|
28
|
+
measures: Measure[];
|
|
29
|
+
startIndex: number;
|
|
30
|
+
endIndex: number;
|
|
31
|
+
incomingSpacing: number;
|
|
32
|
+
columnCount: number;
|
|
33
|
+
availableHeight: number;
|
|
34
|
+
};
|
|
35
|
+
declare function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex, incomingSpacing, columnCount, availableHeight }: BalancedParagraphSectionHeightOptions): number | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Handle a section break block.
|
|
38
|
+
* @param block - The section break block (current section's properties)
|
|
39
|
+
* @param paginator - The paginator instance
|
|
40
|
+
* @param nextSectionConfig - Page layout for the NEXT section
|
|
41
|
+
* @param nextSectionType - Break type of the NEXT section (how it starts relative to current)
|
|
42
|
+
*/
|
|
43
|
+
declare function handleSectionBreak(_block: SectionBreakBlock, paginator: ReturnType<typeof createPaginator>, nextSectionConfig: SectionLayoutConfig, nextSectionType: SectionStart, nextSectionIndex?: number): void;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { CONTINUE_PAGE_NUMBERING, DEFAULT_COLUMNS, SectionLayoutConfig, balancedParagraphSectionHeight, collectSectionConfigs, handleSectionBreak };
|