@stll/folio-core 0.7.0 → 0.9.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 (193) hide show
  1. package/README.md +39 -0
  2. package/dist/ai-edits/apply.js +18 -4
  3. package/dist/ai-edits/blockRange.d.ts +13 -2
  4. package/dist/ai-edits/blockRange.js +23 -2
  5. package/dist/ai-edits/headless.d.ts +31 -4
  6. package/dist/ai-edits/headless.js +148 -35
  7. package/dist/ai-edits/index.d.ts +6 -4
  8. package/dist/ai-edits/index.js +4 -1
  9. package/dist/ai-edits/scoped-reading.d.ts +9 -0
  10. package/dist/ai-edits/scoped-reading.js +60 -0
  11. package/dist/ai-edits/snapshot.js +53 -7
  12. package/dist/ai-edits/types.d.ts +46 -3
  13. package/dist/compat/eigenpal.d.ts +8 -3
  14. package/dist/compat/eigenpal.js +7 -2
  15. package/dist/controller/fontReadiness.d.ts +29 -0
  16. package/dist/controller/fontReadiness.js +139 -0
  17. package/dist/controller/latestRequestGate.d.ts +13 -0
  18. package/dist/controller/latestRequestGate.js +16 -0
  19. package/dist/controller/layoutPipeline.js +88 -10
  20. package/dist/document-operations.d.ts +10 -3
  21. package/dist/document-operations.js +29 -14
  22. package/dist/docx/blockContentParser.js +53 -10
  23. package/dist/docx/commentParser.js +1 -1
  24. package/dist/docx/compatibility.d.ts +30 -2
  25. package/dist/docx/compatibility.js +118 -39
  26. package/dist/docx/conformance.d.ts +6 -0
  27. package/dist/docx/conformance.js +18 -0
  28. package/dist/docx/encryption/agileDecryption.js +1 -1
  29. package/dist/docx/encryption/encryptionInfo.js +1 -1
  30. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  31. package/dist/docx/ensureParaIds.d.ts +29 -0
  32. package/dist/docx/ensureParaIds.js +423 -0
  33. package/dist/docx/fontTableParser.d.ts +6 -0
  34. package/dist/docx/fontTableParser.js +72 -0
  35. package/dist/docx/groupDrawingParser.js +7 -2
  36. package/dist/docx/headerFooterParser.js +1 -1
  37. package/dist/docx/normalizeBaseDirection.js +1 -1
  38. package/dist/docx/numberingParser.js +2 -0
  39. package/dist/docx/paragraphParser.js +21 -1
  40. package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
  41. package/dist/docx/parser.d.ts +1 -1
  42. package/dist/docx/parser.js +14 -9
  43. package/dist/docx/rezip.js +70 -5
  44. package/dist/docx/runConsolidator.js +4 -0
  45. package/dist/docx/runParser.js +19 -1
  46. package/dist/docx/selectiveSave.js +3 -3
  47. package/dist/docx/selectiveXmlPatch.d.ts +10 -1
  48. package/dist/docx/selectiveXmlPatch.js +1 -1
  49. package/dist/docx/serializer/fontTableSerializer.d.ts +6 -0
  50. package/dist/docx/serializer/fontTableSerializer.js +26 -0
  51. package/dist/docx/serializer/headerFooterSerializer.js +1 -1
  52. package/dist/docx/serializer/paragraphSerializer.js +8 -0
  53. package/dist/docx/serializer/runSerializer.js +14 -2
  54. package/dist/docx/serializer/settingsSerializer.d.ts +6 -0
  55. package/dist/docx/serializer/settingsSerializer.js +16 -0
  56. package/dist/docx/serializer/stylesSerializer.d.ts +6 -0
  57. package/dist/docx/serializer/stylesSerializer.js +56 -0
  58. package/dist/docx/serializer/tableSerializer.js +29 -26
  59. package/dist/docx/serializer/themeSerializer.d.ts +6 -0
  60. package/dist/docx/serializer/themeSerializer.js +36 -0
  61. package/dist/docx/settingsParser.js +46 -0
  62. package/dist/docx/styleParser.js +26 -1
  63. package/dist/docx/tableParser.js +19 -1
  64. package/dist/docx/textBoxParser.js +6 -1
  65. package/dist/docx/vmlImageParser.js +146 -1
  66. package/dist/i18n/messages/catalogs.gen.d.ts +5916 -5916
  67. package/dist/i18n/messages/catalogs.gen.js +5910 -5910
  68. package/dist/index.d.ts +8 -3
  69. package/dist/index.js +7 -2
  70. package/dist/layout-bridge/convert/footnoteLayout.d.ts +3 -0
  71. package/dist/layout-bridge/convert/footnoteLayout.js +3 -1
  72. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +3 -0
  73. package/dist/layout-bridge/convert/headerFooterLayout.js +22 -3
  74. package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
  75. package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
  76. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +14 -2
  77. package/dist/layout-bridge/convert/toFlowBlocks.js +93 -17
  78. package/dist/layout-bridge/engine/hitTest.js +2 -1
  79. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  80. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  81. package/dist/layout-bridge/engine/selectionRects.js +2 -1
  82. package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
  83. package/dist/layout-engine/headerFooterRefs.js +40 -0
  84. package/dist/layout-engine/index.d.ts +3 -2
  85. package/dist/layout-engine/index.js +68 -106
  86. package/dist/layout-engine/measure/cache.js +11 -3
  87. package/dist/layout-engine/measure/font-metrics.worker.js +1 -0
  88. package/dist/layout-engine/measure/index.d.ts +2 -1
  89. package/dist/layout-engine/measure/index.js +2 -1
  90. package/dist/layout-engine/measure/lineBreakProvider.d.ts +33 -0
  91. package/dist/layout-engine/measure/lineBreakProvider.js +252 -0
  92. package/dist/layout-engine/measure/lineBreaks.d.ts +7 -2
  93. package/dist/layout-engine/measure/lineBreaks.js +11 -18
  94. package/dist/layout-engine/measure/listMarkerWidth.d.ts +4 -1
  95. package/dist/layout-engine/measure/listMarkerWidth.js +33 -8
  96. package/dist/layout-engine/measure/measureBlocks.d.ts +4 -2
  97. package/dist/layout-engine/measure/measureBlocks.js +138 -86
  98. package/dist/layout-engine/measure/measureContainer.js +32 -9
  99. package/dist/layout-engine/measure/measureHelpers.js +5 -2
  100. package/dist/layout-engine/measure/measureParagraph.js +346 -74
  101. package/dist/layout-engine/measure/measureTypes.d.ts +2 -1
  102. package/dist/layout-engine/measure/measureWorker.d.ts +2 -2
  103. package/dist/layout-engine/measure/measureWorker.js +8 -19
  104. package/dist/layout-engine/measure/measureWorkerProtocol.d.ts +3 -14
  105. package/dist/layout-engine/measure/tableCellFloating.d.ts +10 -3
  106. package/dist/layout-engine/measure/tableCellFloating.js +37 -29
  107. package/dist/layout-engine/measure/tableCellFlow.d.ts +27 -0
  108. package/dist/layout-engine/measure/tableCellFlow.js +73 -0
  109. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
  110. package/dist/layout-engine/measure/tableCellGrid.js +62 -0
  111. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
  112. package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
  113. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +18 -0
  114. package/dist/layout-engine/measure/textMeasurementPolicy.js +24 -0
  115. package/dist/layout-engine/paginator.d.ts +2 -1
  116. package/dist/layout-engine/paginator.js +7 -6
  117. package/dist/layout-engine/paragraphFrame.d.ts +22 -0
  118. package/dist/layout-engine/paragraphFrame.js +17 -0
  119. package/dist/layout-engine/paragraphSequence.d.ts +7 -0
  120. package/dist/layout-engine/paragraphSequence.js +25 -0
  121. package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
  122. package/dist/layout-engine/paragraphSpacing.js +30 -0
  123. package/dist/layout-engine/renderedBreakReconciliation.d.ts +63 -0
  124. package/dist/layout-engine/renderedBreakReconciliation.js +86 -0
  125. package/dist/layout-engine/tableRowBreak.d.ts +5 -2
  126. package/dist/layout-engine/tableRowBreak.js +62 -29
  127. package/dist/layout-engine/types.d.ts +54 -12
  128. package/dist/layout-engine/types.js +15 -2
  129. package/dist/layout-painter/anchoredImagePosition.d.ts +23 -0
  130. package/dist/layout-painter/anchoredImagePosition.js +95 -0
  131. package/dist/layout-painter/borderStroke.d.ts +23 -0
  132. package/dist/layout-painter/borderStroke.js +39 -0
  133. package/dist/layout-painter/renderImage.d.ts +5 -4
  134. package/dist/layout-painter/renderImage.js +18 -4
  135. package/dist/layout-painter/renderPage.d.ts +2 -27
  136. package/dist/layout-painter/renderPage.js +15 -101
  137. package/dist/layout-painter/renderParagraph.js +45 -26
  138. package/dist/layout-painter/renderTable.d.ts +2 -0
  139. package/dist/layout-painter/renderTable.js +257 -47
  140. package/dist/layout-painter/renderTextBox.js +17 -7
  141. package/dist/managers/DocumentLoaderManager.d.ts +1 -1
  142. package/dist/managers/DocumentLoaderManager.js +2 -2
  143. package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
  144. package/dist/paged-layout/sectionBlockWidths.js +9 -0
  145. package/dist/paged-layout/sectionGeometry.js +1 -1
  146. package/dist/prosemirror/attrs/index.d.ts +4 -2
  147. package/dist/prosemirror/attrs/index.js +32 -2
  148. package/dist/prosemirror/commands/propertyChangeScope.js +1 -1
  149. package/dist/prosemirror/conversion/fromProseDoc.js +35 -18
  150. package/dist/prosemirror/conversion/toProseDoc.js +73 -37
  151. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  152. package/dist/prosemirror/extensions/core/ParagraphExtension.js +8 -2
  153. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +1 -1
  154. package/dist/prosemirror/extensions/features/ListExtension.js +2 -0
  155. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
  156. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +1 -0
  157. package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
  158. package/dist/prosemirror/extensions/marks/LanguageExtension.d.ts +7 -0
  159. package/dist/prosemirror/extensions/marks/LanguageExtension.js +30 -0
  160. package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
  161. package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
  162. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +1 -1
  163. package/dist/prosemirror/extensions/marks/markUtils.js +12 -0
  164. package/dist/prosemirror/extensions/nodes/ImageExtension.js +2 -1
  165. package/dist/prosemirror/extensions/nodes/TableExtension.js +4 -2
  166. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  167. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  168. package/dist/prosemirror/schema/index.d.ts +2 -2
  169. package/dist/prosemirror/schema/marks.d.ts +6 -1
  170. package/dist/prosemirror/schema/nodes.d.ts +16 -12
  171. package/dist/prosemirror/styles/resolvedStyleAttrs.js +2 -0
  172. package/dist/prosemirror/utils/tabCalculator.d.ts +3 -1
  173. package/dist/prosemirror/utils/tabCalculator.js +12 -10
  174. package/dist/prosemirror/validation.js +4 -1
  175. package/dist/redline.js +13 -3
  176. package/dist/server.d.ts +9 -4
  177. package/dist/server.js +7 -2
  178. package/dist/style-sets/extract.d.ts +35 -0
  179. package/dist/style-sets/extract.js +123 -0
  180. package/dist/style-sets/stellaStyle.d.ts +13 -0
  181. package/dist/style-sets/stellaStyle.js +516 -0
  182. package/dist/style-sets/types.d.ts +31 -0
  183. package/dist/style-sets/types.js +5 -0
  184. package/dist/types/index.d.ts +2 -1
  185. package/dist/utils/createDocument.d.ts +12 -2
  186. package/dist/utils/createDocument.js +55 -36
  187. package/dist/utils/fontResolver.js +6 -6
  188. package/dist/utils/formatToStyle.js +1 -1
  189. package/dist/utils/hexId.d.ts +8 -1
  190. package/dist/utils/hexId.js +11 -3
  191. package/dist/utils/paragraphFormattingMerge.d.ts +14 -3
  192. package/dist/utils/paragraphFormattingMerge.js +14 -4
  193. package/package.json +5 -1
