@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,22 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
|
|
1
|
+
import { isCjkFont } from "../../utils/fontResolver.js";
|
|
3
2
|
import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
|
|
4
|
-
import { hasCjk, hasComplexScript, hasEastAsiaSlotText } from "../../utils/scriptSegments.js";
|
|
5
|
-
import { splitTrailingToken } from "../../utils/trailingText.js";
|
|
6
|
-
import { JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO, supportsJustifiedFinalLineContraction } from "../justifiedLineFit.js";
|
|
7
3
|
import { measuredLineAdvance } from "../lineFlow.js";
|
|
8
4
|
import { isFloatingImageRun } from "../types.js";
|
|
9
5
|
import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
|
|
10
|
-
import {
|
|
6
|
+
import { collectCrossRunWord, computeFollowingTextLeads, computeProtectedCrossRunGlueWidths, computeTrailingGlueWidths, measureCrossRunPrefix } from "./crossRunWords.js";
|
|
11
7
|
import { resolveEffectiveLineBreakPolicy } from "./effectiveLineBreakPolicy.js";
|
|
8
|
+
import { MIN_WRAP_SEGMENT_WIDTH, findClearLineY } from "./floatingLineClearance.js";
|
|
12
9
|
import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
10
|
+
import { estimateMathFootprintPx, rotatedBlockImageHeight } from "./inlineObjectMetrics.js";
|
|
11
|
+
import { compressibleSpaceWidth, justifyFitTolerance, resolveJustifyFitStrategy, uppercaseLetterRatio } from "./justification.js";
|
|
12
|
+
import { findHyphenationBreaks, findWordBreaks, isBreakBetween, isBreakChar } from "./lineBreaks.js";
|
|
13
|
+
import { exceedsHyphenationZone, findMaxFittingLength, isFinalTextCandidate, isIgnorableFinalTailRun, measureWordWithTrailingWhitespace, precedingLetterSpacing, resolveTextCandidateFit, trailingHangingPunctuationWidth, trimTrailingSpacesAndTabs } from "./lineFitting.js";
|
|
14
|
+
import { applyDocumentGrid, calculateEmptyParagraphMetrics, calculateTypographyMetrics, cjkLineHeightStyle, complexScriptLineHeightStyle } from "./lineTypography.js";
|
|
15
15
|
import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
|
|
16
|
-
import { DEFAULT_FONT_FAMILY, buildRunFontStyle, ptToPx
|
|
16
|
+
import { DEFAULT_FONT_FAMILY, buildRunFontStyle, ptToPx } from "./measureHelpers.js";
|
|
17
17
|
import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
|
|
18
|
+
import { WIDTH_TOLERANCE, fieldMeasureText, isEmptyTextRun, isFieldRun, isImageRun, isLineBreakRun, isMathRun, isTabRun, isTextRun, runToFontStyle } from "./paragraphMeasureShared.js";
|
|
19
|
+
import { canClampTabToRightEdge, hasFollowingTabOnLine, hasPriorTabOnLine, measureDecimalPrefixWidthAfterTab, measureInlineWidthAfterTab } from "./paragraphTabs.js";
|
|
18
20
|
import { calculateTabWidth, pixelsToTwips } from "./tabCalculator.js";
|
|
19
|
-
import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
|
|
20
21
|
//#region src/layout-engine/measure/measureParagraph.ts
|
|
21
22
|
/**
|
|
22
23
|
* Paragraph measurement module
|
|
@@ -24,733 +25,6 @@ import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
|
|
|
24
25
|
* Measures paragraph blocks and computes line breaking.
|
|
25
26
|
* Converts runs into measured lines with typography metrics.
|
|
26
27
|
*/
|
|
27
|
-
const DEFAULT_FONT_SIZE = 11;
|
|
28
|
-
const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
|
|
29
|
-
const WIDTH_TOLERANCE = .5;
|
|
30
|
-
const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
|
|
31
|
-
const JUSTIFY_SPACE_CONTRACTION_RATIO = .25;
|
|
32
|
-
const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
|
|
33
|
-
const JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO = .022;
|
|
34
|
-
const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
|
|
35
|
-
const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
|
|
36
|
-
const DEFAULT_LIST_HANGING_INDENT_PX = 24;
|
|
37
|
-
const ALL_CAPS_RATIO_THRESHOLD = .8;
|
|
38
|
-
/**
|
|
39
|
-
* Find the longest prefix of `text` that fits within `maxWidth` pixels.
|
|
40
|
-
* Returns the number of characters that fit (at least 1 if `forceMin` is true).
|
|
41
|
-
*/
|
|
42
|
-
function findMaxFittingLength(text, style, maxWidth, forceMin = false, policy) {
|
|
43
|
-
const boundaries = findGraphemeBreaks(text, policy);
|
|
44
|
-
let lo = 0;
|
|
45
|
-
let hi = boundaries.length - 1;
|
|
46
|
-
let best = 0;
|
|
47
|
-
while (lo <= hi) {
|
|
48
|
-
const mid = Math.floor((lo + hi) / 2);
|
|
49
|
-
const boundary = boundaries[mid];
|
|
50
|
-
if (measureTextWidth(text.slice(0, boundary), style) <= maxWidth) {
|
|
51
|
-
best = boundary;
|
|
52
|
-
lo = mid + 1;
|
|
53
|
-
} else hi = mid - 1;
|
|
54
|
-
}
|
|
55
|
-
return forceMin && best === 0 ? boundaries.at(0) ?? 0 : best;
|
|
56
|
-
}
|
|
57
|
-
function exceedsHyphenationZone(line, zoneTwips) {
|
|
58
|
-
if (line.width <= 0) return true;
|
|
59
|
-
const visibleWidth = Math.max(0, line.width - line.trailingWhitespaceWidth);
|
|
60
|
-
return Math.max(0, line.availableWidth - visibleWidth) > twipsToPx(zoneTwips) + WIDTH_TOLERANCE;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Extract FontStyle from a run that carries RunFormatting (text, tab, or
|
|
64
|
-
* field). All three share the same formatting shape, so they measure the
|
|
65
|
-
* same way; widening the parameter keeps tab-following measurement
|
|
66
|
-
* (FieldRun page numbers, etc.) consistent with TextRun handling.
|
|
67
|
-
*/
|
|
68
|
-
function runToFontStyle(run) {
|
|
69
|
-
return buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Line-HEIGHT style for a text run. Word derives a CJK line's height from an
|
|
73
|
-
* East-Asian face, not the run's ascii font: real documents routinely put CJK
|
|
74
|
-
* glyphs in runs whose ascii/eastAsia fonts are Latin (e.g. `w:eastAsia`
|
|
75
|
-
* "Century"), and Word font-links those glyphs to the default East-Asian face
|
|
76
|
-
* and uses its taller single-line height (≈1.303 vs the ≈1.15 Latin default).
|
|
77
|
-
*
|
|
78
|
-
* When the run holds CJK text, swap `fontFamily` to the face whose
|
|
79
|
-
* `singleLineRatio` Word would actually use: the run's `w:eastAsia` font when
|
|
80
|
-
* it is a real CJK face, the run's own ascii font when THAT is a CJK face, or
|
|
81
|
-
* `CJK_FALLBACK_FONT_FAMILY` otherwise. Non-CJK runs return `baseStyle`
|
|
82
|
-
* unchanged, so Latin-only documents are unaffected by construction.
|
|
83
|
-
*
|
|
84
|
-
* The result feeds `updateMaxFont` (line height) ONLY — width measurement
|
|
85
|
-
* keeps the base style so wrapping is untouched.
|
|
86
|
-
*/
|
|
87
|
-
function cjkLineHeightStyle(run, baseStyle) {
|
|
88
|
-
if (!run.text) return baseStyle;
|
|
89
|
-
const eastAsia = baseStyle.eastAsiaFontFamily;
|
|
90
|
-
const eastAsiaFace = eastAsia !== void 0 && isCjkFont(eastAsia);
|
|
91
|
-
if (!hasCjk(run.text) && !(eastAsiaFace && hasEastAsiaSlotText(run.text, baseStyle.eastAsiaHint))) return baseStyle;
|
|
92
|
-
if (eastAsia !== void 0 && eastAsiaFace) {
|
|
93
|
-
const result = {
|
|
94
|
-
...baseStyle,
|
|
95
|
-
fontFamily: eastAsia
|
|
96
|
-
};
|
|
97
|
-
delete result.alternateFontFamily;
|
|
98
|
-
if (baseStyle.eastAsiaAlternateFontFamily !== void 0) result.alternateFontFamily = baseStyle.eastAsiaAlternateFontFamily;
|
|
99
|
-
return result;
|
|
100
|
-
}
|
|
101
|
-
if (isCjkFont(baseStyle.fontFamily ?? "Times New Roman")) return baseStyle;
|
|
102
|
-
return {
|
|
103
|
-
...baseStyle,
|
|
104
|
-
fontFamily: CJK_FALLBACK_FONT_FAMILY
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
/** Pick the dominant typography metrics for a run containing complex script. */
|
|
108
|
-
function complexScriptLineHeightStyle(run, baseStyle) {
|
|
109
|
-
if (!hasComplexScriptFormatting(run) || !run.forceComplexScript && !hasComplexScript(run.text)) return baseStyle;
|
|
110
|
-
const complexStyle = applyComplexScriptFormatting(baseStyle, run);
|
|
111
|
-
return (complexStyle.fontSize ?? DEFAULT_FONT_SIZE) >= (baseStyle.fontSize ?? DEFAULT_FONT_SIZE) ? complexStyle : baseStyle;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Calculate typography metrics from font size and spacing settings
|
|
115
|
-
*
|
|
116
|
-
* @param fontSize - Font size in points
|
|
117
|
-
* @param spacing - Paragraph spacing settings
|
|
118
|
-
* @param metrics - Pre-calculated font metrics (in pixels)
|
|
119
|
-
*/
|
|
120
|
-
function calculateTypographyMetrics(fontSize, spacing, metrics) {
|
|
121
|
-
const fontSizePx = ptToPx(fontSize);
|
|
122
|
-
const ascent = metrics?.ascent ?? fontSizePx * .8;
|
|
123
|
-
const descent = metrics?.descent ?? fontSizePx * .2;
|
|
124
|
-
const singleLineBase = fontSizePx * (metrics?.singleLineRatio ?? 1.15);
|
|
125
|
-
let lineHeight;
|
|
126
|
-
if (spacing?.lineRule === "exact" && spacing.line !== void 0) lineHeight = spacing.line;
|
|
127
|
-
else if (spacing?.lineRule === "atLeast" && spacing.line !== void 0) {
|
|
128
|
-
const defaultHeight = singleLineBase * DEFAULT_LINE_HEIGHT_MULTIPLIER;
|
|
129
|
-
lineHeight = Math.max(spacing.line, defaultHeight);
|
|
130
|
-
} else if (spacing?.line !== void 0 && spacing.lineUnit === "multiplier") lineHeight = singleLineBase * spacing.line;
|
|
131
|
-
else if (spacing?.line !== void 0 && spacing.lineUnit === "px") lineHeight = spacing.line;
|
|
132
|
-
else lineHeight = singleLineBase * DEFAULT_LINE_HEIGHT_MULTIPLIER;
|
|
133
|
-
return {
|
|
134
|
-
ascent,
|
|
135
|
-
descent,
|
|
136
|
-
lineHeight
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Word's "single line spacing" floor (≈ 1.15×) applied to empty paragraphs
|
|
141
|
-
* with `auto`/`atLeast` line rules. Without this, narrow-metric fonts
|
|
142
|
-
* (Arial Narrow, OS/2 ratio ≈ 1.117) collapse empty rows visibly tighter
|
|
143
|
-
* than Word renders them. See eigenpal #391/#394.
|
|
144
|
-
*/
|
|
145
|
-
const WORD_SINGLE_LINE_FLOOR = 1.15;
|
|
146
|
-
/**
|
|
147
|
-
* Calculate metrics for an empty paragraph.
|
|
148
|
-
*
|
|
149
|
-
* Word renders an empty paragraph as a single readable line — its line
|
|
150
|
-
* height never collapses below 1.15 × font size, even when the doc
|
|
151
|
-
* explicitly writes `<w:line w:val="240"/>` (1.0×). The floor is scoped to
|
|
152
|
-
* `auto`/`atLeast` line rules; `exact` means exact (per OOXML §17.3.1.33)
|
|
153
|
-
* and stays untouched.
|
|
154
|
-
*/
|
|
155
|
-
function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily, attrs) {
|
|
156
|
-
const result = calculateTypographyMetrics(fontSize, spacing, getFontMetrics({
|
|
157
|
-
fontSize,
|
|
158
|
-
fontFamily: fontFamily ?? "Times New Roman",
|
|
159
|
-
...attrs?.defaultAlternateFontFamily !== void 0 ? { alternateFontFamily: attrs.defaultAlternateFontFamily } : {}
|
|
160
|
-
}));
|
|
161
|
-
const lineRule = spacing?.lineRule ?? "auto";
|
|
162
|
-
if (lineRule === "auto" || lineRule === "atLeast") {
|
|
163
|
-
const floor = ptToPx(fontSize) * WORD_SINGLE_LINE_FLOOR;
|
|
164
|
-
if (result.lineHeight < floor) return applyDocumentGrid({
|
|
165
|
-
...result,
|
|
166
|
-
lineHeight: floor
|
|
167
|
-
}, attrs, spacing);
|
|
168
|
-
}
|
|
169
|
-
return applyDocumentGrid(result, attrs, spacing);
|
|
170
|
-
}
|
|
171
|
-
function applyDocumentGrid(typography, attrs, spacing) {
|
|
172
|
-
const pitch = attrs?.documentGridLinePitch;
|
|
173
|
-
if (pitch === void 0 || pitch <= 0 || attrs?.snapToGrid === false) return typography;
|
|
174
|
-
if (spacing?.lineRule === "exact") return typography;
|
|
175
|
-
const lineHeight = Math.ceil(typography.lineHeight / pitch - 1e-9) * pitch;
|
|
176
|
-
return lineHeight === typography.lineHeight ? typography : {
|
|
177
|
-
...typography,
|
|
178
|
-
lineHeight
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Check if a run is a text run
|
|
183
|
-
*/
|
|
184
|
-
function isTextRun(run) {
|
|
185
|
-
return run.kind === "text";
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Check if a run is a tab run
|
|
189
|
-
*/
|
|
190
|
-
function isTabRun(run) {
|
|
191
|
-
return run.kind === "tab";
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Check if a run is an image run
|
|
195
|
-
*/
|
|
196
|
-
function isImageRun(run) {
|
|
197
|
-
return run.kind === "image";
|
|
198
|
-
}
|
|
199
|
-
function parseRotationDegrees(transform) {
|
|
200
|
-
if (!transform) return 0;
|
|
201
|
-
const match = /rotate\(\s*(?<degrees>[-\d.]+)\s*deg\s*\)/u.exec(transform);
|
|
202
|
-
if (!match) return 0;
|
|
203
|
-
const raw = Number.parseFloat(match.groups["degrees"]);
|
|
204
|
-
if (!Number.isFinite(raw)) return 0;
|
|
205
|
-
return (raw % 360 + 360) % 360;
|
|
206
|
-
}
|
|
207
|
-
function rotatedBlockImageHeight(run) {
|
|
208
|
-
const deg = parseRotationDegrees(run.transform);
|
|
209
|
-
if (deg === 0 || deg === 180) return run.height;
|
|
210
|
-
if (deg === 90 || deg === 270) return run.width;
|
|
211
|
-
const rad = deg * Math.PI / 180;
|
|
212
|
-
const sinA = Math.abs(Math.sin(rad));
|
|
213
|
-
const cosA = Math.abs(Math.cos(rad));
|
|
214
|
-
return run.width * sinA + run.height * cosA;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Check if a run is a line break run
|
|
218
|
-
*/
|
|
219
|
-
function isLineBreakRun(run) {
|
|
220
|
-
return run.kind === "lineBreak";
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Check if a run is a field run
|
|
224
|
-
*/
|
|
225
|
-
function isFieldRun(run) {
|
|
226
|
-
return run.kind === "field";
|
|
227
|
-
}
|
|
228
|
-
/** Text a field run is measured at: its resolved value when known (so the
|
|
229
|
-
* measurer agrees with the painter), else the cached fallback. */
|
|
230
|
-
function fieldMeasureText(run, fieldValues) {
|
|
231
|
-
return (run.pmStart === void 0 ? void 0 : fieldValues?.get(run.pmStart)) ?? (run.fallback || "1");
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Check if a run is a math equation run
|
|
235
|
-
*/
|
|
236
|
-
function isMathRun(run) {
|
|
237
|
-
return run.kind === "math";
|
|
238
|
-
}
|
|
239
|
-
function isStackedMathLocalName(localName) {
|
|
240
|
-
switch (localName) {
|
|
241
|
-
case "bar":
|
|
242
|
-
case "d":
|
|
243
|
-
case "eqArr":
|
|
244
|
-
case "f":
|
|
245
|
-
case "groupChr":
|
|
246
|
-
case "limLow":
|
|
247
|
-
case "limUpp":
|
|
248
|
-
case "m":
|
|
249
|
-
case "nary":
|
|
250
|
-
case "rad":
|
|
251
|
-
case "sPre":
|
|
252
|
-
case "sSubSup": return true;
|
|
253
|
-
default: return false;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function hasStackedMathLayout(run) {
|
|
257
|
-
if (run.display === "block") return true;
|
|
258
|
-
for (const match of run.ommlXml.matchAll(/<(?<tag>[A-Za-z_][\w.-]*(?::[A-Za-z_][\w.-]*)?)(?:\s|\/|>)/gu)) {
|
|
259
|
-
const tagName = match.groups?.["tag"];
|
|
260
|
-
if (!tagName) continue;
|
|
261
|
-
const colonIndex = tagName.indexOf(":");
|
|
262
|
-
if (isStackedMathLocalName(colonIndex === -1 ? tagName : tagName.slice(colonIndex + 1))) return true;
|
|
263
|
-
}
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
function estimateMathFootprintPx(run) {
|
|
267
|
-
return ptToPx(run.fontSize ?? DEFAULT_FONT_SIZE) * (hasStackedMathLayout(run) ? 2.4 : 1.25);
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Check if text run is empty (only whitespace or no text)
|
|
271
|
-
*/
|
|
272
|
-
function isEmptyTextRun(run) {
|
|
273
|
-
return !run.text || run.text.replace(/\u00a0/gu, " ").trim().length === 0;
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Sum the inline pixel widths of runs after a tab, up to (but not including)
|
|
277
|
-
* the next tab or line break. Measured per-run so widths reserved match what
|
|
278
|
-
* the painter draws even when trailing runs use different fonts/sizes.
|
|
279
|
-
*
|
|
280
|
-
* Floating/anchored images are skipped — the painter lifts them out of the
|
|
281
|
-
* paragraph flow (see `isFloatingImageRun`) and `measureFollowingContentWidth`
|
|
282
|
-
* in the painter already excludes them, so counting their width here would
|
|
283
|
-
* desync measurer and painter on tab advance for paragraphs with a tab
|
|
284
|
-
* preceding a floating image.
|
|
285
|
-
*/
|
|
286
|
-
function isBlockLayoutImageRun(run) {
|
|
287
|
-
if (isFloatingImageRun(run)) return false;
|
|
288
|
-
return run.wrapType === "topAndBottom" || run.displayMode === "block";
|
|
289
|
-
}
|
|
290
|
-
function measureInlineWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
291
|
-
let width = 0;
|
|
292
|
-
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
293
|
-
const next = runs[i];
|
|
294
|
-
if (!next || isTabRun(next) || isLineBreakRun(next)) break;
|
|
295
|
-
if (isImageRun(next)) {
|
|
296
|
-
if (isBlockLayoutImageRun(next)) break;
|
|
297
|
-
if (!isFloatingImageRun(next)) width += inlineImageBoundingBox(next).width || 0;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
if (isTextRun(next)) width += measureTextWidth(next.text || "", runToFontStyle(next));
|
|
301
|
-
else if (isFieldRun(next)) width += measureTextWidth(fieldMeasureText(next, fieldValues), runToFontStyle(next));
|
|
302
|
-
else if (isMathRun(next)) width += measureTextWidth(next.plainText || "[equation]", runToFontStyle(next));
|
|
303
|
-
}
|
|
304
|
-
return width;
|
|
305
|
-
}
|
|
306
|
-
function hasFollowingTabOnLine(runs, tabIndex) {
|
|
307
|
-
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
308
|
-
const next = runs[i];
|
|
309
|
-
if (!next || isLineBreakRun(next)) break;
|
|
310
|
-
if (isImageRun(next) && isBlockLayoutImageRun(next)) break;
|
|
311
|
-
if (isTabRun(next)) return true;
|
|
312
|
-
}
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
function hasPriorTabOnLine(runs, tabIndex) {
|
|
316
|
-
for (let i = tabIndex - 1; i >= 0; i--) {
|
|
317
|
-
const prior = runs[i];
|
|
318
|
-
if (!prior || isLineBreakRun(prior) || isImageRun(prior) && isBlockLayoutImageRun(prior)) break;
|
|
319
|
-
if (isTabRun(prior)) return true;
|
|
320
|
-
}
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
function canClampTabToRightEdge(alignment, currentLineWidth, hasPriorTab, followingWidth, availableWidth) {
|
|
324
|
-
if (alignment === "start" || alignment === "default") return currentLineWidth > WIDTH_TOLERANCE && (hasPriorTab || followingWidth <= availableWidth);
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Width of the inline content preceding the first `.` in the runs that follow
|
|
329
|
-
* a tab, used to anchor `decimal` tab stops. Mirrors `getTextAfterTab` +
|
|
330
|
-
* decimal-prefix measurement in the painter (`renderParagraph.ts`) so the
|
|
331
|
-
* measurer and painter agree on tab advance for decimal stops.
|
|
332
|
-
*
|
|
333
|
-
* Returns 0 when no decimal separator appears before the next tab / line
|
|
334
|
-
* break — `calculateTabWidth` treats that as "no anchor adjustment".
|
|
335
|
-
*/
|
|
336
|
-
function measureDecimalPrefixWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
337
|
-
let text = "";
|
|
338
|
-
let firstRun;
|
|
339
|
-
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
340
|
-
const next = runs[i];
|
|
341
|
-
if (!next || isTabRun(next) || isLineBreakRun(next)) break;
|
|
342
|
-
if (isTextRun(next)) {
|
|
343
|
-
text += next.text || "";
|
|
344
|
-
firstRun ??= next;
|
|
345
|
-
} else if (isFieldRun(next)) {
|
|
346
|
-
text += fieldMeasureText(next, fieldValues);
|
|
347
|
-
firstRun ??= next;
|
|
348
|
-
} else if (isMathRun(next)) {
|
|
349
|
-
text += next.plainText || "[equation]";
|
|
350
|
-
firstRun ??= next;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
const decimalIndex = text.indexOf(".");
|
|
354
|
-
if (decimalIndex === -1 || !firstRun) return 0;
|
|
355
|
-
return measureTextWidth(text.slice(0, decimalIndex), runToFontStyle(firstRun));
|
|
356
|
-
}
|
|
357
|
-
function isSpaceOrTab(char) {
|
|
358
|
-
return char === " " || char === " ";
|
|
359
|
-
}
|
|
360
|
-
function uppercaseLetterRatio(text) {
|
|
361
|
-
let letters = 0;
|
|
362
|
-
let uppercase = 0;
|
|
363
|
-
for (const char of text) {
|
|
364
|
-
const lower = char.toLocaleLowerCase();
|
|
365
|
-
const upper = char.toLocaleUpperCase();
|
|
366
|
-
if (lower === upper) continue;
|
|
367
|
-
letters++;
|
|
368
|
-
if (char === upper) uppercase++;
|
|
369
|
-
}
|
|
370
|
-
return letters === 0 ? 0 : uppercase / letters;
|
|
371
|
-
}
|
|
372
|
-
function resolveJustifyFitStrategy(block, isFirstLine, profile) {
|
|
373
|
-
if (block.attrs?.justificationCompatibility?.type === "legacy") return { type: "rounding" };
|
|
374
|
-
if (isShallowFullHangingListContinuation(block, isFirstLine)) return { type: "rounding" };
|
|
375
|
-
if (block.attrs?.listMarker !== void 0) {
|
|
376
|
-
const hanging = block.attrs.indent?.hanging ?? 0;
|
|
377
|
-
if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ? {
|
|
378
|
-
type: "space",
|
|
379
|
-
ratio: JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO
|
|
380
|
-
} : {
|
|
381
|
-
type: "width",
|
|
382
|
-
ratio: JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO
|
|
383
|
-
};
|
|
384
|
-
return {
|
|
385
|
-
type: "space",
|
|
386
|
-
ratio: JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO,
|
|
387
|
-
maxWidthRatio: JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
|
|
391
|
-
if (isFirstLine && profile.hasTabRuns && (block.attrs?.indent?.hanging ?? 0) > 0) return {
|
|
392
|
-
type: "width",
|
|
393
|
-
ratio: JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
394
|
-
};
|
|
395
|
-
if (!isFirstLine && profile.hasTabRuns && !hasTabStops) return {
|
|
396
|
-
type: "width",
|
|
397
|
-
ratio: JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO
|
|
398
|
-
};
|
|
399
|
-
if (profile.hasTabRuns && (isFirstLine || hasTabStops)) return {
|
|
400
|
-
type: "width",
|
|
401
|
-
ratio: (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
402
|
-
};
|
|
403
|
-
if (profile.uppercaseRatio > ALL_CAPS_RATIO_THRESHOLD) return {
|
|
404
|
-
type: "width",
|
|
405
|
-
ratio: JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
406
|
-
};
|
|
407
|
-
return {
|
|
408
|
-
type: "space",
|
|
409
|
-
ratio: JUSTIFY_SPACE_CONTRACTION_RATIO
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
function resolveFinalLineJustifyFitStrategy(block, profile) {
|
|
413
|
-
if (supportsJustifiedFinalLineContraction(block)) return {
|
|
414
|
-
type: "space",
|
|
415
|
-
ratio: JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO,
|
|
416
|
-
maxWidthRatio: JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO
|
|
417
|
-
};
|
|
418
|
-
return resolveJustifyFitStrategy(block, false, profile);
|
|
419
|
-
}
|
|
420
|
-
function resolveCandidateJustifyFitStrategy({ isFirstLine, isFinalCandidate, firstLineStrategy, continuationStrategy, finalLineStrategy }) {
|
|
421
|
-
if (isFirstLine) return firstLineStrategy;
|
|
422
|
-
if (isFinalCandidate) return finalLineStrategy;
|
|
423
|
-
return continuationStrategy;
|
|
424
|
-
}
|
|
425
|
-
const isIgnorableFinalTailRun = (run) => run.kind === "renderedPageBreak" || isTextRun(run) && run.text.length === 0;
|
|
426
|
-
function isFinalTextCandidate(block, runIndex, nextBreak) {
|
|
427
|
-
const currentRun = block.runs[runIndex];
|
|
428
|
-
if (!currentRun || !isTextRun(currentRun) || nextBreak !== currentRun.text.length) return false;
|
|
429
|
-
for (let index = runIndex + 1; index < block.runs.length; index += 1) {
|
|
430
|
-
const run = block.runs[index];
|
|
431
|
-
if (run && isIgnorableFinalTailRun(run)) continue;
|
|
432
|
-
return false;
|
|
433
|
-
}
|
|
434
|
-
return true;
|
|
435
|
-
}
|
|
436
|
-
function resolveTextCandidateFit({ block, line, isFirstLine, isFinalCandidate, candidateWidth, candidateSpaceWidth, fallbackTolerancePx, continuationStrategy, finalStrategy }) {
|
|
437
|
-
if (!isFinalCandidate) return {
|
|
438
|
-
type: "ordinary",
|
|
439
|
-
tolerancePx: fallbackTolerancePx
|
|
440
|
-
};
|
|
441
|
-
if (!supportsJustifiedFinalLineContraction(block)) return admitFinalCandidate(candidateWidth, line.availableWidth, fallbackTolerancePx);
|
|
442
|
-
const ordinaryTolerancePx = isFirstLine ? fallbackTolerancePx : justifyFitTolerance(line, continuationStrategy, candidateSpaceWidth);
|
|
443
|
-
if (candidateWidth <= line.availableWidth + ordinaryTolerancePx) return admitFinalCandidate(candidateWidth, line.availableWidth, ordinaryTolerancePx);
|
|
444
|
-
const finalTolerancePx = justifyFitTolerance(line, finalStrategy, candidateSpaceWidth);
|
|
445
|
-
if (candidateWidth > line.availableWidth + finalTolerancePx) return {
|
|
446
|
-
type: "final-contraction-rejected",
|
|
447
|
-
tolerancePx: finalTolerancePx
|
|
448
|
-
};
|
|
449
|
-
return {
|
|
450
|
-
type: "final-contraction-admitted",
|
|
451
|
-
tolerancePx: finalTolerancePx,
|
|
452
|
-
paint: {
|
|
453
|
-
type: "space-contraction",
|
|
454
|
-
contractionPx: candidateWidth - line.availableWidth
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* A paragraph's last line is not stretched, so a final word admitted by space
|
|
460
|
-
* contraction needs an explicit paint plan; otherwise it would paint past the
|
|
461
|
-
* measure. Sub-pixel rounding overflow stays on the ordinary path.
|
|
462
|
-
*/
|
|
463
|
-
function admitFinalCandidate(candidateWidth, availableWidth, tolerancePx) {
|
|
464
|
-
const contractionPx = candidateWidth - availableWidth;
|
|
465
|
-
if (contractionPx <= WIDTH_TOLERANCE || contractionPx > tolerancePx) return {
|
|
466
|
-
type: "ordinary",
|
|
467
|
-
tolerancePx
|
|
468
|
-
};
|
|
469
|
-
return {
|
|
470
|
-
type: "final-contraction-admitted",
|
|
471
|
-
tolerancePx,
|
|
472
|
-
paint: {
|
|
473
|
-
type: "space-contraction",
|
|
474
|
-
contractionPx
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
function compressibleSpaceWidth(text, style) {
|
|
479
|
-
const count = countCompressibleSpaces(text);
|
|
480
|
-
return count === 0 ? 0 : count * measureTextWidth(" ", style);
|
|
481
|
-
}
|
|
482
|
-
function justifyFitTolerance(line, strategy, candidateSpaceWidth) {
|
|
483
|
-
if (strategy.type === "rounding") return WIDTH_TOLERANCE;
|
|
484
|
-
if (strategy.type === "width") return Math.max(WIDTH_TOLERANCE, line.availableWidth * strategy.ratio);
|
|
485
|
-
const spaceTolerance = (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio;
|
|
486
|
-
const boundedTolerance = strategy.maxWidthRatio === void 0 ? spaceTolerance : Math.min(spaceTolerance, line.availableWidth * strategy.maxWidthRatio);
|
|
487
|
-
return Math.max(WIDTH_TOLERANCE, boundedTolerance);
|
|
488
|
-
}
|
|
489
|
-
function isShallowFullHangingListContinuation(block, isFirstLine) {
|
|
490
|
-
if (isFirstLine || block.attrs?.listMarker === void 0) return false;
|
|
491
|
-
const hanging = block.attrs.indent?.hanging ?? 0;
|
|
492
|
-
const left = block.attrs.indent?.left ?? 0;
|
|
493
|
-
return hanging > 0 && hanging < DEFAULT_LIST_HANGING_INDENT_PX && Math.abs(left - hanging) <= WIDTH_TOLERANCE;
|
|
494
|
-
}
|
|
495
|
-
function trimTrailingSpacesAndTabs(text) {
|
|
496
|
-
let end = text.length;
|
|
497
|
-
while (end > 0) {
|
|
498
|
-
const char = text[end - 1];
|
|
499
|
-
if (char !== " " && char !== " ") break;
|
|
500
|
-
end--;
|
|
501
|
-
}
|
|
502
|
-
return text.slice(0, end);
|
|
503
|
-
}
|
|
504
|
-
function measureWordWithTrailingWhitespace(word, style) {
|
|
505
|
-
const measuredWord = trimTrailingSpacesAndTabs(word);
|
|
506
|
-
const wordWidth = measureTextWidth(measuredWord, style);
|
|
507
|
-
const trailingWhitespace = word.slice(measuredWord.length);
|
|
508
|
-
if (trailingWhitespace.length === 0) return {
|
|
509
|
-
measuredWord,
|
|
510
|
-
wordWidth,
|
|
511
|
-
fullWordWidth: wordWidth
|
|
512
|
-
};
|
|
513
|
-
if (style.kerning || trailingWhitespace.includes(" ")) return {
|
|
514
|
-
measuredWord,
|
|
515
|
-
wordWidth,
|
|
516
|
-
fullWordWidth: measureTextWidth(word, style)
|
|
517
|
-
};
|
|
518
|
-
const trailingWhitespaceWidth = measureTextWidth(trailingWhitespace, style);
|
|
519
|
-
const boundarySpacing = measuredWord.length > 0 ? (style.letterSpacing ?? 0) * getHorizontalScaleFactor(style.horizontalScale) : 0;
|
|
520
|
-
return {
|
|
521
|
-
measuredWord,
|
|
522
|
-
wordWidth,
|
|
523
|
-
fullWordWidth: wordWidth + trailingWhitespaceWidth + boundarySpacing
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
const scaledLetterSpacing = (style) => (style.letterSpacing ?? 0) * getHorizontalScaleFactor(style.horizontalScale);
|
|
527
|
-
/**
|
|
528
|
-
* Character spacing still owed after the last character placed on the line,
|
|
529
|
-
* or 0 when the line has no preceding character at `charIndex` of `runIndex`.
|
|
530
|
-
* Within a run it is the run's own spacing; at a boundary between two text
|
|
531
|
-
* runs it is the preceding run's spacing on its final character.
|
|
532
|
-
*/
|
|
533
|
-
function precedingLetterSpacing({ block, line, runIndex, charIndex, runStyle }) {
|
|
534
|
-
if (line.fromRun === line.toRun && line.fromChar === line.toChar) return 0;
|
|
535
|
-
if (line.toRun === runIndex && line.toChar === charIndex) return scaledLetterSpacing(runStyle);
|
|
536
|
-
if (charIndex !== 0 || line.toRun !== runIndex - 1) return 0;
|
|
537
|
-
const previousRun = block.runs[runIndex - 1];
|
|
538
|
-
if (!previousRun || !isTextRun(previousRun) || previousRun.text.length === 0 || line.toChar !== previousRun.text.length) return 0;
|
|
539
|
-
return scaledLetterSpacing(runToFontStyle(previousRun));
|
|
540
|
-
}
|
|
541
|
-
function trailingCodePoint(text) {
|
|
542
|
-
if (text.length === 0) return;
|
|
543
|
-
const trailing = text.charCodeAt(text.length - 1);
|
|
544
|
-
const preceding = text.charCodeAt(text.length - 2);
|
|
545
|
-
const start = trailing >= 56320 && trailing <= 57343 && preceding >= 55296 && preceding <= 56319 ? text.length - 2 : text.length - 1;
|
|
546
|
-
return text.slice(start);
|
|
547
|
-
}
|
|
548
|
-
const usesDefaultHangingPunctuationClassifier = () => {
|
|
549
|
-
return (getLineBreakProvider().isHangingPunctuation ?? defaultLineBreakProvider.isHangingPunctuation) === defaultLineBreakProvider.isHangingPunctuation;
|
|
550
|
-
};
|
|
551
|
-
function trailingHangingPunctuationWidth(text, style, policy, enabled) {
|
|
552
|
-
if (!enabled || text.length === 0) return 0;
|
|
553
|
-
const trailing = trailingCodePoint(text);
|
|
554
|
-
if (usesDefaultHangingPunctuationClassifier() && !isHangingPunctuation(trailing ?? "", policy)) return 0;
|
|
555
|
-
const lastStart = findGraphemeBreaks(text, policy).at(-2) ?? 0;
|
|
556
|
-
const lastGrapheme = text.slice(lastStart);
|
|
557
|
-
if (!isHangingPunctuation(lastGrapheme, policy)) return 0;
|
|
558
|
-
return measureTextWidth(lastGrapheme, style);
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Width of the unbreakable text glued to the end of each run.
|
|
562
|
-
* A run boundary is not itself a wrap opportunity: adjacent note markers and
|
|
563
|
-
* format-only word splits must wrap as one cluster.
|
|
564
|
-
*/
|
|
565
|
-
function computeTrailingGlueWidths(block) {
|
|
566
|
-
const runs = block.runs;
|
|
567
|
-
const widths = Array.from({ length: runs.length }, () => 0);
|
|
568
|
-
for (let index = runs.length - 1; index >= 0; index--) {
|
|
569
|
-
const nextRun = runs[index + 1];
|
|
570
|
-
if (!nextRun || !isTextRun(nextRun)) continue;
|
|
571
|
-
const text = nextRun.text;
|
|
572
|
-
if (!text) {
|
|
573
|
-
widths[index] = widths[index + 1] ?? 0;
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
if (isSpaceOrTab(text.at(0))) continue;
|
|
577
|
-
const style = runToFontStyle(nextRun);
|
|
578
|
-
const firstBreak = findWordBreaks(text, resolveEffectiveLineBreakPolicy({
|
|
579
|
-
attrs: block.attrs,
|
|
580
|
-
run: nextRun
|
|
581
|
-
}).provider).at(0);
|
|
582
|
-
widths[index] = measureTextWidth(firstBreak === void 0 ? text : trimTrailingSpacesAndTabs(text.slice(0, firstBreak)), style) + (firstBreak === void 0 ? widths[index + 1] ?? 0 : 0);
|
|
583
|
-
}
|
|
584
|
-
return widths;
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* First character of the text that follows each run, looking through empty
|
|
588
|
-
* text runs; undefined before a non-text run or at the paragraph end.
|
|
589
|
-
*/
|
|
590
|
-
function computeFollowingTextLeads(block) {
|
|
591
|
-
const leads = Array.from({ length: block.runs.length }, () => void 0);
|
|
592
|
-
let lead;
|
|
593
|
-
for (let index = block.runs.length - 1; index >= 0; index--) {
|
|
594
|
-
leads[index] = lead;
|
|
595
|
-
const run = block.runs[index];
|
|
596
|
-
if (!run || !isTextRun(run)) lead = void 0;
|
|
597
|
-
else if (run.text) lead = run.text[0];
|
|
598
|
-
}
|
|
599
|
-
return leads;
|
|
600
|
-
}
|
|
601
|
-
function computeProtectedCrossRunGlueWidths(block) {
|
|
602
|
-
const widths = Array.from({ length: block.runs.length }, () => 0);
|
|
603
|
-
for (let index = 0; index < block.runs.length; index++) {
|
|
604
|
-
const run = block.runs[index];
|
|
605
|
-
if (!run || !isTextRun(run)) continue;
|
|
606
|
-
const policy = resolveEffectiveLineBreakPolicy({
|
|
607
|
-
attrs: block.attrs,
|
|
608
|
-
run
|
|
609
|
-
}).provider;
|
|
610
|
-
if (!policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
|
|
611
|
-
const trailing = splitTrailingToken(run.text ?? "");
|
|
612
|
-
if (!trailing || trailing.token.length !== 1) continue;
|
|
613
|
-
const { token } = trailing;
|
|
614
|
-
let separator = trailing.separator;
|
|
615
|
-
let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
|
|
616
|
-
let followingWord = "";
|
|
617
|
-
let unseparatedFollowingText = false;
|
|
618
|
-
for (let nextIndex = index + 1; nextIndex < block.runs.length; nextIndex++) {
|
|
619
|
-
const nextRun = block.runs[nextIndex];
|
|
620
|
-
if (!nextRun || !isTextRun(nextRun)) break;
|
|
621
|
-
const text = nextRun.text ?? "";
|
|
622
|
-
let consumed = "";
|
|
623
|
-
for (const char of text) {
|
|
624
|
-
const isWhitespace = /\s/u.test(char);
|
|
625
|
-
if (followingWord.length > 0 && isWhitespace) break;
|
|
626
|
-
if (separator.length === 0 && !isWhitespace) {
|
|
627
|
-
unseparatedFollowingText = true;
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
consumed += char;
|
|
631
|
-
if (isWhitespace) separator += char;
|
|
632
|
-
else followingWord += char;
|
|
633
|
-
}
|
|
634
|
-
if (consumed.length > 0) glueWidth += measureTextWidth(consumed, runToFontStyle(nextRun));
|
|
635
|
-
if (unseparatedFollowingText) break;
|
|
636
|
-
if (followingWord.length > 0 && consumed.length < text.length) break;
|
|
637
|
-
}
|
|
638
|
-
if (unseparatedFollowingText || separator.length === 0 || followingWord.length === 0) continue;
|
|
639
|
-
const boundary = token.length + separator.length;
|
|
640
|
-
if (!findWordBreaks(token + separator + followingWord, policy).includes(boundary)) widths[index] = glueWidth;
|
|
641
|
-
}
|
|
642
|
-
return widths;
|
|
643
|
-
}
|
|
644
|
-
/**
|
|
645
|
-
* Cap on the number of runs `collectCrossRunWord` traverses per call. A
|
|
646
|
-
* paragraph split into many tiny (including empty) runs would otherwise let
|
|
647
|
-
* the search walk the full run list for every word-start position it is
|
|
648
|
-
* invoked from, without the per-word text-length budget ever kicking in.
|
|
649
|
-
*/
|
|
650
|
-
const MAX_CROSS_RUN_SEGMENTS = 64;
|
|
651
|
-
/** Collect one lexical word continued through adjacent formatting runs. */
|
|
652
|
-
function collectCrossRunWord({ block, startRunIndex, startChar }) {
|
|
653
|
-
const segments = [];
|
|
654
|
-
let text = "";
|
|
655
|
-
let width = 0;
|
|
656
|
-
for (let runIndex = startRunIndex; runIndex < block.runs.length; runIndex++) {
|
|
657
|
-
if (runIndex - startRunIndex >= MAX_CROSS_RUN_SEGMENTS) return;
|
|
658
|
-
const run = block.runs[runIndex];
|
|
659
|
-
if (!run || !isTextRun(run)) break;
|
|
660
|
-
const start = runIndex === startRunIndex ? startChar : 0;
|
|
661
|
-
const remainder = run.text.slice(start);
|
|
662
|
-
if (remainder.length === 0) continue;
|
|
663
|
-
if (runIndex !== startRunIndex && isSpaceOrTab(remainder[0])) break;
|
|
664
|
-
const remainingBudget = 256 - text.length;
|
|
665
|
-
const boundedRemainder = remainder.slice(0, remainingBudget + 1);
|
|
666
|
-
const firstBreak = findWordBreaks(boundedRemainder, resolveEffectiveLineBreakPolicy({
|
|
667
|
-
attrs: block.attrs,
|
|
668
|
-
run
|
|
669
|
-
}).provider).at(0);
|
|
670
|
-
const segmentText = trimTrailingSpacesAndTabs(firstBreak === void 0 ? boundedRemainder : boundedRemainder.slice(0, firstBreak));
|
|
671
|
-
if (segmentText.length === 0) break;
|
|
672
|
-
if (segmentText.length > remainingBudget) return;
|
|
673
|
-
const style = runToFontStyle(run);
|
|
674
|
-
segments.push({
|
|
675
|
-
runIndex,
|
|
676
|
-
startChar: start,
|
|
677
|
-
text: segmentText,
|
|
678
|
-
run,
|
|
679
|
-
style
|
|
680
|
-
});
|
|
681
|
-
text += segmentText;
|
|
682
|
-
width += measureTextWidth(segmentText, style);
|
|
683
|
-
if (firstBreak !== void 0) break;
|
|
684
|
-
}
|
|
685
|
-
if (segments.length < 2) return;
|
|
686
|
-
const firstRun = segments[0]?.run;
|
|
687
|
-
if (!firstRun) return;
|
|
688
|
-
return {
|
|
689
|
-
text,
|
|
690
|
-
width,
|
|
691
|
-
breaks: findHyphenationBreaks(text, resolveEffectiveLineBreakPolicy({
|
|
692
|
-
attrs: block.attrs,
|
|
693
|
-
run: firstRun
|
|
694
|
-
}).provider),
|
|
695
|
-
segments
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
function measureCrossRunPrefix(word, breakOffset) {
|
|
699
|
-
let width = 0;
|
|
700
|
-
let remaining = breakOffset;
|
|
701
|
-
const measuredSegments = [];
|
|
702
|
-
for (const segment of word.segments) {
|
|
703
|
-
const length = Math.min(remaining, segment.text.length);
|
|
704
|
-
if (length > 0) {
|
|
705
|
-
width += measureTextWidth(segment.text.slice(0, length), segment.style);
|
|
706
|
-
measuredSegments.push(segment);
|
|
707
|
-
}
|
|
708
|
-
if (remaining <= segment.text.length) return {
|
|
709
|
-
width,
|
|
710
|
-
endRunIndex: segment.runIndex,
|
|
711
|
-
endChar: segment.startChar + remaining,
|
|
712
|
-
hyphenStyle: segment.style,
|
|
713
|
-
segments: measuredSegments
|
|
714
|
-
};
|
|
715
|
-
remaining -= segment.text.length;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Minimum horizontal room a line must offer before we treat it as usable for
|
|
720
|
-
* body text. Below this threshold the line is bumped past obstructing floats
|
|
721
|
-
* via `findClearLineY` instead of being rendered into the unusable sliver.
|
|
722
|
-
* Without this guard, a near-full-width float (e.g. floating table) produces
|
|
723
|
-
* a ~2px segment that collapses every wrap line to one glyph per row.
|
|
724
|
-
*/
|
|
725
|
-
const MIN_WRAP_SEGMENT_WIDTH = 24;
|
|
726
|
-
/**
|
|
727
|
-
* Find the next vertical position at or below `startY` where the available
|
|
728
|
-
* text width is at least `minWidth`. Used to skip lines past stacked floats
|
|
729
|
-
* when there is no horizontal room for meaningful text at the current Y.
|
|
730
|
-
*
|
|
731
|
-
* Returns `startY` if the current position already has enough room, otherwise
|
|
732
|
-
* the lowest `bottomY` of any zone currently obstructing the line. The caller
|
|
733
|
-
* is expected to re-query margins at the returned Y.
|
|
734
|
-
*
|
|
735
|
-
* Coordinates are absolute (i.e., already include any paragraphYOffset).
|
|
736
|
-
*/
|
|
737
|
-
function findClearLineY(startY, lineHeight, zones, contentWidth, minWidth) {
|
|
738
|
-
if (!zones || zones.length === 0) return startY;
|
|
739
|
-
let y = startY;
|
|
740
|
-
for (let i = 0; i < zones.length + 2; i++) {
|
|
741
|
-
const margins = getFloatingMargins(y, lineHeight, zones, 0);
|
|
742
|
-
if (Math.max(0, getFloatingAvailableWidth(margins, contentWidth)) >= minWidth) return y;
|
|
743
|
-
const lineBottom = y + lineHeight;
|
|
744
|
-
let nextY = Number.POSITIVE_INFINITY;
|
|
745
|
-
for (const zone of zones) {
|
|
746
|
-
if (lineBottom <= zone.topY || y >= zone.bottomY) continue;
|
|
747
|
-
if (zone.bottomY > y && zone.bottomY < nextY) nextY = zone.bottomY;
|
|
748
|
-
}
|
|
749
|
-
if (!Number.isFinite(nextY) || nextY <= y) return y;
|
|
750
|
-
y = nextY;
|
|
751
|
-
}
|
|
752
|
-
return y;
|
|
753
|
-
}
|
|
754
28
|
/**
|
|
755
29
|
* Measure a paragraph block and compute line breaks
|
|
756
30
|
*
|
|
@@ -770,7 +44,6 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
770
44
|
};
|
|
771
45
|
const firstLineJustifyFitStrategy = resolveJustifyFitStrategy(block, true, justificationProfile);
|
|
772
46
|
const continuationJustifyFitStrategy = resolveJustifyFitStrategy(block, false, justificationProfile);
|
|
773
|
-
const finalLineJustifyFitStrategy = resolveFinalLineJustifyFitStrategy(block, justificationProfile);
|
|
774
47
|
const floatingZones = options?.floatingZones;
|
|
775
48
|
const paragraphYOffset = options?.paragraphYOffset ?? 0;
|
|
776
49
|
const indent = attrs?.indent;
|
|
@@ -796,7 +69,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
796
69
|
pendingFloatSkip += skip;
|
|
797
70
|
}
|
|
798
71
|
};
|
|
799
|
-
const estimatedFirstLineHeight = ptToPx(
|
|
72
|
+
const estimatedFirstLineHeight = ptToPx(11) * 1;
|
|
800
73
|
skipObstructingFloats(estimatedFirstLineHeight, baseFirstLineWidth);
|
|
801
74
|
const firstLineFloatingMargins = getFloatingMargins(cumulativeHeight, estimatedFirstLineHeight, floatingZones, paragraphYOffset);
|
|
802
75
|
const firstLineWidth = Math.max(1, getFloatingAvailableWidth(firstLineFloatingMargins, baseFirstLineWidth));
|
|
@@ -822,7 +95,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
822
95
|
};
|
|
823
96
|
}
|
|
824
97
|
if (runs.length === 0) {
|
|
825
|
-
const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ??
|
|
98
|
+
const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? 11, spacing, attrs?.defaultFontFamily ?? "Times New Roman", attrs);
|
|
826
99
|
const outlineLineHeight = attrs?.reserveEmptyOutlineHeight ? emptyMetrics.lineHeight * 2 : emptyMetrics.lineHeight;
|
|
827
100
|
lines.push({
|
|
828
101
|
fromRun: 0,
|
|
@@ -843,7 +116,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
843
116
|
};
|
|
844
117
|
}
|
|
845
118
|
if (runs.length > 0 && runs.every((run) => isTextRun(run) && isEmptyTextRun(run))) {
|
|
846
|
-
const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ??
|
|
119
|
+
const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? 11, spacing, attrs?.defaultFontFamily ?? "Times New Roman", attrs);
|
|
847
120
|
lines.push({
|
|
848
121
|
fromRun: 0,
|
|
849
122
|
fromChar: 0,
|
|
@@ -872,9 +145,9 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
872
145
|
width: 0,
|
|
873
146
|
trailingWhitespaceWidth: 0,
|
|
874
147
|
regularSpaceWidth: 0,
|
|
875
|
-
maxFontSize:
|
|
148
|
+
maxFontSize: 11,
|
|
876
149
|
maxFontMetrics: null,
|
|
877
|
-
maxTabFontSize:
|
|
150
|
+
maxTabFontSize: 11,
|
|
878
151
|
maxTabFontMetrics: null,
|
|
879
152
|
maxImageHeightPx: 0,
|
|
880
153
|
maxExactImageHeightPx: 0,
|
|
@@ -885,7 +158,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
885
158
|
...firstLineFloatingMargins.segments?.length ? { segmentZones: firstLineFloatingMargins.segments } : {}
|
|
886
159
|
};
|
|
887
160
|
const calculateLineTypography = (line) => {
|
|
888
|
-
const paragraphFontSize = attrs?.defaultFontSize ??
|
|
161
|
+
const paragraphFontSize = attrs?.defaultFontSize ?? 11;
|
|
889
162
|
const paragraphFontFamily = attrs?.defaultFontFamily ?? "Times New Roman";
|
|
890
163
|
let lineFontMetrics = line.maxFontMetrics;
|
|
891
164
|
let fontSize = line.maxFontSize;
|
|
@@ -925,7 +198,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
925
198
|
if (!floatingZones || floatingZones.length === 0) return bodyContentWidth;
|
|
926
199
|
const lineTypography = calculateLineTypography(currentLine);
|
|
927
200
|
let nextCumulativeHeight = cumulativeHeight + lineTypography.lineHeight;
|
|
928
|
-
const estimatedLineHeight = ptToPx(
|
|
201
|
+
const estimatedLineHeight = ptToPx(11) * 1;
|
|
929
202
|
const absoluteY = paragraphYOffset + nextCumulativeHeight;
|
|
930
203
|
const skip = findClearLineY(absoluteY, estimatedLineHeight, floatingZones, bodyContentWidth, 24) - absoluteY;
|
|
931
204
|
if (skip > 0) nextCumulativeHeight += skip;
|
|
@@ -963,10 +236,9 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
963
236
|
*/
|
|
964
237
|
const startNewLine = (runIndex, charIndex) => {
|
|
965
238
|
finalizeLine();
|
|
966
|
-
const estimatedLineHeight = ptToPx(
|
|
239
|
+
const estimatedLineHeight = ptToPx(11) * 1;
|
|
967
240
|
skipObstructingFloats(estimatedLineHeight, bodyContentWidth);
|
|
968
241
|
const floatingMargins = getFloatingMargins(cumulativeHeight, estimatedLineHeight, floatingZones, paragraphYOffset);
|
|
969
|
-
const adjustedWidth = Math.max(1, getFloatingAvailableWidth(floatingMargins, bodyContentWidth));
|
|
970
242
|
currentLine = {
|
|
971
243
|
fromRun: runIndex,
|
|
972
244
|
fromChar: charIndex,
|
|
@@ -975,14 +247,14 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
975
247
|
width: 0,
|
|
976
248
|
trailingWhitespaceWidth: 0,
|
|
977
249
|
regularSpaceWidth: 0,
|
|
978
|
-
maxFontSize:
|
|
250
|
+
maxFontSize: 11,
|
|
979
251
|
maxFontMetrics: null,
|
|
980
|
-
maxTabFontSize:
|
|
252
|
+
maxTabFontSize: 11,
|
|
981
253
|
maxTabFontMetrics: null,
|
|
982
254
|
maxImageHeightPx: 0,
|
|
983
255
|
maxExactImageHeightPx: 0,
|
|
984
256
|
maxMathHeightPx: 0,
|
|
985
|
-
availableWidth:
|
|
257
|
+
availableWidth: Math.max(1, getFloatingAvailableWidth(floatingMargins, bodyContentWidth)),
|
|
986
258
|
leftOffset: floatingMargins.leftMargin,
|
|
987
259
|
rightOffset: floatingMargins.rightMargin,
|
|
988
260
|
...floatingMargins.segments?.length ? { segmentZones: floatingMargins.segments } : {}
|
|
@@ -992,7 +264,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
992
264
|
* Update max font tracking for the current line
|
|
993
265
|
*/
|
|
994
266
|
const updateMaxFont = (style) => {
|
|
995
|
-
const fontSize = style.fontSize ??
|
|
267
|
+
const fontSize = style.fontSize ?? 11;
|
|
996
268
|
if (!currentLine.maxFontMetrics || fontSize > currentLine.maxFontSize) {
|
|
997
269
|
currentLine.maxFontSize = fontSize;
|
|
998
270
|
currentLine.maxFontMetrics = getFontMetrics(style);
|
|
@@ -1009,7 +281,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
1009
281
|
* the paragraph-mark size) leaves the line at the text's height.
|
|
1010
282
|
*/
|
|
1011
283
|
const updateMaxTabFont = (style) => {
|
|
1012
|
-
const fontSize = style.fontSize ??
|
|
284
|
+
const fontSize = style.fontSize ?? 11;
|
|
1013
285
|
if (!currentLine.maxTabFontMetrics || fontSize > currentLine.maxTabFontSize) {
|
|
1014
286
|
currentLine.maxTabFontSize = fontSize;
|
|
1015
287
|
currentLine.maxTabFontMetrics = getFontMetrics(style);
|
|
@@ -1060,12 +332,12 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
1060
332
|
let tabWidth = tabResult.width;
|
|
1061
333
|
const authoredEndpoint = contentX + tabWidth + followingWidth;
|
|
1062
334
|
const activeContentRightEdge = maxWidth - currentLine.rightOffset;
|
|
1063
|
-
const preservesAuthoredStop = tabResult.alignment === "end" && options?.allowEndTabOverflow === true || tabResult.explicit === true && authoredEndpoint <= activeContentRightEdge +
|
|
1064
|
-
const landsOnLeftIndent = tabResult.alignment === "start" && indentLeft > 0 && Math.abs(contentX + tabWidth - indentLeft) <=
|
|
335
|
+
const preservesAuthoredStop = tabResult.alignment === "end" && options?.allowEndTabOverflow === true || tabResult.explicit === true && authoredEndpoint <= activeContentRightEdge + .5;
|
|
336
|
+
const landsOnLeftIndent = tabResult.alignment === "start" && indentLeft > 0 && Math.abs(contentX + tabWidth - indentLeft) <= .5;
|
|
1065
337
|
const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
|
|
1066
|
-
if (!preservesAuthoredStop && !landsOnLeftIndent && !hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX +
|
|
338
|
+
if (!preservesAuthoredStop && !landsOnLeftIndent && !hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + .5) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
|
|
1067
339
|
if (preservesAuthoredStop) currentLine.availableWidth = Math.max(currentLine.availableWidth, currentLine.width + tabWidth + followingWidth);
|
|
1068
|
-
if (currentLine.width + tabWidth > currentLine.availableWidth +
|
|
340
|
+
if (currentLine.width + tabWidth > currentLine.availableWidth + .5) {
|
|
1069
341
|
startNewLine(runIndex, 0);
|
|
1070
342
|
updateMaxTabFont(style);
|
|
1071
343
|
}
|
|
@@ -1096,7 +368,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
1096
368
|
const inlineBbox = inlineImageBoundingBox(run);
|
|
1097
369
|
const imageWidth = inlineBbox.width;
|
|
1098
370
|
const imageHeight = inlineBbox.height;
|
|
1099
|
-
if (currentLine.width > 0 && currentLine.width + imageWidth > currentLine.availableWidth +
|
|
371
|
+
if (currentLine.width > 0 && currentLine.width + imageWidth > currentLine.availableWidth + .5) startNewLine(runIndex, 0);
|
|
1100
372
|
const imageFootprintPx = imageHeight + (run.distTop ?? 0) + (run.distBottom ?? 0);
|
|
1101
373
|
if (imageFootprintPx > currentLine.maxImageHeightPx) currentLine.maxImageHeightPx = imageFootprintPx;
|
|
1102
374
|
if (run.exactLineHeight === true && imageFootprintPx > currentLine.maxExactImageHeightPx) currentLine.maxExactImageHeightPx = imageFootprintPx;
|
|
@@ -1108,11 +380,14 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
1108
380
|
}
|
|
1109
381
|
if (isFieldRun(run)) {
|
|
1110
382
|
const fallback = fieldMeasureText(run, options?.fieldValues);
|
|
1111
|
-
const style = buildRunFontStyle(run, DEFAULT_FONT_FAMILY,
|
|
383
|
+
const style = buildRunFontStyle(run, DEFAULT_FONT_FAMILY, 11);
|
|
1112
384
|
updateMaxFont(style);
|
|
1113
385
|
const fieldWidth = measureTextWidth(fallback, style);
|
|
1114
386
|
const fieldSpaceWidth = compressibleSpaceWidth(fallback, style);
|
|
1115
|
-
const fieldTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, lines.length === 0 ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, fieldSpaceWidth
|
|
387
|
+
const fieldTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, lines.length === 0 ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, fieldSpaceWidth, {
|
|
388
|
+
width: currentLine.width + fieldWidth,
|
|
389
|
+
innerSpaceWidth: fieldSpaceWidth
|
|
390
|
+
}) : WIDTH_TOLERANCE;
|
|
1116
391
|
if (currentLine.width > 0 && currentLine.width + fieldWidth > currentLine.availableWidth + fieldTolerance) {
|
|
1117
392
|
startNewLine(runIndex, 0);
|
|
1118
393
|
updateMaxFont(style);
|
|
@@ -1127,14 +402,14 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
1127
402
|
if (isMathRun(run)) {
|
|
1128
403
|
const style = {
|
|
1129
404
|
fontFamily: run.fontFamily ?? "Cambria Math",
|
|
1130
|
-
fontSize: run.fontSize ??
|
|
405
|
+
fontSize: run.fontSize ?? 11,
|
|
1131
406
|
...run.bold !== void 0 ? { bold: run.bold } : {},
|
|
1132
407
|
...run.italic !== void 0 ? { italic: run.italic } : {}
|
|
1133
408
|
};
|
|
1134
409
|
updateMaxFont(style);
|
|
1135
410
|
const mathWidth = measureTextWidth(run.plainText || "[equation]", style);
|
|
1136
411
|
const mathFootprintPx = estimateMathFootprintPx(run);
|
|
1137
|
-
if (currentLine.width > 0 && currentLine.width + mathWidth > currentLine.availableWidth +
|
|
412
|
+
if (currentLine.width > 0 && currentLine.width + mathWidth > currentLine.availableWidth + .5) {
|
|
1138
413
|
startNewLine(runIndex, 0);
|
|
1139
414
|
updateMaxFont(style);
|
|
1140
415
|
}
|
|
@@ -1181,14 +456,10 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
1181
456
|
const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, effectiveLineBreakPolicy.hangingPunctuation);
|
|
1182
457
|
const isFirstLine = lines.length === 0;
|
|
1183
458
|
const regularSpaceWidth = compressibleSpaceWidth(measuredWord, style);
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
continuationStrategy: continuationJustifyFitStrategy,
|
|
1189
|
-
finalLineStrategy: finalLineJustifyFitStrategy
|
|
1190
|
-
});
|
|
1191
|
-
const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, justifyFitStrategy, regularSpaceWidth) : WIDTH_TOLERANCE;
|
|
459
|
+
const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, isFirstLine ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, regularSpaceWidth, {
|
|
460
|
+
width: currentLine.width + leadingLetterSpacing + wordWidth,
|
|
461
|
+
innerSpaceWidth: 0
|
|
462
|
+
}) : WIDTH_TOLERANCE;
|
|
1192
463
|
const automaticHyphenation = effectiveLineBreakPolicy.automaticHyphenation;
|
|
1193
464
|
const mayHyphenateLine = automaticHyphenation.type === "enabled" && (automaticHyphenation.consecutiveLineLimit === 0 || consecutiveHyphenatedLines < automaticHyphenation.consecutiveLineLimit) && exceedsHyphenationZone(currentLine, automaticHyphenation.hyphenationZoneTwips);
|
|
1194
465
|
const isRunTail = nextBreak === text.length;
|
|
@@ -1305,17 +576,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
1305
576
|
const rawGlueWidth = isRunTail && wordTail !== void 0 && (!isBreakBetween(wordTail, followingTextLeads[runIndex]) || protectedGlueWidth > 0) ? Math.max(trailingGlueWidths[runIndex] ?? 0, protectedGlueWidth) : 0;
|
|
1306
577
|
const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + widthTolerance ? rawGlueWidth : 0;
|
|
1307
578
|
const hangingAllowance = glueWidth === 0 ? hangingPunctuationWidth : 0;
|
|
1308
|
-
const candidateFit = isJustifiedParagraph ? resolveTextCandidateFit(
|
|
1309
|
-
block,
|
|
1310
|
-
line: currentLine,
|
|
1311
|
-
isFirstLine,
|
|
1312
|
-
isFinalCandidate: isFinalTextCandidate(block, runIndex, nextBreak),
|
|
1313
|
-
candidateWidth: currentLine.width + leadingLetterSpacing + wordWidth + glueWidth - hangingAllowance,
|
|
1314
|
-
candidateSpaceWidth: regularSpaceWidth,
|
|
1315
|
-
fallbackTolerancePx: widthTolerance,
|
|
1316
|
-
continuationStrategy: continuationJustifyFitStrategy,
|
|
1317
|
-
finalStrategy: finalLineJustifyFitStrategy
|
|
1318
|
-
}) : void 0;
|
|
579
|
+
const candidateFit = isJustifiedParagraph ? resolveTextCandidateFit(isFinalTextCandidate(block, runIndex, nextBreak), currentLine.width + leadingLetterSpacing + wordWidth + glueWidth - hangingAllowance, currentLine.availableWidth, widthTolerance) : void 0;
|
|
1319
580
|
const finalWrapTolerance = candidateFit?.tolerancePx ?? widthTolerance;
|
|
1320
581
|
if (wordWidth > 0 && currentLine.width > 0 && currentLine.width + leadingLetterSpacing + wordWidth + glueWidth > currentLine.availableWidth + finalWrapTolerance + hangingAllowance) {
|
|
1321
582
|
startNewLine(runIndex, charIndex);
|