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