@@ -1,6 +1,8 @@
1
- import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
2
1
  import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
2
+ import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
3
+ import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
3
4
  import { buildRunFontStyle, ptToPx } from "./measureHelpers.js";
5
+ import "./lineBreakProvider.js";
4
6
  import { calculateTabWidth, pixelsToTwips } from "../../prosemirror/utils/tabCalculator.js";
5
7
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
6
8
  import { hasCjk } from "../../utils/scriptSegments.js";
@@ -9,7 +11,7 @@ import { isFloatingImageRun } from "../types.js";
9
11
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
10
12
  import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
11
13
  import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
12
- import { findWordBreaks, isBreakChar } from "./lineBreaks.js";
14
+ import { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakChar, isHangingPunctuation } from "./lineBreaks.js";
13
15
  //#region src/layout-engine/measure/measureParagraph.ts
14
16
  /**
15
17
  * Paragraph measurement module
@@ -22,8 +24,11 @@ const DEFAULT_FONT_FAMILY = "Calibri";
22
24
  const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
23
25
  const WIDTH_TOLERANCE = .5;
24
26
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
27
+ const JUSTIFY_SPACE_CONTRACTION_RATIO = .1;
28
+ const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
29
+ const JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO = .23;
30
+ const JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO = .015;
25
31
  const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
26
- const JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO = .025;
27
32
  const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
28
33
  const DEFAULT_LIST_HANGING_INDENT_PX = 24;
29
34
  const ALL_CAPS_RATIO_THRESHOLD = .8;
@@ -31,18 +36,20 @@ const ALL_CAPS_RATIO_THRESHOLD = .8;
31
36
  * Find the longest prefix of `text` that fits within `maxWidth` pixels.
32
37
  * Returns the number of characters that fit (at least 1 if `forceMin` is true).
33
38
  */
