@stll/folio-core 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-edits/apply.js +565 -121
- package/dist/ai-edits/headless.d.ts +37 -1
- package/dist/ai-edits/headless.js +47 -16
- package/dist/ai-edits/minimal-replacement.d.ts +64 -0
- package/dist/ai-edits/minimal-replacement.js +282 -0
- package/dist/controller/layoutPipeline.js +11 -1
- package/dist/display-list/build/paragraphPrimitives.js +63 -7
- package/dist/display-list/build/textBoxPrimitives.js +1 -1
- package/dist/display-list/build/unsupported.d.ts +2 -2
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
- package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
- package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
- package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
- package/dist/display-list/selectDisplayPages.d.ts +6 -0
- package/dist/display-list/selectDisplayPages.js +44 -0
- package/dist/docx/blockContentParser.js +2 -2
- package/dist/docx/bulletMarkers.d.ts +18 -2
- package/dist/docx/bulletMarkers.js +27 -2
- package/dist/docx/drawingGroupChildren.d.ts +60 -0
- package/dist/docx/drawingGroupChildren.js +176 -0
- package/dist/docx/drawingUtils.js +3 -0
- package/dist/docx/groupDrawingParser.d.ts +7 -1
- package/dist/docx/groupDrawingParser.js +123 -32
- package/dist/docx/packageParts.d.ts +45 -1
- package/dist/docx/packageParts.js +93 -2
- package/dist/docx/paraIdAttribute.d.ts +3 -2
- package/dist/docx/paragraphParser.js +91 -7
- package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
- package/dist/docx/runParser.js +10 -0
- package/dist/docx/selectiveSaveFlags.d.ts +10 -3
- package/dist/docx/selectiveSaveFlags.js +1 -1
- package/dist/docx/selectiveXmlPatch.d.ts +56 -34
- package/dist/docx/selectiveXmlPatch.js +222 -115
- package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
- package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
- package/dist/docx/serializer/paragraphSerializer.js +5 -4
- package/dist/docx/serializer/runSerializer.d.ts +5 -1
- package/dist/docx/serializer/runSerializer.js +4 -3
- package/dist/docx/shapeAlternateContent.d.ts +25 -0
- package/dist/docx/shapeAlternateContent.js +52 -0
- package/dist/docx/textBoxParser.d.ts +10 -2
- package/dist/docx/textBoxParser.js +9 -3
- package/dist/export-pdf.d.ts +35 -2
- package/dist/export-pdf.js +51 -20
- package/dist/fonts/fontsourceFaces.d.ts +47 -0
- package/dist/fonts/fontsourceFaces.js +230 -0
- package/dist/internal/headlessRevisionResolution.js +50 -13
- package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
- package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
- package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
- package/dist/layout-bridge/convert/flowBorders.js +31 -0
- package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
- package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
- package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
- package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
- package/dist/layout-bridge/convert/imageConversion.js +118 -0
- package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
- package/dist/layout-bridge/convert/listMarkers.js +98 -0
- package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
- package/dist/layout-bridge/convert/noteReferences.js +29 -0
- package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
- package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
- package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
- package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
- package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
- package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
- package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
- package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
- package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
- package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
- package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
- package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
- package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
- package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
- package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
- package/dist/layout-bridge/convert/tableConversion.js +303 -0
- package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
- package/dist/layout-bridge/convert/textBoxFill.js +42 -0
- package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
- package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
- package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2060
- package/dist/layout-bridge/engine/hitTest.js +1 -1
- package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
- package/dist/layout-bridge/engine/measuring/index.js +2 -1
- package/dist/layout-bridge/engine/selectionRects.js +1 -1
- package/dist/layout-engine/imageLayout.d.ts +9 -0
- package/dist/layout-engine/imageLayout.js +48 -0
- package/dist/layout-engine/index.d.ts +4 -19
- package/dist/layout-engine/index.js +14 -910
- package/dist/layout-engine/keep-together.d.ts +5 -2
- package/dist/layout-engine/keep-together.js +5 -1
- package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
- package/dist/layout-engine/layoutFlowShared.js +8 -0
- package/dist/layout-engine/measure/advanceComposition.js +49 -3
- package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
- package/dist/layout-engine/measure/crossRunWords.js +172 -0
- package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
- package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
- package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
- package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
- package/dist/layout-engine/measure/justification.d.ts +32 -0
- package/dist/layout-engine/measure/justification.js +83 -0
- package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
- package/dist/layout-engine/measure/lineFitting.js +145 -0
- package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
- package/dist/layout-engine/measure/lineTypography.js +124 -0
- package/dist/layout-engine/measure/measureBlocks.js +7 -4
- package/dist/layout-engine/measure/measureContainer.js +46 -3
- package/dist/layout-engine/measure/measureHelpers.js +13 -3
- package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
- package/dist/layout-engine/measure/measureParagraph.js +41 -780
- package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
- package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
- package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
- package/dist/layout-engine/measure/paragraphTabs.js +78 -0
- package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
- package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
- package/dist/layout-engine/paginator.d.ts +2 -0
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphLayout.d.ts +23 -0
- package/dist/layout-engine/paragraphLayout.js +204 -0
- package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
- package/dist/layout-engine/paragraphSpacing.js +2 -1
- package/dist/layout-engine/sectionLayout.d.ts +45 -0
- package/dist/layout-engine/sectionLayout.js +216 -0
- package/dist/layout-engine/tableLayout.d.ts +17 -0
- package/dist/layout-engine/tableLayout.js +369 -0
- package/dist/layout-engine/tableRowBreak.d.ts +8 -1
- package/dist/layout-engine/tableRowBreak.js +22 -1
- package/dist/layout-engine/textBoxLayout.d.ts +15 -0
- package/dist/layout-engine/textBoxLayout.js +137 -0
- package/dist/layout-engine/types.d.ts +21 -2
- package/dist/layout-painter/renderPage.js +1 -0
- package/dist/layout-painter/renderParagraph.js +41 -4
- package/dist/layout-painter/renderTextBox.d.ts +12 -2
- package/dist/layout-painter/renderTextBox.js +26 -2
- package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
- package/dist/paged-layout/sectionBlockWidths.js +1 -1
- package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
- package/dist/prosemirror/alternateContentAttrs.js +35 -0
- package/dist/prosemirror/attrs/index.js +90 -0
- package/dist/prosemirror/commands/comments.js +51 -41
- package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
- package/dist/prosemirror/contentControlRevisions.js +84 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
- package/dist/prosemirror/conversion/toProseDoc.js +42 -19
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
- package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
- package/dist/prosemirror/listMarker.js +2 -2
- package/dist/prosemirror/paragraphDirection.d.ts +29 -2
- package/dist/prosemirror/paragraphDirection.js +21 -2
- package/dist/prosemirror/plugins/suggestionMode.js +37 -8
- package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
- package/dist/prosemirror/rejoinRunCarriers.js +66 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
- package/dist/prosemirror/schema/nodes.d.ts +37 -5
- package/dist/server.d.ts +3 -2
- package/dist/types/content.d.ts +2 -2
- package/package.json +2 -2
- package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
- package/dist/layout-engine/justifiedLineFit.js +0 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FlowBlock, Measure } from "./types.js";
|
|
1
|
+
import { FlowBlock, Measure, TableBlock, TableMeasure } from "./types.js";
|
|
2
2
|
//#region src/layout-engine/keep-together.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* A chain of paragraphs that Word keeps with following content. This includes
|
|
@@ -36,12 +36,15 @@ declare function computeKeepNextChains(blocks: FlowBlock[]): Map<number, KeepNex
|
|
|
36
36
|
* reservation continues through it; otherwise the chain stops after the
|
|
37
37
|
* opening, because a later split cannot separate it from its predecessor.
|
|
38
38
|
*
|
|
39
|
+
* A table anchor contributes `tableOpeningHeight`: the part of the table
|
|
40
|
+
* that must start on the same page as the paragraph before it.
|
|
41
|
+
*
|
|
39
42
|
* A paragraph measure's `totalHeight` already includes its own spacing before
|
|
40
43
|
* and after. The chain accounts for spacing separately (collapsing each gap to
|
|
41
44
|
* the larger side, as the paginator does), so members contribute only their
|
|
42
45
|
* line advances; otherwise every gap would be reserved twice.
|
|
43
46
|
*/
|
|
44
|
-
declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[], incomingSpacing?: number): number;
|
|
47
|
+
declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[], incomingSpacing?: number, tableOpeningHeight?: (block: TableBlock, measure: TableMeasure) => number): number;
|
|
45
48
|
/**
|
|
46
49
|
* Get the set of indices that are mid-chain (not chain starters).
|
|
47
50
|
* These should skip the keepNext check since their chain starter already decided.
|
|
@@ -102,12 +102,15 @@ function minimumOpeningLineCount(block, measure) {
|
|
|
102
102
|
* reservation continues through it; otherwise the chain stops after the
|
|
103
103
|
* opening, because a later split cannot separate it from its predecessor.
|
|
104
104
|
*
|
|
105
|
+
* A table anchor contributes `tableOpeningHeight`: the part of the table
|
|
106
|
+
* that must start on the same page as the paragraph before it.
|
|
107
|
+
*
|
|
105
108
|
* A paragraph measure's `totalHeight` already includes its own spacing before
|
|
106
109
|
* and after. The chain accounts for spacing separately (collapsing each gap to
|
|
107
110
|
* the larger side, as the paginator does), so members contribute only their
|
|
108
111
|
* line advances; otherwise every gap would be reserved twice.
|
|
109
112
|
*/
|
|
110
|
-
function calculateChainHeight(chain, blocks, measures, incomingSpacing = 0) {
|
|
113
|
+
function calculateChainHeight(chain, blocks, measures, incomingSpacing = 0, tableOpeningHeight) {
|
|
111
114
|
const firstMemberIndex = chain.memberIndices.at(0);
|
|
112
115
|
if (firstMemberIndex === void 0) return 0;
|
|
113
116
|
const firstBlock = blocks[firstMemberIndex];
|
|
@@ -125,6 +128,7 @@ function calculateChainHeight(chain, blocks, measures, incomingSpacing = 0) {
|
|
|
125
128
|
if (successorIndex === void 0) continue;
|
|
126
129
|
const successorBlock = blocks[successorIndex];
|
|
127
130
|
const successorMeasure = measures[successorIndex];
|
|
131
|
+
if (tableOpeningHeight !== void 0 && successorBlock?.kind === "table" && successorMeasure?.kind === "table" && successorBlock.floating === void 0) return totalHeight + trailingSpacing + tableOpeningHeight(successorBlock, successorMeasure);
|
|
128
132
|
if (successorBlock?.kind !== "paragraph" || successorMeasure?.kind !== "paragraph") return totalHeight;
|
|
129
133
|
totalHeight += collapseParagraphSpacing({
|
|
130
134
|
before: successorBlock.attrs?.spacing?.before ?? 0,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PageState } from "./paginator.js";
|
|
2
|
+
//#region src/layout-engine/layoutFlowShared.d.ts
|
|
3
|
+
declare const RENDERED_BREAK_REFLOW_TOLERANCE_LINES = 3;
|
|
4
|
+
declare function projectedFootnoteReserveGrowth(state: PageState, additionalDemandHeight: number): number;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { RENDERED_BREAK_REFLOW_TOLERANCE_LINES, projectedFootnoteReserveGrowth };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/layout-engine/layoutFlowShared.ts
|
|
2
|
+
const RENDERED_BREAK_REFLOW_TOLERANCE_LINES = 3;
|
|
3
|
+
function projectedFootnoteReserveGrowth(state, additionalDemandHeight) {
|
|
4
|
+
const projectedDemand = state.footnoteDemandHeight + additionalDemandHeight;
|
|
5
|
+
return Math.max(state.footnoteHeightFloor, projectedDemand) - state.footnoteHeight;
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { RENDERED_BREAK_REFLOW_TOLERANCE_LINES, projectedFootnoteReserveGrowth };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
|
|
2
2
|
import { SCRIPT_CLASS, hasComplexScript, hasEastAsiaSlotText, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
|
|
3
3
|
import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./complexScriptFormatting.js";
|
|
4
|
+
import "./measureHelpers.js";
|
|
5
|
+
import { SMALL_CAPS_SCALE, smallCapsMask, smallCapsSegments } from "./smallCapsCasing.js";
|
|
4
6
|
//#region src/layout-engine/measure/advanceComposition.ts
|
|
5
7
|
/**
|
|
6
8
|
* The arithmetic that turns raw glyph advances into measured widths.
|
|
@@ -50,6 +52,38 @@ const glyphAdvanceStyle = (style) => {
|
|
|
50
52
|
};
|
|
51
53
|
const needsPerScriptFonts = (style, text) => style.eastAsiaFontFamily !== void 0 && hasEastAsiaSlotText(text, style.eastAsiaHint) || hasComplexScriptFormatting(style) && hasComplexScript(text);
|
|
52
54
|
/**
|
|
55
|
+
* Width of a `w:smallCaps` string: its lowercase letters (and the whitespace
|
|
56
|
+
* that follows them — see smallCapsCasing.ts) draw at `SMALL_CAPS_SCALE` of
|
|
57
|
+
* the style's size, everything else at full size. A small segment is measured
|
|
58
|
+
* at that literal, smaller size — not the full-size advance scaled by
|
|
59
|
+
* arithmetic — mirroring {@link canvasMeasureTextWidth}'s
|
|
60
|
+
* `measureSmallCapsWidth` so the two backends agree; each segment still
|
|
61
|
+
* resolves its own per-script font, the same as the plain (non-small-caps)
|
|
62
|
+
* path below.
|
|
63
|
+
*/
|
|
64
|
+
const smallCapsGlyphWidth = (text, style, advanceOf) => {
|
|
65
|
+
const segmentStyle = { ...style };
|
|
66
|
+
delete segmentStyle.fontVariant;
|
|
67
|
+
delete segmentStyle.letterSpacing;
|
|
68
|
+
delete segmentStyle.horizontalScale;
|
|
69
|
+
const smallSegmentStyle = {
|
|
70
|
+
...segmentStyle,
|
|
71
|
+
fontSize: (segmentStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
|
|
72
|
+
};
|
|
73
|
+
let total = 0;
|
|
74
|
+
for (const segment of smallCapsSegments(text)) {
|
|
75
|
+
const activeStyle = segment.small ? smallSegmentStyle : segmentStyle;
|
|
76
|
+
total += needsPerScriptFonts(activeStyle, segment.text) ? segmentByScript(segment.text, activeStyle.eastAsiaHint).reduce((sum, scriptSegment) => sum + advanceOf(scriptSegment.text, glyphAdvanceStyle(scriptStyle(activeStyle, scriptSegment.script))), 0) : advanceOf(segment.text, activeStyle);
|
|
77
|
+
}
|
|
78
|
+
return total;
|
|
79
|
+
};
|
|
80
|
+
/** The un-spaced, un-scaled glyph width `composeTextWidth` spaces and scales. */
|
|
81
|
+
const glyphWidthOf = ({ source, style, transformed, letterSpacing, advanceOf }) => {
|
|
82
|
+
if (!source.forceComplexScript && style.fontVariant === "small-caps") return smallCapsGlyphWidth(transformed, style, advanceOf);
|
|
83
|
+
if (!source.forceComplexScript && !letterSpacing && needsPerScriptFonts(style, transformed)) return segmentByScript(transformed, style.eastAsiaHint).reduce((sum, segment) => sum + advanceOf(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script))), 0);
|
|
84
|
+
return advanceOf(transformed, style);
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
53
87
|
* Width of a string: the advance width used for line breaking, never the
|
|
54
88
|
* painted (ink) extent, which includes overhang a break decision must ignore.
|
|
55
89
|
*/
|
|
@@ -59,7 +93,13 @@ const composeTextWidth = ({ text, style: source, advanceOf }) => {
|
|
|
59
93
|
const transformed = applyMeasurementTextTransform(text, style);
|
|
60
94
|
const letterSpacing = style.letterSpacing ?? 0;
|
|
61
95
|
const horizontalScale = getHorizontalScaleFactor(style.horizontalScale);
|
|
62
|
-
const glyphWidth =
|
|
96
|
+
const glyphWidth = glyphWidthOf({
|
|
97
|
+
source,
|
|
98
|
+
style,
|
|
99
|
+
transformed,
|
|
100
|
+
letterSpacing,
|
|
101
|
+
advanceOf
|
|
102
|
+
});
|
|
63
103
|
const codePoints = countCodePoints(transformed);
|
|
64
104
|
return (letterSpacing && codePoints > 1 ? glyphWidth + letterSpacing * (codePoints - 1) : glyphWidth) * horizontalScale;
|
|
65
105
|
};
|
|
@@ -92,10 +132,16 @@ const composeRunMeasurement = ({ text, style: source, metrics, advanceOf, advanc
|
|
|
92
132
|
perScript
|
|
93
133
|
});
|
|
94
134
|
let codePointIndex = 0;
|
|
135
|
+
const smallCapsAt = style.fontVariant === "small-caps" ? smallCapsMask(text) : void 0;
|
|
95
136
|
for (const char of text) {
|
|
96
137
|
const cp = char.codePointAt(0);
|
|
97
|
-
const
|
|
98
|
-
|
|
138
|
+
const isSmallCap = smallCapsAt?.[codePointIndex] ?? false;
|
|
139
|
+
const scriptedStyle = perScript ? scriptStyle(style, scriptClassOf(cp, style.eastAsiaHint)) : style;
|
|
140
|
+
const charStyle = isSmallCap ? {
|
|
141
|
+
...scriptedStyle,
|
|
142
|
+
fontSize: (scriptedStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
|
|
143
|
+
} : scriptedStyle;
|
|
144
|
+
let charWidth = shaped.get(codePointIndex) ?? advanceOf(isSmallCap ? char.toLocaleUpperCase() : applyMeasurementTextTransform(char, style), charStyle);
|
|
99
145
|
if (letterSpacing && offset + char.length < text.length) charWidth += letterSpacing;
|
|
100
146
|
charWidth *= scale;
|
|
101
147
|
charWidths.push(charWidth);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ParagraphBlock, TextRun } from "../types.js";
|
|
2
|
+
import { FontStyle } from "./measureTypes.js";
|
|
3
|
+
//#region src/layout-engine/measure/crossRunWords.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Width of the unbreakable text glued to the end of each run.
|
|
6
|
+
* A run boundary is not itself a wrap opportunity: adjacent note markers and
|
|
7
|
+
* format-only word splits must wrap as one cluster.
|
|
8
|
+
*/
|
|
9
|
+
declare function computeTrailingGlueWidths(block: ParagraphBlock): number[];
|
|
10
|
+
/**
|
|
11
|
+
* First character of the text that follows each run, looking through empty
|
|
12
|
+
* text runs; undefined before a non-text run or at the paragraph end.
|
|
13
|
+
*/
|
|
14
|
+
declare function computeFollowingTextLeads(block: ParagraphBlock): (string | undefined)[];
|
|
15
|
+
declare function computeProtectedCrossRunGlueWidths(block: ParagraphBlock): number[];
|
|
16
|
+
type CrossRunWordSegment = {
|
|
17
|
+
runIndex: number;
|
|
18
|
+
startChar: number;
|
|
19
|
+
text: string;
|
|
20
|
+
run: TextRun;
|
|
21
|
+
style: FontStyle;
|
|
22
|
+
};
|
|
23
|
+
type CrossRunWord = {
|
|
24
|
+
text: string;
|
|
25
|
+
width: number;
|
|
26
|
+
breaks: number[];
|
|
27
|
+
segments: CrossRunWordSegment[];
|
|
28
|
+
};
|
|
29
|
+
type CollectCrossRunWordOptions = {
|
|
30
|
+
block: ParagraphBlock;
|
|
31
|
+
startRunIndex: number;
|
|
32
|
+
startChar: number;
|
|
33
|
+
};
|
|
34
|
+
/** Collect one lexical word continued through adjacent formatting runs. */
|
|
35
|
+
declare function collectCrossRunWord({ block, startRunIndex, startChar }: CollectCrossRunWordOptions): CrossRunWord | undefined;
|
|
36
|
+
type CrossRunPrefix = {
|
|
37
|
+
width: number;
|
|
38
|
+
endRunIndex: number;
|
|
39
|
+
endChar: number;
|
|
40
|
+
hyphenStyle: FontStyle;
|
|
41
|
+
segments: CrossRunWordSegment[];
|
|
42
|
+
};
|
|
43
|
+
declare function measureCrossRunPrefix(word: CrossRunWord, breakOffset: number): CrossRunPrefix | undefined;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { CrossRunPrefix, collectCrossRunWord, computeFollowingTextLeads, computeProtectedCrossRunGlueWidths, computeTrailingGlueWidths, measureCrossRunPrefix };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { splitTrailingToken } from "../../utils/trailingText.js";
|
|
2
|
+
import { resolveEffectiveLineBreakPolicy } from "./effectiveLineBreakPolicy.js";
|
|
3
|
+
import "./lineBreakProvider.js";
|
|
4
|
+
import { findHyphenationBreaks, findWordBreaks } from "./lineBreaks.js";
|
|
5
|
+
import { trimTrailingSpacesAndTabs } from "./lineFitting.js";
|
|
6
|
+
import { measureTextWidth } from "./measureProvider.js";
|
|
7
|
+
import { isSpaceOrTab, isTextRun, runToFontStyle } from "./paragraphMeasureShared.js";
|
|
8
|
+
//#region src/layout-engine/measure/crossRunWords.ts
|
|
9
|
+
/**
|
|
10
|
+
* Words that span runs: glue widths between differently formatted runs and
|
|
11
|
+
* the segments of a word collected across several runs.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Width of the unbreakable text glued to the end of each run.
|
|
15
|
+
* A run boundary is not itself a wrap opportunity: adjacent note markers and
|
|
16
|
+
* format-only word splits must wrap as one cluster.
|
|
17
|
+
*/
|
|
18
|
+
function computeTrailingGlueWidths(block) {
|
|
19
|
+
const runs = block.runs;
|
|
20
|
+
const widths = Array.from({ length: runs.length }, () => 0);
|
|
21
|
+
for (let index = runs.length - 1; index >= 0; index--) {
|
|
22
|
+
const nextRun = runs[index + 1];
|
|
23
|
+
if (!nextRun || !isTextRun(nextRun)) continue;
|
|
24
|
+
const text = nextRun.text;
|
|
25
|
+
if (!text) {
|
|
26
|
+
widths[index] = widths[index + 1] ?? 0;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (isSpaceOrTab(text.at(0))) continue;
|
|
30
|
+
const style = runToFontStyle(nextRun);
|
|
31
|
+
const firstBreak = findWordBreaks(text, resolveEffectiveLineBreakPolicy({
|
|
32
|
+
attrs: block.attrs,
|
|
33
|
+
run: nextRun
|
|
34
|
+
}).provider).at(0);
|
|
35
|
+
widths[index] = measureTextWidth(firstBreak === void 0 ? text : trimTrailingSpacesAndTabs(text.slice(0, firstBreak)), style) + (firstBreak === void 0 ? widths[index + 1] ?? 0 : 0);
|
|
36
|
+
}
|
|
37
|
+
return widths;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* First character of the text that follows each run, looking through empty
|
|
41
|
+
* text runs; undefined before a non-text run or at the paragraph end.
|
|
42
|
+
*/
|
|
43
|
+
function computeFollowingTextLeads(block) {
|
|
44
|
+
const leads = Array.from({ length: block.runs.length }, () => void 0);
|
|
45
|
+
let lead;
|
|
46
|
+
for (let index = block.runs.length - 1; index >= 0; index--) {
|
|
47
|
+
leads[index] = lead;
|
|
48
|
+
const run = block.runs[index];
|
|
49
|
+
if (!run || !isTextRun(run)) lead = void 0;
|
|
50
|
+
else if (run.text) lead = run.text[0];
|
|
51
|
+
}
|
|
52
|
+
return leads;
|
|
53
|
+
}
|
|
54
|
+
function computeProtectedCrossRunGlueWidths(block) {
|
|
55
|
+
const widths = Array.from({ length: block.runs.length }, () => 0);
|
|
56
|
+
for (let index = 0; index < block.runs.length; index++) {
|
|
57
|
+
const run = block.runs[index];
|
|
58
|
+
if (!run || !isTextRun(run)) continue;
|
|
59
|
+
const policy = resolveEffectiveLineBreakPolicy({
|
|
60
|
+
attrs: block.attrs,
|
|
61
|
+
run
|
|
62
|
+
}).provider;
|
|
63
|
+
if (!policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
|
|
64
|
+
const trailing = splitTrailingToken(run.text ?? "");
|
|
65
|
+
if (!trailing || trailing.token.length !== 1) continue;
|
|
66
|
+
const { token } = trailing;
|
|
67
|
+
let separator = trailing.separator;
|
|
68
|
+
let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
|
|
69
|
+
let followingWord = "";
|
|
70
|
+
let unseparatedFollowingText = false;
|
|
71
|
+
for (let nextIndex = index + 1; nextIndex < block.runs.length; nextIndex++) {
|
|
72
|
+
const nextRun = block.runs[nextIndex];
|
|
73
|
+
if (!nextRun || !isTextRun(nextRun)) break;
|
|
74
|
+
const text = nextRun.text ?? "";
|
|
75
|
+
let consumed = "";
|
|
76
|
+
for (const char of text) {
|
|
77
|
+
const isWhitespace = /\s/u.test(char);
|
|
78
|
+
if (followingWord.length > 0 && isWhitespace) break;
|
|
79
|
+
if (separator.length === 0 && !isWhitespace) {
|
|
80
|
+
unseparatedFollowingText = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
consumed += char;
|
|
84
|
+
if (isWhitespace) separator += char;
|
|
85
|
+
else followingWord += char;
|
|
86
|
+
}
|
|
87
|
+
if (consumed.length > 0) glueWidth += measureTextWidth(consumed, runToFontStyle(nextRun));
|
|
88
|
+
if (unseparatedFollowingText) break;
|
|
89
|
+
if (followingWord.length > 0 && consumed.length < text.length) break;
|
|
90
|
+
}
|
|
91
|
+
if (unseparatedFollowingText || separator.length === 0 || followingWord.length === 0) continue;
|
|
92
|
+
const boundary = token.length + separator.length;
|
|
93
|
+
if (!findWordBreaks(token + separator + followingWord, policy).includes(boundary)) widths[index] = glueWidth;
|
|
94
|
+
}
|
|
95
|
+
return widths;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Cap on the number of runs `collectCrossRunWord` traverses per call. A
|
|
99
|
+
* paragraph split into many tiny (including empty) runs would otherwise let
|
|
100
|
+
* the search walk the full run list for every word-start position it is
|
|
101
|
+
* invoked from, without the per-word text-length budget ever kicking in.
|
|
102
|
+
*/
|
|
103
|
+
const MAX_CROSS_RUN_SEGMENTS = 64;
|
|
104
|
+
/** Collect one lexical word continued through adjacent formatting runs. */
|
|
105
|
+
function collectCrossRunWord({ block, startRunIndex, startChar }) {
|
|
106
|
+
const segments = [];
|
|
107
|
+
let text = "";
|
|
108
|
+
let width = 0;
|
|
109
|
+
for (let runIndex = startRunIndex; runIndex < block.runs.length; runIndex++) {
|
|
110
|
+
if (runIndex - startRunIndex >= MAX_CROSS_RUN_SEGMENTS) return;
|
|
111
|
+
const run = block.runs[runIndex];
|
|
112
|
+
if (!run || !isTextRun(run)) break;
|
|
113
|
+
const start = runIndex === startRunIndex ? startChar : 0;
|
|
114
|
+
const remainder = run.text.slice(start);
|
|
115
|
+
if (remainder.length === 0) continue;
|
|
116
|
+
if (runIndex !== startRunIndex && isSpaceOrTab(remainder[0])) break;
|
|
117
|
+
const remainingBudget = 256 - text.length;
|
|
118
|
+
const boundedRemainder = remainder.slice(0, remainingBudget + 1);
|
|
119
|
+
const firstBreak = findWordBreaks(boundedRemainder, resolveEffectiveLineBreakPolicy({
|
|
120
|
+
attrs: block.attrs,
|
|
121
|
+
run
|
|
122
|
+
}).provider).at(0);
|
|
123
|
+
const segmentText = trimTrailingSpacesAndTabs(firstBreak === void 0 ? boundedRemainder : boundedRemainder.slice(0, firstBreak));
|
|
124
|
+
if (segmentText.length === 0) break;
|
|
125
|
+
if (segmentText.length > remainingBudget) return;
|
|
126
|
+
const style = runToFontStyle(run);
|
|
127
|
+
segments.push({
|
|
128
|
+
runIndex,
|
|
129
|
+
startChar: start,
|
|
130
|
+
text: segmentText,
|
|
131
|
+
run,
|
|
132
|
+
style
|
|
133
|
+
});
|
|
134
|
+
text += segmentText;
|
|
135
|
+
width += measureTextWidth(segmentText, style);
|
|
136
|
+
if (firstBreak !== void 0) break;
|
|
137
|
+
}
|
|
138
|
+
if (segments.length < 2) return;
|
|
139
|
+
const firstRun = segments[0]?.run;
|
|
140
|
+
if (!firstRun) return;
|
|
141
|
+
return {
|
|
142
|
+
text,
|
|
143
|
+
width,
|
|
144
|
+
breaks: findHyphenationBreaks(text, resolveEffectiveLineBreakPolicy({
|
|
145
|
+
attrs: block.attrs,
|
|
146
|
+
run: firstRun
|
|
147
|
+
}).provider),
|
|
148
|
+
segments
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function measureCrossRunPrefix(word, breakOffset) {
|
|
152
|
+
let width = 0;
|
|
153
|
+
let remaining = breakOffset;
|
|
154
|
+
const measuredSegments = [];
|
|
155
|
+
for (const segment of word.segments) {
|
|
156
|
+
const length = Math.min(remaining, segment.text.length);
|
|
157
|
+
if (length > 0) {
|
|
158
|
+
width += measureTextWidth(segment.text.slice(0, length), segment.style);
|
|
159
|
+
measuredSegments.push(segment);
|
|
160
|
+
}
|
|
161
|
+
if (remaining <= segment.text.length) return {
|
|
162
|
+
width,
|
|
163
|
+
endRunIndex: segment.runIndex,
|
|
164
|
+
endChar: segment.startChar + remaining,
|
|
165
|
+
hyphenStyle: segment.style,
|
|
166
|
+
segments: measuredSegments
|
|
167
|
+
};
|
|
168
|
+
remaining -= segment.text.length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { collectCrossRunWord, computeFollowingTextLeads, computeProtectedCrossRunGlueWidths, computeTrailingGlueWidths, measureCrossRunPrefix };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FloatingImageZone } from "./floatingZones.js";
|
|
2
|
+
//#region src/layout-engine/measure/floatingLineClearance.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Minimum horizontal room a line must offer before we treat it as usable for
|
|
5
|
+
* body text. Below this threshold the line is bumped past obstructing floats
|
|
6
|
+
* via `findClearLineY` instead of being rendered into the unusable sliver.
|
|
7
|
+
* Without this guard, a near-full-width float (e.g. floating table) produces
|
|
8
|
+
* a ~2px segment that collapses every wrap line to one glyph per row.
|
|
9
|
+
*/
|
|
10
|
+
declare const MIN_WRAP_SEGMENT_WIDTH = 24;
|
|
11
|
+
/**
|
|
12
|
+
* Find the next vertical position at or below `startY` where the available
|
|
13
|
+
* text width is at least `minWidth`. Used to skip lines past stacked floats
|
|
14
|
+
* when there is no horizontal room for meaningful text at the current Y.
|
|
15
|
+
*
|
|
16
|
+
* Returns `startY` if the current position already has enough room, otherwise
|
|
17
|
+
* the lowest `bottomY` of any zone currently obstructing the line. The caller
|
|
18
|
+
* is expected to re-query margins at the returned Y.
|
|
19
|
+
*
|
|
20
|
+
* Coordinates are absolute (i.e., already include any paragraphYOffset).
|
|
21
|
+
*/
|
|
22
|
+
declare function findClearLineY(startY: number, lineHeight: number, zones: FloatingImageZone[] | undefined, contentWidth: number, minWidth: number): number;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { MIN_WRAP_SEGMENT_WIDTH, findClearLineY };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
|
|
2
|
+
//#region src/layout-engine/measure/floatingLineClearance.ts
|
|
3
|
+
/**
|
|
4
|
+
* Finds where a line clears floating objects that leave too little width
|
|
5
|
+
* beside them.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Minimum horizontal room a line must offer before we treat it as usable for
|
|
9
|
+
* body text. Below this threshold the line is bumped past obstructing floats
|
|
10
|
+
* via `findClearLineY` instead of being rendered into the unusable sliver.
|
|
11
|
+
* Without this guard, a near-full-width float (e.g. floating table) produces
|
|
12
|
+
* a ~2px segment that collapses every wrap line to one glyph per row.
|
|
13
|
+
*/
|
|
14
|
+
const MIN_WRAP_SEGMENT_WIDTH = 24;
|
|
15
|
+
/**
|
|
16
|
+
* Find the next vertical position at or below `startY` where the available
|
|
17
|
+
* text width is at least `minWidth`. Used to skip lines past stacked floats
|
|
18
|
+
* when there is no horizontal room for meaningful text at the current Y.
|
|
19
|
+
*
|
|
20
|
+
* Returns `startY` if the current position already has enough room, otherwise
|
|
21
|
+
* the lowest `bottomY` of any zone currently obstructing the line. The caller
|
|
22
|
+
* is expected to re-query margins at the returned Y.
|
|
23
|
+
*
|
|
24
|
+
* Coordinates are absolute (i.e., already include any paragraphYOffset).
|
|
25
|
+
*/
|
|
26
|
+
function findClearLineY(startY, lineHeight, zones, contentWidth, minWidth) {
|
|
27
|
+
if (!zones || zones.length === 0) return startY;
|
|
28
|
+
let y = startY;
|
|
29
|
+
for (let i = 0; i < zones.length + 2; i++) {
|
|
30
|
+
const margins = getFloatingMargins(y, lineHeight, zones, 0);
|
|
31
|
+
if (Math.max(0, getFloatingAvailableWidth(margins, contentWidth)) >= minWidth) return y;
|
|
32
|
+
const lineBottom = y + lineHeight;
|
|
33
|
+
let nextY = Number.POSITIVE_INFINITY;
|
|
34
|
+
for (const zone of zones) {
|
|
35
|
+
if (lineBottom <= zone.topY || y >= zone.bottomY) continue;
|
|
36
|
+
if (zone.bottomY > y && zone.bottomY < nextY) nextY = zone.bottomY;
|
|
37
|
+
}
|
|
38
|
+
if (!Number.isFinite(nextY) || nextY <= y) return y;
|
|
39
|
+
y = nextY;
|
|
40
|
+
}
|
|
41
|
+
return y;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { MIN_WRAP_SEGMENT_WIDTH, findClearLineY };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ImageRun, MathRun } from "../types.js";
|
|
2
|
+
//#region src/layout-engine/measure/inlineObjectMetrics.d.ts
|
|
3
|
+
declare function rotatedBlockImageHeight(run: ImageRun): number;
|
|
4
|
+
declare function estimateMathFootprintPx(run: MathRun): number;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { estimateMathFootprintPx, rotatedBlockImageHeight };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ptToPx } from "./measureHelpers.js";
|
|
2
|
+
import "./paragraphMeasureShared.js";
|
|
3
|
+
//#region src/layout-engine/measure/inlineObjectMetrics.ts
|
|
4
|
+
function parseRotationDegrees(transform) {
|
|
5
|
+
if (!transform) return 0;
|
|
6
|
+
const match = /rotate\(\s*(?<degrees>[-\d.]+)\s*deg\s*\)/u.exec(transform);
|
|
7
|
+
if (!match) return 0;
|
|
8
|
+
const raw = Number.parseFloat(match.groups["degrees"]);
|
|
9
|
+
if (!Number.isFinite(raw)) return 0;
|
|
10
|
+
return (raw % 360 + 360) % 360;
|
|
11
|
+
}
|
|
12
|
+
function rotatedBlockImageHeight(run) {
|
|
13
|
+
const deg = parseRotationDegrees(run.transform);
|
|
14
|
+
if (deg === 0 || deg === 180) return run.height;
|
|
15
|
+
if (deg === 90 || deg === 270) return run.width;
|
|
16
|
+
const rad = deg * Math.PI / 180;
|
|
17
|
+
const sinA = Math.abs(Math.sin(rad));
|
|
18
|
+
const cosA = Math.abs(Math.cos(rad));
|
|
19
|
+
return run.width * sinA + run.height * cosA;
|
|
20
|
+
}
|
|
21
|
+
function isStackedMathLocalName(localName) {
|
|
22
|
+
switch (localName) {
|
|
23
|
+
case "bar":
|
|
24
|
+
case "d":
|
|
25
|
+
case "eqArr":
|
|
26
|
+
case "f":
|
|
27
|
+
case "groupChr":
|
|
28
|
+
case "limLow":
|
|
29
|
+
case "limUpp":
|
|
30
|
+
case "m":
|
|
31
|
+
case "nary":
|
|
32
|
+
case "rad":
|
|
33
|
+
case "sPre":
|
|
34
|
+
case "sSubSup": return true;
|
|
35
|
+
default: return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function hasStackedMathLayout(run) {
|
|
39
|
+
if (run.display === "block") return true;
|
|
40
|
+
for (const match of run.ommlXml.matchAll(/<(?<tag>[A-Za-z_][\w.-]*(?::[A-Za-z_][\w.-]*)?)(?:\s|\/|>)/gu)) {
|
|
41
|
+
const tagName = match.groups?.["tag"];
|
|
42
|
+
if (!tagName) continue;
|
|
43
|
+
const colonIndex = tagName.indexOf(":");
|
|
44
|
+
if (isStackedMathLocalName(colonIndex === -1 ? tagName : tagName.slice(colonIndex + 1))) return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
function estimateMathFootprintPx(run) {
|
|
49
|
+
return ptToPx(run.fontSize ?? 11) * (hasStackedMathLayout(run) ? 2.4 : 1.25);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { estimateMathFootprintPx, rotatedBlockImageHeight };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ParagraphBlock } from "../types.js";
|
|
2
|
+
import { FontStyle } from "./measureTypes.js";
|
|
3
|
+
import { LineState } from "./paragraphMeasureShared.js";
|
|
4
|
+
//#region src/layout-engine/measure/justification.d.ts
|
|
5
|
+
declare function uppercaseLetterRatio(text: string): number;
|
|
6
|
+
type JustifyFitStrategy = {
|
|
7
|
+
type: "rounding";
|
|
8
|
+
} | {
|
|
9
|
+
type: "space";
|
|
10
|
+
ratio: number;
|
|
11
|
+
} | {
|
|
12
|
+
type: "width";
|
|
13
|
+
ratio: number;
|
|
14
|
+
};
|
|
15
|
+
type JustificationProfile = {
|
|
16
|
+
hasTabRuns: boolean;
|
|
17
|
+
uppercaseRatio: number;
|
|
18
|
+
};
|
|
19
|
+
declare function resolveJustifyFitStrategy(block: ParagraphBlock, isFirstLine: boolean, profile: JustificationProfile): JustifyFitStrategy;
|
|
20
|
+
declare function compressibleSpaceWidth(text: string, style: FontStyle): number;
|
|
21
|
+
/**
|
|
22
|
+
* Content being fitted onto a justified line: the line width with it added,
|
|
23
|
+
* and the width of spaces inside it that stay on the line (an atomic field
|
|
24
|
+
* result). A word's own trailing space hangs instead, so it is not counted.
|
|
25
|
+
*/
|
|
26
|
+
type JustifyCandidate = {
|
|
27
|
+
width: number;
|
|
28
|
+
innerSpaceWidth: number;
|
|
29
|
+
};
|
|
30
|
+
declare function justifyFitTolerance(line: LineState, strategy: JustifyFitStrategy, candidateSpaceWidth: number, candidate?: JustifyCandidate): number;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { JustificationProfile, compressibleSpaceWidth, justifyFitTolerance, resolveJustifyFitStrategy, uppercaseLetterRatio };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { measureTextWidth } from "./measureProvider.js";
|
|
2
|
+
import { WIDTH_TOLERANCE } from "./paragraphMeasureShared.js";
|
|
3
|
+
import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
|
|
4
|
+
//#region src/layout-engine/measure/justification.ts
|
|
5
|
+
const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
|
|
6
|
+
const JUSTIFY_SPACE_CONTRACTION_RATIO = .25;
|
|
7
|
+
const JUSTIFY_STRETCH_PER_SHRINK = 1.7;
|
|
8
|
+
const JUSTIFY_ALWAYS_SHRINK_STRETCH = 1.5;
|
|
9
|
+
const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
|
|
10
|
+
const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
|
|
11
|
+
const ALL_CAPS_RATIO_THRESHOLD = .8;
|
|
12
|
+
function uppercaseLetterRatio(text) {
|
|
13
|
+
let letters = 0;
|
|
14
|
+
let uppercase = 0;
|
|
15
|
+
for (const char of text) {
|
|
16
|
+
const lower = char.toLocaleLowerCase();
|
|
17
|
+
const upper = char.toLocaleUpperCase();
|
|
18
|
+
if (lower === upper) continue;
|
|
19
|
+
letters++;
|
|
20
|
+
if (char === upper) uppercase++;
|
|
21
|
+
}
|
|
22
|
+
return letters === 0 ? 0 : uppercase / letters;
|
|
23
|
+
}
|
|
24
|
+
function resolveJustifyFitStrategy(block, isFirstLine, profile) {
|
|
25
|
+
if (block.attrs?.justificationCompatibility?.type === "legacy") return { type: "rounding" };
|
|
26
|
+
const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
|
|
27
|
+
if (isFirstLine && profile.hasTabRuns && (block.attrs?.indent?.hanging ?? 0) > 0) return {
|
|
28
|
+
type: "width",
|
|
29
|
+
ratio: JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
30
|
+
};
|
|
31
|
+
if (!isFirstLine && profile.hasTabRuns && !hasTabStops) return {
|
|
32
|
+
type: "width",
|
|
33
|
+
ratio: JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO
|
|
34
|
+
};
|
|
35
|
+
if (profile.hasTabRuns && (isFirstLine || hasTabStops)) return {
|
|
36
|
+
type: "width",
|
|
37
|
+
ratio: (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
38
|
+
};
|
|
39
|
+
if (profile.uppercaseRatio > ALL_CAPS_RATIO_THRESHOLD) return {
|
|
40
|
+
type: "width",
|
|
41
|
+
ratio: JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
type: "space",
|
|
45
|
+
ratio: JUSTIFY_SPACE_CONTRACTION_RATIO
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function compressibleSpaceWidth(text, style) {
|
|
49
|
+
const count = countCompressibleSpaces(text);
|
|
50
|
+
return count === 0 ? 0 : count * measureTextWidth(" ", style);
|
|
51
|
+
}
|
|
52
|
+
function justifyFitTolerance(line, strategy, candidateSpaceWidth, candidate) {
|
|
53
|
+
if (strategy.type === "rounding") return WIDTH_TOLERANCE;
|
|
54
|
+
if (strategy.type === "width") return Math.max(WIDTH_TOLERANCE, line.availableWidth * strategy.ratio);
|
|
55
|
+
if (candidate !== void 0 && !prefersSpaceShrink(line, candidate)) return WIDTH_TOLERANCE;
|
|
56
|
+
return Math.max(WIDTH_TOLERANCE, (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A justified line whose next word overflows the measure has two outcomes:
|
|
60
|
+
* keep the word and shrink the line's spaces, or wrap it and stretch the
|
|
61
|
+
* spaces left on the shorter line. Shrinking wins only when the stretch it
|
|
62
|
+
* avoids is large compared with the shrink it costs: the shrink factor may
|
|
63
|
+
* not exceed `1 + (stretch - 1) / JUSTIFY_STRETCH_PER_SHRINK`, and a stretch
|
|
64
|
+
* beyond `JUSTIFY_ALWAYS_SHRINK_STRETCH` always prefers shrinking. The space
|
|
65
|
+
* before the wrapped word hangs at the line end, so it does not stretch; a
|
|
66
|
+
* line left with nothing to stretch defers to the shrink budget alone.
|
|
67
|
+
*/
|
|
68
|
+
function prefersSpaceShrink(line, candidate) {
|
|
69
|
+
const overflow = candidate.width - line.availableWidth;
|
|
70
|
+
if (overflow <= .5) return true;
|
|
71
|
+
const shrinkableSpaceWidth = line.regularSpaceWidth + candidate.innerSpaceWidth;
|
|
72
|
+
if (shrinkableSpaceWidth <= overflow) return false;
|
|
73
|
+
const hangingSpaceWidth = Math.min(line.trailingWhitespaceWidth, line.regularSpaceWidth);
|
|
74
|
+
const stretchableSpaceWidth = line.regularSpaceWidth - hangingSpaceWidth;
|
|
75
|
+
if (stretchableSpaceWidth <= 0) return true;
|
|
76
|
+
const visibleWidth = line.width - line.trailingWhitespaceWidth;
|
|
77
|
+
const stretch = 1 + Math.max(0, line.availableWidth - visibleWidth) / stretchableSpaceWidth;
|
|
78
|
+
if (stretch > JUSTIFY_ALWAYS_SHRINK_STRETCH) return true;
|
|
79
|
+
const shrink = shrinkableSpaceWidth / (shrinkableSpaceWidth - overflow);
|
|
80
|
+
return 1 + (stretch - 1) / JUSTIFY_STRETCH_PER_SHRINK >= shrink;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { compressibleSpaceWidth, justifyFitTolerance, resolveJustifyFitStrategy, uppercaseLetterRatio };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { MeasuredLine, ParagraphBlock, Run } from "../types.js";
|
|
2
|
+
import { FontStyle } from "./measureTypes.js";
|
|
3
|
+
import { LineBreakPolicy } from "./lineBreakProvider.js";
|
|
4
|
+
import { LineState } from "./paragraphMeasureShared.js";
|
|
5
|
+
//#region src/layout-engine/measure/lineFitting.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Find the longest prefix of `text` that fits within `maxWidth` pixels.
|
|
8
|
+
* Returns the number of characters that fit (at least 1 if `forceMin` is true).
|
|
9
|
+
*/
|
|
10
|
+
declare function findMaxFittingLength(text: string, style: FontStyle, maxWidth: number, forceMin?: boolean, policy?: LineBreakPolicy): number;
|
|
11
|
+
declare function exceedsHyphenationZone(line: LineState, zoneTwips: number): boolean;
|
|
12
|
+
declare const isIgnorableFinalTailRun: (run: Run) => boolean;
|
|
13
|
+
declare function isFinalTextCandidate(block: ParagraphBlock, runIndex: number, nextBreak: number): boolean;
|
|
14
|
+
type TextCandidateFit = {
|
|
15
|
+
type: "ordinary";
|
|
16
|
+
tolerancePx: number;
|
|
17
|
+
} | {
|
|
18
|
+
type: "final-contraction-admitted";
|
|
19
|
+
tolerancePx: number;
|
|
20
|
+
paint: NonNullable<MeasuredLine["justificationPaint"]>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Fit of a text candidate on a justified line: the paragraph's final word may
|
|
24
|
+
* be admitted by shrinking spaces, which then needs a paint plan.
|
|
25
|
+
*/
|
|
26
|
+
declare function resolveTextCandidateFit(isFinalCandidate: boolean, candidateWidth: number, availableWidth: number, tolerancePx: number): TextCandidateFit;
|
|
27
|
+
declare function trimTrailingSpacesAndTabs(text: string): string;
|
|
28
|
+
declare function measureWordWithTrailingWhitespace(word: string, style: FontStyle): {
|
|
29
|
+
measuredWord: string;
|
|
30
|
+
wordWidth: number;
|
|
31
|
+
fullWordWidth: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Character spacing still owed after the last character placed on the line,
|
|
35
|
+
* or 0 when the line has no preceding character at `charIndex` of `runIndex`.
|
|
36
|
+
* Within a run it is the run's own spacing; at a boundary between two text
|
|
37
|
+
* runs it is the preceding run's spacing on its final character.
|
|
38
|
+
*/
|
|
39
|
+
declare function precedingLetterSpacing({ block, line, runIndex, charIndex, runStyle }: {
|
|
40
|
+
block: ParagraphBlock;
|
|
41
|
+
line: LineState;
|
|
42
|
+
runIndex: number;
|
|
43
|
+
charIndex: number;
|
|
44
|
+
runStyle: FontStyle;
|
|
45
|
+
}): number;
|
|
46
|
+
declare function trailingHangingPunctuationWidth(text: string, style: FontStyle, policy: LineBreakPolicy, enabled: boolean): number;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { exceedsHyphenationZone, findMaxFittingLength, isFinalTextCandidate, isIgnorableFinalTailRun, measureWordWithTrailingWhitespace, precedingLetterSpacing, resolveTextCandidateFit, trailingHangingPunctuationWidth, trimTrailingSpacesAndTabs };
|