34
- function findMaxFittingLength(text, style, maxWidth, forceMin = false) {
35
- let lo = 1;
36
- let hi = text.length;
39
+ function findMaxFittingLength(text, style, maxWidth, forceMin = false, policy) {
40
+ const boundaries = findGraphemeBreaks(text, policy);
41
+ let lo = 0;
42
+ let hi = boundaries.length - 1;
37
43
  let best = 0;
38
44
  while (lo <= hi) {
39
45
  const mid = Math.floor((lo + hi) / 2);
40
- if (measureTextWidth(text.slice(0, mid), style) <= maxWidth) {
41
- best = mid;
46
+ const boundary = boundaries[mid];
47
+ if (measureTextWidth(text.slice(0, boundary), style) <= maxWidth) {
48
+ best = boundary;
42
49
  lo = mid + 1;
43
50
  } else hi = mid - 1;
44
51
  }
45
- return forceMin && best === 0 ? 1 : best;
52
+ return forceMin && best === 0 ? boundaries.at(0) ?? 0 : best;
46
53
  }
47
54
  /**
48
55
  * Extract FontStyle from a run that carries RunFormatting (text, tab, or
@@ -53,6 +60,31 @@ function findMaxFittingLength(text, style, maxWidth, forceMin = false) {
53
60
  function runToFontStyle(run) {
54
61
  return buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
55
62
  }
63
+ function languageMatches(ruleLanguage, locale) {
64
+ if (!ruleLanguage) return true;
65
+ if (!locale) return false;
66
+ const rule = ruleLanguage.toLowerCase();
67
+ const active = locale.toLowerCase();
68
+ return active === rule || active.startsWith(`${rule}-`) || rule.startsWith(`${active}-`);
69
+ }
70
+ function lineBreakPolicy(block, run) {
71
+ const language = run.language;
72
+ let locale = language?.val;
73
+ if (hasCjk(run.text)) locale = language?.eastAsia ?? language?.val;
74
+ else if (run.rtl) locale = language?.bidi ?? language?.val;
75
+ const rules = block.attrs?.lineBreakRules;
76
+ const before = rules?.noLineBreaksBefore;
77
+ const after = rules?.noLineBreaksAfter;
78
+ return {
79
+ ...locale ? { locale } : {},
80
+ ...block.attrs?.kinsoku !== void 0 ? { kinsoku: block.attrs.kinsoku } : {},
81
+ ...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: before.characters } : {},
82
+ ...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: after.characters } : {},
83
+ ...rules?.useLegacyEthiopicAmharicRules ? { useLegacyEthiopicAmharicRules: true } : {},
84
+ ...block.attrs?.automaticHyphenation?.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: block.attrs.automaticHyphenation.doNotHyphenateCaps } : {},
85
+ ...run.allCaps === true ? { renderedAllCaps: true } : {}
86
+ };
87
+ }
56
88
  /**
57
89
  * Line-HEIGHT style for a text run. Word derives a CJK line's height from an
58
90
  * East-Asian face, not the run's ascii font: real documents routinely put CJK
@@ -245,6 +277,7 @@ function isEmptyTextRun(run) {
245
277
  * preceding a floating image.
246
278
  */
247
279
  function isBlockLayoutImageRun(run) {
280
+ if (isFloatingImageRun(run)) return false;
248
281
  return run.wrapType === "topAndBottom" || run.displayMode === "block";
249
282
  }
250
283
  function measureInlineWidthAfterTab(runs, tabIndex, fieldValues) {
@@ -329,34 +362,63 @@ function uppercaseLetterRatio(text) {
329
362
  }
330
363
  return letters === 0 ? 0 : uppercase / letters;
331
364
  }
332
- function fixedSpaceAdjustedShrinkTolerance({ tolerance, regularSpaceCount, nonBreakingSpaceCount }) {
333
- const totalSpaces = regularSpaceCount + nonBreakingSpaceCount;
334
- if (totalSpaces === 0) return tolerance;
335
- return Math.max(JUSTIFY_SHRINK_TOLERANCE_RATIO, tolerance * (regularSpaceCount / totalSpaces));
336
- }
337
- function justifyShrinkToleranceRatio(block, isFirstLine, regularSpaceCount, nonBreakingSpaceCount) {
365
+ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
366
+ if (isShallowFullHangingListContinuation(block, isFirstLine)) return { type: "rounding" };
338
367
  if (block.attrs?.listMarker !== void 0) {
339
368
  const hanging = block.attrs.indent?.hanging ?? 0;
340
- if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ? JUSTIFY_SHRINK_TOLERANCE_RATIO : JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO;
369
+ if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ? {
370
+ type: "space",
371
+ ratio: JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO
372
+ } : {
373
+ type: "width",
374
+ ratio: JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO
375
+ };
341
376
  const left = block.attrs.indent?.left ?? 0;
342
- if (hanging > 0 && left > hanging) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
343
- return fixedSpaceAdjustedShrinkTolerance({
344
- tolerance: JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO,
345
- regularSpaceCount,
346
- nonBreakingSpaceCount
347
- });
377
+ if (hanging > 0 && left > hanging) return {
378
+ type: "width",
379
+ ratio: JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO
380
+ };
381
+ return {
382
+ type: "space",
383
+ ratio: JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO
384
+ };
348
385
  }
349
386
  const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
350
- const hasTabRuns = block.runs.some(isTabRun);
351
- if (isFirstLine && hasTabRuns && (block.attrs?.indent?.hanging ?? 0) > 0) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
352
- if (!isFirstLine && hasTabRuns && !hasTabStops) return JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO;
353
- if (hasTabRuns && (isFirstLine || hasTabStops)) return (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO;
354
- if (uppercaseLetterRatio(block.runs.map((run) => isTextRun(run) ? run.text ?? "" : "").join("")) > ALL_CAPS_RATIO_THRESHOLD) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
355
- return fixedSpaceAdjustedShrinkTolerance({
356
- tolerance: JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO,
357
- regularSpaceCount,
358
- nonBreakingSpaceCount
359
- });
387
+ if (isFirstLine && profile.hasTabRuns && (block.attrs?.indent?.hanging ?? 0) > 0) return {
388
+ type: "width",
389
+ ratio: JUSTIFY_SHRINK_TOLERANCE_RATIO
390
+ };
391
+ if (!isFirstLine && profile.hasTabRuns && !hasTabStops) return {
392
+ type: "width",
393
+ ratio: JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO
394
+ };
395
+ if (profile.hasTabRuns && (isFirstLine || hasTabStops)) return {
396
+ type: "width",
397
+ ratio: (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO
398
+ };
399
+ if (profile.uppercaseRatio > ALL_CAPS_RATIO_THRESHOLD) return {
400
+ type: "width",
401
+ ratio: JUSTIFY_SHRINK_TOLERANCE_RATIO
402
+ };
403
+ return {
404
+ type: "space",
405
+ ratio: JUSTIFY_SPACE_CONTRACTION_RATIO
406
+ };
407
+ }
408
+ function compressibleSpaceWidth(text, style) {
409
+ const count = countCompressibleSpaces(text);
410
+ return count === 0 ? 0 : count * measureTextWidth(" ", style);
411
+ }
412
+ function justifyFitTolerance(line, strategy, candidateSpaceWidth) {
413
+ if (strategy.type === "rounding") return WIDTH_TOLERANCE;
414
+ if (strategy.type === "width") return Math.max(WIDTH_TOLERANCE, line.availableWidth * strategy.ratio);
415
+ return Math.max(WIDTH_TOLERANCE, (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio);
416
+ }
417
+ function isShallowFullHangingListContinuation(block, isFirstLine) {
418
+ if (isFirstLine || block.attrs?.listMarker === void 0) return false;
419
+ const hanging = block.attrs.indent?.hanging ?? 0;
420
+ const left = block.attrs.indent?.left ?? 0;
421
+ return hanging > 0 && hanging < DEFAULT_LIST_HANGING_INDENT_PX && Math.abs(left - hanging) <= WIDTH_TOLERANCE;
360
422
  }
361
423
  function trimTrailingSpacesAndTabs(text) {
362
424
  let end = text.length;
@@ -367,12 +429,20 @@ function trimTrailingSpacesAndTabs(text) {
367
429
  }
368
430
  return text.slice(0, end);
369
431
  }
432
+ function trailingHangingPunctuationWidth(text, style, policy, enabled) {
433
+ if (!enabled || text.length === 0) return 0;
434
+ const lastStart = findGraphemeBreaks(text, policy).at(-2) ?? 0;
435
+ const lastGrapheme = text.slice(lastStart);
436
+ if (!isHangingPunctuation(lastGrapheme, policy)) return 0;
437
+ return measureTextWidth(lastGrapheme, style);
438
+ }
370
439
  /**
371
440
  * Width of the unbreakable text glued to the end of each run.
372
441
  * A run boundary is not itself a wrap opportunity: adjacent note markers and
373
442
  * format-only word splits must wrap as one cluster.
374
443
  */
375
- function computeTrailingGlueWidths(runs) {
444
+ function computeTrailingGlueWidths(block) {
445
+ const runs = block.runs;
376
446
  const widths = Array.from({ length: runs.length }, () => 0);
377
447
  for (let index = runs.length - 1; index >= 0; index--) {
378
448
  const nextRun = runs[index + 1];
@@ -382,19 +452,112 @@ function computeTrailingGlueWidths(runs) {
382
452
  widths[index] = widths[index + 1] ?? 0;
383
453
  continue;
384
454
  }
385
- if (isSpaceOrTab(text[0])) continue;
455
+ if (isSpaceOrTab(text.at(0))) continue;
386
456
  const style = runToFontStyle(nextRun);
387
- const breaks = findWordBreaks(text);
388
- if (breaks.length === 0) {
389
- widths[index] = measureTextWidth(text, style) + (widths[index + 1] ?? 0);
390
- continue;
457
+ const firstBreak = findWordBreaks(text, lineBreakPolicy(block, nextRun)).at(0);
458
+ widths[index] = measureTextWidth(firstBreak === void 0 ? text : trimTrailingSpacesAndTabs(text.slice(0, firstBreak)), style) + (firstBreak === void 0 ? widths[index + 1] ?? 0 : 0);
459
+ }
460
+ return widths;
461
+ }
462
+ function computeProtectedCrossRunGlueWidths(block) {
463
+ const widths = Array.from({ length: block.runs.length }, () => 0);
464
+ for (let index = 0; index < block.runs.length; index++) {
465
+ const run = block.runs[index];
466
+ if (!run || !isTextRun(run)) continue;
467
+ const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
468
+ const token = trailing?.[1];
469
+ const policy = lineBreakPolicy(block, run);
470
+ if (!token || token.length !== 1 || !policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
471
+ let separator = trailing[2] ?? "";
472
+ let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
473
+ let followingWord = "";
474
+ let unseparatedFollowingText = false;
475
+ for (let nextIndex = index + 1; nextIndex < block.runs.length; nextIndex++) {
476
+ const nextRun = block.runs[nextIndex];
477
+ if (!nextRun || !isTextRun(nextRun)) break;
478
+ const text = nextRun.text ?? "";
479
+ let consumed = "";
480
+ for (const char of text) {
481
+ const isWhitespace = /\s/u.test(char);
482
+ if (followingWord.length > 0 && isWhitespace) break;
483
+ if (separator.length === 0 && !isWhitespace) {
484
+ unseparatedFollowingText = true;
485
+ break;
486
+ }
487
+ consumed += char;
488
+ if (isWhitespace) separator += char;
489
+ else followingWord += char;
490
+ }
491
+ if (consumed.length > 0) glueWidth += measureTextWidth(consumed, runToFontStyle(nextRun));
492
+ if (unseparatedFollowingText) break;
493
+ if (followingWord.length > 0 && consumed.length < text.length) break;
391
494
  }
392
- const firstBreak = breaks[0];
393
- if (firstBreak === void 0) continue;
394
- widths[index] = measureTextWidth(trimTrailingSpacesAndTabs(text.slice(0, firstBreak)), style);
495
+ if (unseparatedFollowingText || separator.length === 0 || followingWord.length === 0) continue;
496
+ const boundary = token.length + separator.length;
497
+ if (!findWordBreaks(token + separator + followingWord, policy).includes(boundary)) widths[index] = glueWidth;
395
498
  }
396
499
  return widths;
397
500
  }
501
+ /** Collect one lexical word continued through adjacent formatting runs. */
502
+ function collectCrossRunWord({ block, startRunIndex, startChar }) {
503
+ const segments = [];
504
+ let text = "";
505
+ let width = 0;
506
+ for (let runIndex = startRunIndex; runIndex < block.runs.length; runIndex++) {
507
+ const run = block.runs[runIndex];
508
+ if (!run || !isTextRun(run)) break;
509
+ const start = runIndex === startRunIndex ? startChar : 0;
510
+ const remainder = run.text.slice(start);
511
+ if (remainder.length === 0) continue;
512
+ if (runIndex !== startRunIndex && isSpaceOrTab(remainder[0])) break;
513
+ const remainingBudget = 256 - text.length;
514
+ const boundedRemainder = remainder.slice(0, remainingBudget + 1);
515
+ const firstBreak = findWordBreaks(boundedRemainder, lineBreakPolicy(block, run)).at(0);
516
+ const segmentText = trimTrailingSpacesAndTabs(firstBreak === void 0 ? boundedRemainder : boundedRemainder.slice(0, firstBreak));
517
+ if (segmentText.length === 0) break;
518
+ if (segmentText.length > remainingBudget) return;
519
+ const style = runToFontStyle(run);
520
+ segments.push({
521
+ runIndex,
522
+ startChar: start,
523
+ text: segmentText,
524
+ run,
525
+ style
526
+ });
527
+ text += segmentText;
528
+ width += measureTextWidth(segmentText, style);
529
+ if (firstBreak !== void 0) break;
530
+ }
531
+ if (segments.length < 2) return;
532
+ const firstRun = segments[0]?.run;
533
+ if (!firstRun) return;
534
+ return {
535
+ text,
536
+ width,
537
+ breaks: findHyphenationBreaks(text, lineBreakPolicy(block, firstRun)),
538
+ segments
539
+ };
540
+ }
541
+ function measureCrossRunPrefix(word, breakOffset) {
542
+ let width = 0;
543
+ let remaining = breakOffset;
544
+ const measuredSegments = [];
545
+ for (const segment of word.segments) {
546
+ const length = Math.min(remaining, segment.text.length);
547
+ if (length > 0) {
548
+ width += measureTextWidth(segment.text.slice(0, length), segment.style);
549
+ measuredSegments.push(segment);
550
+ }
551
+ if (remaining <= segment.text.length) return {
552
+ width,
553
+ endRunIndex: segment.runIndex,
554
+ endChar: segment.startChar + remaining,
555
+ hyphenStyle: segment.style,
556
+ segments: measuredSegments
557
+ };
558
+ remaining -= segment.text.length;
559
+ }
560
+ }
398
561
  /**
399
562
  * Minimum horizontal room a line must offer before we treat it as usable for
400
563
  * body text. Below this threshold the line is bumped past obstructing floats
@@ -444,6 +607,12 @@ function measureParagraph(block, maxWidth, options) {
444
607
  const attrs = block.attrs;
445
608
  const spacing = attrs?.spacing;
446
609
  const isJustifiedParagraph = attrs?.alignment === "justify";
610
+ const justificationProfile = {
611
+ hasTabRuns: isJustifiedParagraph && runs.some(isTabRun),
612
+ uppercaseRatio: isJustifiedParagraph ? uppercaseLetterRatio(runs.map((run) => isTextRun(run) ? run.text ?? "" : "").join("")) : 0
613
+ };
614
+ const firstLineJustifyFitStrategy = resolveJustifyFitStrategy(block, true, justificationProfile);
615
+ const continuationJustifyFitStrategy = resolveJustifyFitStrategy(block, false, justificationProfile);
447
616
  const floatingZones = options?.floatingZones;
448
617
  const paragraphYOffset = options?.paragraphYOffset ?? 0;
449
618
  const indent = attrs?.indent;
@@ -474,6 +643,7 @@ function measureParagraph(block, maxWidth, options) {
474
643
  const firstLineFloatingMargins = getFloatingMargins(cumulativeHeight, estimatedFirstLineHeight, floatingZones, paragraphYOffset);
475
644
  const firstLineWidth = Math.max(1, getFloatingAvailableWidth(firstLineFloatingMargins, baseFirstLineWidth));
476
645
  const lines = [];
646
+ let consecutiveHyphenatedLines = 0;
477
647
  if (runs.length === 0) {
478
648
  if (attrs?.suppressEmptyParagraphHeight) {
479
649
  lines.push({
@@ -532,7 +702,8 @@ function measureParagraph(block, maxWidth, options) {
532
702
  totalHeight
533
703
  };
534
704
  }
535
- const trailingGlueWidths = computeTrailingGlueWidths(runs);
705
+ const trailingGlueWidths = computeTrailingGlueWidths(block);
706
+ const protectedCrossRunGlueWidths = computeProtectedCrossRunGlueWidths(block);
536
707
  let currentLine = {
537
708
  fromRun: 0,
538
709
  fromChar: 0,
@@ -540,11 +711,11 @@ function measureParagraph(block, maxWidth, options) {
540
711
  toChar: 0,
541
712
  width: 0,
542
713
  trailingWhitespaceWidth: 0,
543
- regularSpaceCount: 0,
544
- nonBreakingSpaceCount: 0,
714
+ regularSpaceWidth: 0,
545
715
  maxFontSize: DEFAULT_FONT_SIZE,
546
716
  maxFontMetrics: null,
547
717
  maxImageHeightPx: 0,
718
+ maxExactImageHeightPx: 0,
548
719
  maxMathHeightPx: 0,
549
720
  availableWidth: firstLineWidth,
550
721
  leftOffset: firstLineFloatingMargins.leftMargin,
@@ -552,14 +723,29 @@ function measureParagraph(block, maxWidth, options) {
552
723
  ...firstLineFloatingMargins.segments?.length ? { segmentZones: firstLineFloatingMargins.segments } : {}
553
724
  };
554
725
  const calculateLineTypography = (line) => {
555
- const finalTypography = { ...calculateTypographyMetrics(line.maxFontSize, spacing, line.maxFontMetrics) };
726
+ const paragraphFontSize = attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE;
727
+ const paragraphFontFamily = attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY;
728
+ const fontSize = line.maxFontMetrics ? line.maxFontSize : paragraphFontSize;
729
+ const metrics = line.maxFontMetrics ?? getFontMetrics({
730
+ fontSize: paragraphFontSize,
731
+ fontFamily: paragraphFontFamily
732
+ });
733
+ const finalTypography = { ...calculateTypographyMetrics(fontSize, spacing, metrics) };
556
734
  const inlineObjectHeight = Math.max(line.maxImageHeightPx, line.maxMathHeightPx);
557
735
  if (inlineObjectHeight > finalTypography.lineHeight) {
558
736
  const objectHeight = inlineObjectHeight;
559
737
  const buffer = finalTypography.descent;
560
- if (line.fromRun === line.toRun) {
561
- finalTypography.lineHeight = objectHeight + buffer * 2;
562
- finalTypography.ascent = objectHeight + buffer;
738
+ const soleRun = line.fromRun === line.toRun ? runs[line.fromRun] : void 0;
739
+ if (line.maxExactImageHeightPx >= objectHeight) {
740
+ finalTypography.lineHeight = objectHeight;
741
+ finalTypography.ascent = objectHeight;
742
+ finalTypography.descent = 0;
743
+ return finalTypography;
744
+ }
745
+ if (soleRun && isImageRun(soleRun)) {
746
+ finalTypography.lineHeight = objectHeight;
747
+ finalTypography.ascent = objectHeight;
748
+ finalTypography.descent = 0;
563
749
  } else {
564
750
  finalTypography.lineHeight = objectHeight + buffer;
565
751
  finalTypography.ascent = objectHeight;
@@ -589,7 +775,8 @@ function measureParagraph(block, maxWidth, options) {
589
775
  toRun: currentLine.toRun,
590
776
  toChar: currentLine.toChar,
591
777
  width: Math.max(0, currentLine.width - currentLine.trailingWhitespaceWidth),
592
- ...finalTypography
778
+ ...finalTypography,
779
+ ...currentLine.discretionaryHyphen ? { discretionaryHyphen: currentLine.discretionaryHyphen } : {}
593
780
  };
594
781
  if (currentLine.leftOffset > 0) line.leftOffset = currentLine.leftOffset;
595
782
  if (currentLine.rightOffset > 0) line.rightOffset = currentLine.rightOffset;
@@ -598,6 +785,7 @@ function measureParagraph(block, maxWidth, options) {
598
785
  pendingFloatSkip = 0;
599
786
  }
600
787
  lines.push(line);
788
+ consecutiveHyphenatedLines = currentLine.discretionaryHyphen ? consecutiveHyphenatedLines + 1 : 0;
601
789
  cumulativeHeight += finalTypography.lineHeight;
602
790
  };
603
791
  /**
@@ -616,11 +804,11 @@ function measureParagraph(block, maxWidth, options) {
616
804
  toChar: charIndex,
617
805
  width: 0,
618
806
  trailingWhitespaceWidth: 0,
619
- regularSpaceCount: 0,
620
- nonBreakingSpaceCount: 0,
807
+ regularSpaceWidth: 0,
621
808
  maxFontSize: DEFAULT_FONT_SIZE,
622
809
  maxFontMetrics: null,
623
810
  maxImageHeightPx: 0,
811
+ maxExactImageHeightPx: 0,
624
812
  maxMathHeightPx: 0,
625
813
  availableWidth: adjustedWidth,
626
814
  leftOffset: floatingMargins.leftMargin,
@@ -642,7 +830,9 @@ function measureParagraph(block, maxWidth, options) {
642
830
  const metrics = getFontMetrics(style);
643
831
  if (metrics.singleLineRatio > currentLine.maxFontMetrics.singleLineRatio) currentLine.maxFontMetrics = metrics;
644
832
  };
833
+ let crossRunResume;
645
834
  for (let runIndex = 0; runIndex < runs.length; runIndex++) {
835
+ if (crossRunResume && runIndex < crossRunResume.runIndex) continue;
646
836
  const run = runs[runIndex];
647
837
  if (isLineBreakRun(run)) {
648
838
  currentLine.toRun = runIndex;
@@ -682,7 +872,7 @@ function measureParagraph(block, maxWidth, options) {
682
872
  }
683
873
  if (isImageRun(run)) {
684
874
  const wrapType = run.wrapType;
685
- if (run.displayMode === "float" || wrapType === "square" || wrapType === "tight" || wrapType === "through" || wrapType === "behind" || wrapType === "inFront") {
875
+ if (isFloatingImageRun(run)) {
686
876
  currentLine.toRun = runIndex;
687
877
  currentLine.toChar = 1;
688
878
  continue;
@@ -704,6 +894,7 @@ function measureParagraph(block, maxWidth, options) {
704
894
  if (currentLine.width > 0 && currentLine.width + imageWidth > currentLine.availableWidth + WIDTH_TOLERANCE) startNewLine(runIndex, 0);
705
895
  const imageFootprintPx = imageHeight + (run.distTop ?? 0) + (run.distBottom ?? 0);
706
896
  if (imageFootprintPx > currentLine.maxImageHeightPx) currentLine.maxImageHeightPx = imageFootprintPx;
897
+ if (run.exactLineHeight === true && imageFootprintPx > currentLine.maxExactImageHeightPx) currentLine.maxExactImageHeightPx = imageFootprintPx;
707
898
  currentLine.width += imageWidth;
708
899
  currentLine.trailingWhitespaceWidth = 0;
709
900
  currentLine.toRun = runIndex;
@@ -715,11 +906,14 @@ function measureParagraph(block, maxWidth, options) {
715
906
  const style = buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
716
907
  updateMaxFont(style);
717
908
  const fieldWidth = measureTextWidth(fallback, style);
718
- if (currentLine.width > 0 && currentLine.width + fieldWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
909
+ const fieldSpaceWidth = compressibleSpaceWidth(fallback, style);
910
+ const fieldTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, lines.length === 0 ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, fieldSpaceWidth) : WIDTH_TOLERANCE;
911
+ if (currentLine.width > 0 && currentLine.width + fieldWidth > currentLine.availableWidth + fieldTolerance) {
719
912
  startNewLine(runIndex, 0);
720
913
  updateMaxFont(style);
721
914
  }
722
915
  currentLine.width += fieldWidth;
916
+ currentLine.regularSpaceWidth += fieldSpaceWidth;
723
917
  currentLine.trailingWhitespaceWidth = 0;
724
918
  currentLine.toRun = runIndex;
725
919
  currentLine.toChar = 1;
@@ -750,6 +944,7 @@ function measureParagraph(block, maxWidth, options) {
750
944
  const textRun = run;
751
945
  const text = textRun.text;
752
946
  const style = runToFontStyle(textRun);
947
+ const breakPolicy = lineBreakPolicy(block, textRun);
753
948
  const lineHeightStyle = cjkLineHeightStyle(textRun, style);
754
949
  updateMaxFont(lineHeightStyle);
755
950
  if (!text || text.length === 0) {
@@ -757,8 +952,10 @@ function measureParagraph(block, maxWidth, options) {
757
952
  currentLine.toChar = 0;
758
953
  continue;
759
954
  }
760
- const wordBreaks = findWordBreaks(text);
761
- let charIndex = 0;
955
+ const wordBreaks = findWordBreaks(text, breakPolicy);
956
+ let charIndex = crossRunResume?.runIndex === runIndex ? crossRunResume.charIndex : 0;
957
+ if (crossRunResume?.runIndex === runIndex) crossRunResume = void 0;
958
+ let activeHyphenationWord;
762
959
  while (charIndex < text.length) {
763
960
  let nextBreak = text.length;
764
961
  for (const breakPoint of wordBreaks) if (breakPoint > charIndex) {
@@ -769,29 +966,103 @@ function measureParagraph(block, maxWidth, options) {
769
966
  const measuredWord = trimTrailingSpacesAndTabs(word);
770
967
  const wordWidth = measureTextWidth(measuredWord, style);
771
968
  const fullWordWidth = measureTextWidth(word, style);
772
- const regularSpaces = measuredWord.split(" ").length - 1;
773
- const nonBreakingSpaces = measuredWord.split("\xA0").length - 1;
774
- const widthTolerance = isJustifiedParagraph ? Math.max(WIDTH_TOLERANCE, currentLine.availableWidth * justifyShrinkToleranceRatio(block, lines.length === 0, currentLine.regularSpaceCount + regularSpaces, currentLine.nonBreakingSpaceCount + nonBreakingSpaces)) : WIDTH_TOLERANCE;
775
- if (wordWidth > currentLine.availableWidth + widthTolerance) {
969
+ const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, block.attrs?.overflowPunctuation);
970
+ const isFirstLine = lines.length === 0;
971
+ const regularSpaceWidth = compressibleSpaceWidth(measuredWord, style);
972
+ const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, isFirstLine ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, regularSpaceWidth) : WIDTH_TOLERANCE;
973
+ const automaticHyphenation = block.attrs?.automaticHyphenation;
974
+ const consecutiveLineLimit = automaticHyphenation?.consecutiveLineLimit ?? 0;
975
+ const mayHyphenateLine = automaticHyphenation?.enabled === true && block.attrs?.suppressAutoHyphens !== true && (consecutiveLineLimit === 0 || consecutiveHyphenatedLines < consecutiveLineLimit);
976
+ const isRunTail = nextBreak === text.length;
977
+ const crossRunWord = mayHyphenateLine && isRunTail && word.length > 0 && !isBreakChar(word.at(-1)) ? collectCrossRunWord({
978
+ block,
979
+ startRunIndex: runIndex,
980
+ startChar: charIndex
981
+ }) : void 0;
982
+ if (mayHyphenateLine && crossRunWord === void 0 && measuredWord.length > 0 && (activeHyphenationWord === void 0 || charIndex < activeHyphenationWord.start || charIndex >= activeHyphenationWord.end)) {
983
+ const fullWordEnd = charIndex + measuredWord.length;
984
+ const fullWordText = text.slice(charIndex, fullWordEnd);
985
+ activeHyphenationWord = {
986
+ start: charIndex,
987
+ end: fullWordEnd,
988
+ text: fullWordText,
989
+ breaks: findHyphenationBreaks(fullWordText, breakPolicy)
990
+ };
991
+ }
992
+ const overflowsCurrentLine = wordWidth > 0 && currentLine.width + wordWidth > currentLine.availableWidth + widthTolerance;
993
+ if (mayHyphenateLine && overflowsCurrentLine && activeHyphenationWord) {
994
+ const consumed = charIndex - activeHyphenationWord.start;
995
+ const spaceLeft = currentLine.availableWidth - currentLine.width + widthTolerance;
996
+ const hyphenWidth = measureTextWidth("-", style);
997
+ let fittingBreak;
998
+ for (const breakOffset of activeHyphenationWord.breaks) {
999
+ if (breakOffset <= consumed || breakOffset >= activeHyphenationWord.text.length) continue;
1000
+ if (measureTextWidth(activeHyphenationWord.text.slice(consumed, breakOffset), style) + hyphenWidth <= spaceLeft) fittingBreak = breakOffset;
1001
+ }
1002
+ if (fittingBreak !== void 0) {
1003
+ const absoluteBreak = activeHyphenationWord.start + fittingBreak;
1004
+ const prefix = text.slice(charIndex, absoluteBreak);
1005
+ currentLine.width += measureTextWidth(prefix, style) + hyphenWidth;
1006
+ currentLine.trailingWhitespaceWidth = 0;
1007
+ currentLine.toRun = runIndex;
1008
+ currentLine.toChar = absoluteBreak;
1009
+ currentLine.discretionaryHyphen = { runIndex };
1010
+ startNewLine(runIndex, absoluteBreak);
1011
+ updateMaxFont(lineHeightStyle);
1012
+ charIndex = absoluteBreak;
1013
+ continue;
1014
+ }
1015
+ }
1016
+ if (crossRunWord && currentLine.width + crossRunWord.width > currentLine.availableWidth + widthTolerance) {
1017
+ const spaceLeft = currentLine.availableWidth - currentLine.width + widthTolerance;
1018
+ let fittingPrefix;
1019
+ for (let breakIndex = crossRunWord.breaks.length - 1; breakIndex >= 0; breakIndex -= 1) {
1020
+ const breakOffset = crossRunWord.breaks[breakIndex];
1021
+ if (breakOffset === void 0) continue;
1022
+ if (breakOffset <= 0 || breakOffset >= crossRunWord.text.length) continue;
1023
+ const prefix = measureCrossRunPrefix(crossRunWord, breakOffset);
1024
+ if (prefix && prefix.width + measureTextWidth("-", prefix.hyphenStyle) <= spaceLeft) {
1025
+ fittingPrefix = prefix;
1026
+ break;
1027
+ }
1028
+ }
1029
+ if (fittingPrefix) {
1030
+ for (const segment of fittingPrefix.segments) updateMaxFont(cjkLineHeightStyle(segment.run, segment.style));
1031
+ currentLine.width += fittingPrefix.width + measureTextWidth("-", fittingPrefix.hyphenStyle);
1032
+ currentLine.trailingWhitespaceWidth = 0;
1033
+ currentLine.toRun = fittingPrefix.endRunIndex;
1034
+ currentLine.toChar = fittingPrefix.endChar;
1035
+ currentLine.discretionaryHyphen = { runIndex: fittingPrefix.endRunIndex };
1036
+ startNewLine(fittingPrefix.endRunIndex, fittingPrefix.endChar);
1037
+ if (fittingPrefix.endRunIndex === runIndex) {
1038
+ updateMaxFont(lineHeightStyle);
1039
+ charIndex = fittingPrefix.endChar;
1040
+ continue;
1041
+ }
1042
+ crossRunResume = {
1043
+ runIndex: fittingPrefix.endRunIndex,
1044
+ charIndex: fittingPrefix.endChar
1045
+ };
1046
+ charIndex = text.length;
1047
+ continue;
1048
+ }
1049
+ }
1050
+ if (wordWidth - hangingPunctuationWidth > currentLine.availableWidth + widthTolerance) {
776
1051
  let chunkStart = 0;
777
1052
  while (chunkStart < measuredWord.length) {
778
1053
  const spaceLeft = currentLine.availableWidth - currentLine.width + WIDTH_TOLERANCE;
779
- let bestEnd = findMaxFittingLength(measuredWord.slice(chunkStart), style, spaceLeft);
1054
+ let bestEnd = findMaxFittingLength(measuredWord.slice(chunkStart), style, spaceLeft, currentLine.width === 0, breakPolicy);
780
1055
  if (bestEnd === 0) {
781
- if (currentLine.width > 0) {
782
- startNewLine(runIndex, charIndex + chunkStart);
783
- updateMaxFont(lineHeightStyle);
784
- continue;
785
- }
786
- bestEnd = 1;
1056
+ startNewLine(runIndex, charIndex + chunkStart);
1057
+ updateMaxFont(lineHeightStyle);
1058
+ continue;
787
1059
  }
788
1060
  const chunkEnd = chunkStart + bestEnd;
789
1061
  const chunk = measuredWord.slice(chunkStart, chunkEnd);
790
1062
  const chunkWidth = measureTextWidth(chunk, style);
791
1063
  currentLine.width += chunkWidth;
792
1064
  currentLine.trailingWhitespaceWidth = chunkWidth - measureTextWidth(trimTrailingSpacesAndTabs(chunk), style);
793
- currentLine.regularSpaceCount += chunk.split(" ").length - 1;
794
- currentLine.nonBreakingSpaceCount += chunk.split("\xA0").length - 1;
1065
+ currentLine.regularSpaceWidth += compressibleSpaceWidth(chunk, style);
795
1066
  currentLine.toRun = runIndex;
796
1067
  currentLine.toChar = charIndex + chunkEnd;
797
1068
  chunkStart = chunkEnd;
@@ -803,23 +1074,24 @@ function measureParagraph(block, maxWidth, options) {
803
1074
  const trailingWhitespaceWidth = fullWordWidth - wordWidth;
804
1075
  currentLine.width += trailingWhitespaceWidth;
805
1076
  currentLine.trailingWhitespaceWidth = trailingWhitespaceWidth;
806
- currentLine.regularSpaceCount += word.split(" ").length - 1;
807
- currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
1077
+ currentLine.regularSpaceWidth += compressibleSpaceWidth(word, style);
808
1078
  currentLine.toChar = nextBreak;
809
1079
  charIndex = nextBreak;
810
1080
  continue;
811
1081
  }
812
- const rawGlueWidth = nextBreak === text.length && word.length > 0 && !isBreakChar(word[word.length - 1]) ? trailingGlueWidths[runIndex] ?? 0 : 0;
1082
+ const wordTail = word.at(-1);
1083
+ const protectedGlueWidth = protectedCrossRunGlueWidths[runIndex] ?? 0;
1084
+ const rawGlueWidth = isRunTail && wordTail !== void 0 && (!isBreakChar(wordTail) || protectedGlueWidth > 0) ? Math.max(trailingGlueWidths[runIndex] ?? 0, protectedGlueWidth) : 0;
813
1085
  const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + widthTolerance ? rawGlueWidth : 0;
814
- if (wordWidth > 0 && currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + widthTolerance) {
1086
+ const hangingAllowance = glueWidth === 0 ? hangingPunctuationWidth : 0;
1087
+ if (wordWidth > 0 && currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + widthTolerance + hangingAllowance) {
815
1088
  startNewLine(runIndex, charIndex);
816
1089
  updateMaxFont(lineHeightStyle);
817
1090
  }
818
1091
  currentLine.width += fullWordWidth;
819
1092
  const wordTrailingWhitespaceWidth = fullWordWidth - wordWidth;
820
1093
  currentLine.trailingWhitespaceWidth = wordWidth === 0 ? currentLine.trailingWhitespaceWidth + wordTrailingWhitespaceWidth : wordTrailingWhitespaceWidth;
821
- currentLine.regularSpaceCount += word.split(" ").length - 1;
822
- currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
1094
+ currentLine.regularSpaceWidth += compressibleSpaceWidth(word, style);
823
1095
  currentLine.toRun = runIndex;
824
1096
  currentLine.toChar = nextBreak;
825
1097
  charIndex = nextBreak;
@@ -27,7 +27,8 @@ type FontStyle = {
27
27
  letterSpacing?: number;
28
28
  textTransform?: "uppercase";
29
29
  fontVariant?: "small-caps";
30
- horizontalScale?: number;
30
+ horizontalScale?: number; /** Enable pair kerning for runs whose authored threshold is met. */
31
+ kerning?: boolean;
31
32
  };
32
33
  /**
33
34
  * Typography metrics for a font