@stll/folio-core 0.47.7 → 0.49.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 (213) hide show
  1. package/dist/ai-edits/apply.js +565 -121
  2. package/dist/ai-edits/minimal-replacement.d.ts +64 -0
  3. package/dist/ai-edits/minimal-replacement.js +282 -0
  4. package/dist/controller/fontReadiness.js +3 -3
  5. package/dist/controller/hyphenationReadiness.d.ts +19 -0
  6. package/dist/controller/hyphenationReadiness.js +47 -0
  7. package/dist/controller/layoutPipeline.d.ts +3 -0
  8. package/dist/controller/layoutPipeline.js +70 -18
  9. package/dist/display-list/build/buildDisplayList.js +39 -12
  10. package/dist/display-list/build/pageFurniture.d.ts +7 -1
  11. package/dist/display-list/build/pageFurniture.js +21 -8
  12. package/dist/display-list/build/paragraphPrimitives.js +68 -11
  13. package/dist/display-list/build/tablePrimitives.js +10 -6
  14. package/dist/display-list/build/textBoxPrimitives.js +1 -1
  15. package/dist/display-list/build/unsupported.d.ts +2 -2
  16. package/dist/display-list/build/unsupported.js +0 -0
  17. package/dist/display-list/dom/renderDisplayListToDom.js +0 -1
  18. package/dist/docx/attributeRemainder.js +10 -2
  19. package/dist/docx/blockContentParser.d.ts +2 -0
  20. package/dist/docx/blockContentParser.js +79 -64
  21. package/dist/docx/bulletMarkers.d.ts +18 -2
  22. package/dist/docx/bulletMarkers.js +27 -2
  23. package/dist/docx/commentParser.d.ts +2 -1
  24. package/dist/docx/commentParser.js +57 -45
  25. package/dist/docx/containerChildren.d.ts +21 -8
  26. package/dist/docx/containerChildren.js +39 -9
  27. package/dist/docx/documentParser.d.ts +5 -1
  28. package/dist/docx/documentParser.js +8 -5
  29. package/dist/docx/documentSectionFacts.d.ts +22 -0
  30. package/dist/docx/documentSectionFacts.js +43 -0
  31. package/dist/docx/drawingGroupChildren.d.ts +60 -0
  32. package/dist/docx/drawingGroupChildren.js +176 -0
  33. package/dist/docx/drawingUtils.js +3 -0
  34. package/dist/docx/fieldParser.d.ts +8 -1
  35. package/dist/docx/fieldParser.js +47 -3
  36. package/dist/docx/fontTableParser.js +34 -27
  37. package/dist/docx/footnoteParser.d.ts +3 -2
  38. package/dist/docx/footnoteParser.js +15 -12
  39. package/dist/docx/groupDrawingParser.d.ts +9 -2
  40. package/dist/docx/groupDrawingParser.js +125 -37
  41. package/dist/docx/headerFooterParser.d.ts +7 -2
  42. package/dist/docx/headerFooterParser.js +15 -5
  43. package/dist/docx/hyperlinkParser.d.ts +48 -36
  44. package/dist/docx/hyperlinkParser.js +97 -82
  45. package/dist/docx/numberingParser.js +108 -84
  46. package/dist/docx/packageParts.d.ts +45 -1
  47. package/dist/docx/packageParts.js +93 -2
  48. package/dist/docx/paraIdAttribute.d.ts +3 -2
  49. package/dist/docx/paragraphParser.d.ts +5 -1
  50. package/dist/docx/paragraphParser.js +468 -365
  51. package/dist/docx/paragraphProperties.js +123 -109
  52. package/dist/docx/paragraphPropertySource.d.ts +6 -1
  53. package/dist/docx/paragraphPropertySource.js +9 -1
  54. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  55. package/dist/docx/paragraphTextBoxEnrichment.js +143 -10
  56. package/dist/docx/parser.d.ts +7 -1
  57. package/dist/docx/parser.js +33 -16
  58. package/dist/docx/previewBudget.d.ts +55 -33
  59. package/dist/docx/previewBudget.js +59 -65
  60. package/dist/docx/rezip.js +41 -58
  61. package/dist/docx/runParser.d.ts +4 -1
  62. package/dist/docx/runParser.js +182 -184
  63. package/dist/docx/sdtProperties.js +87 -84
  64. package/dist/docx/sectionParser.js +169 -160
  65. package/dist/docx/selectiveSaveFlags.d.ts +10 -3
  66. package/dist/docx/selectiveSaveFlags.js +1 -1
  67. package/dist/docx/selectiveXmlPatch.d.ts +56 -34
  68. package/dist/docx/selectiveXmlPatch.js +222 -115
  69. package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
  70. package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
  71. package/dist/docx/serializer/paragraphSerializer.js +47 -42
  72. package/dist/docx/serializer/runSerializer.d.ts +5 -1
  73. package/dist/docx/serializer/runSerializer.js +4 -3
  74. package/dist/docx/serializer/settingsSerializer.js +19 -1
  75. package/dist/docx/settingsParser.js +8 -0
  76. package/dist/docx/shapeAlternateContent.d.ts +25 -0
  77. package/dist/docx/shapeAlternateContent.js +52 -0
  78. package/dist/docx/streamingXmlParser.d.ts +6 -2
  79. package/dist/docx/streamingXmlParser.js +12 -7
  80. package/dist/docx/styleParser.js +34 -8
  81. package/dist/docx/tableParser.d.ts +21 -12
  82. package/dist/docx/tableParser.js +518 -441
  83. package/dist/docx/textBoxParser.d.ts +18 -5
  84. package/dist/docx/textBoxParser.js +12 -6
  85. package/dist/docx/verbatimCapture.d.ts +10 -8
  86. package/dist/docx/verbatimCapture.js +45 -2
  87. package/dist/docx/vmlImageParser.d.ts +15 -13
  88. package/dist/docx/vmlImageParser.js +75 -60
  89. package/dist/docx/vmlPreview.d.ts +3 -3
  90. package/dist/docx/vmlPreview.js +3 -4
  91. package/dist/docx/xmlNamespaceContext.d.ts +9 -0
  92. package/dist/docx/xmlNamespaceContext.js +54 -0
  93. package/dist/docx/xmlParser.d.ts +6 -3
  94. package/dist/docx/xmlParser.js +11 -41
  95. package/dist/docx/xmlResourceLimits.d.ts +2 -1
  96. package/dist/docx/xmlResourceLimits.js +4 -1
  97. package/dist/fonts/headlessMeasure.js +0 -0
  98. package/dist/headless-layout.d.ts +11 -2
  99. package/dist/headless-layout.js +153 -107
  100. package/dist/internal/headlessRevisionResolution.js +50 -13
  101. package/dist/internal/paragraphFormattingSerialization.js +3 -3
  102. package/dist/layout-bridge/convert/endnoteLayout.d.ts +65 -0
  103. package/dist/layout-bridge/convert/endnoteLayout.js +227 -0
  104. package/dist/layout-bridge/convert/footnoteLayout.d.ts +19 -3
  105. package/dist/layout-bridge/convert/footnoteLayout.js +40 -31
  106. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -7
  107. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -20
  108. package/dist/layout-bridge/convert/paragraphMarkFormatting.d.ts +10 -0
  109. package/dist/layout-bridge/convert/paragraphMarkFormatting.js +59 -0
  110. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +29 -1
  111. package/dist/layout-bridge/convert/toFlowBlocks.js +284 -39
  112. package/dist/layout-bridge/engine/hitTest.js +5 -3
  113. package/dist/layout-bridge/engine/selectionRects.js +5 -3
  114. package/dist/layout-engine/anchorLayoutInCellCompatibility.d.ts +10 -0
  115. package/dist/layout-engine/anchorLayoutInCellCompatibility.js +22 -0
  116. package/dist/layout-engine/index.d.ts +2 -2
  117. package/dist/layout-engine/index.js +102 -14
  118. package/dist/layout-engine/justificationCompatibility.d.ts +1 -1
  119. package/dist/layout-engine/justificationCompatibility.js +9 -5
  120. package/dist/layout-engine/keep-together.d.ts +15 -6
  121. package/dist/layout-engine/keep-together.js +43 -16
  122. package/dist/layout-engine/layoutInstrumentation.d.ts +8 -2
  123. package/dist/layout-engine/layoutInstrumentation.js +8 -1
  124. package/dist/layout-engine/measure/advanceComposition.js +50 -7
  125. package/dist/layout-engine/measure/hyphenationDictionaries.d.ts +74 -0
  126. package/dist/layout-engine/measure/hyphenationDictionaries.js +138 -0
  127. package/dist/layout-engine/measure/hyphenationPreload.d.ts +11 -0
  128. package/dist/layout-engine/measure/hyphenationPreload.js +24 -0
  129. package/dist/layout-engine/measure/lineBreakProvider.d.ts +12 -2
  130. package/dist/layout-engine/measure/lineBreakProvider.js +30 -30
  131. package/dist/layout-engine/measure/lineBreaks.d.ts +6 -1
  132. package/dist/layout-engine/measure/lineBreaks.js +10 -3
  133. package/dist/layout-engine/measure/listMarkerWidth.d.ts +9 -6
  134. package/dist/layout-engine/measure/listMarkerWidth.js +20 -22
  135. package/dist/layout-engine/measure/measureBlocks.js +46 -10
  136. package/dist/layout-engine/measure/measureContainer.js +51 -8
  137. package/dist/layout-engine/measure/measureHelpers.d.ts +15 -3
  138. package/dist/layout-engine/measure/measureHelpers.js +34 -8
  139. package/dist/layout-engine/measure/measureParagraph.js +143 -101
  140. package/dist/layout-engine/measure/measureTypes.d.ts +5 -0
  141. package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
  142. package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
  143. package/dist/layout-engine/measure/tabCalculator.d.ts +2 -0
  144. package/dist/layout-engine/measure/tabCalculator.js +2 -1
  145. package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
  146. package/dist/layout-engine/measure/tableCellFloating.js +51 -19
  147. package/dist/layout-engine/measure/tableCellGrid.d.ts +28 -3
  148. package/dist/layout-engine/measure/tableCellGrid.js +45 -5
  149. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +2 -1
  150. package/dist/layout-engine/noteAreaFlow.d.ts +19 -0
  151. package/dist/layout-engine/noteAreaFlow.js +83 -0
  152. package/dist/layout-engine/paginator.d.ts +12 -1
  153. package/dist/layout-engine/paginator.js +18 -2
  154. package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
  155. package/dist/layout-engine/paragraphSpacing.js +2 -1
  156. package/dist/layout-engine/tableRowBreak.d.ts +8 -1
  157. package/dist/layout-engine/tableRowBreak.js +25 -2
  158. package/dist/layout-engine/types.d.ts +108 -3
  159. package/dist/layout-engine/types.js +96 -1
  160. package/dist/layout-painter/index.d.ts +3 -0
  161. package/dist/layout-painter/renderPage.js +32 -83
  162. package/dist/layout-painter/renderParagraph.d.ts +6 -1
  163. package/dist/layout-painter/renderParagraph.js +75 -26
  164. package/dist/layout-painter/renderTable.js +26 -13
  165. package/dist/layout-painter/renderTextBox.d.ts +12 -2
  166. package/dist/layout-painter/renderTextBox.js +26 -2
  167. package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
  168. package/dist/prosemirror/alternateContentAttrs.js +35 -0
  169. package/dist/prosemirror/attrs/index.d.ts +9 -9
  170. package/dist/prosemirror/attrs/index.js +133 -11
  171. package/dist/prosemirror/commands/comments.js +51 -41
  172. package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
  173. package/dist/prosemirror/contentControlRevisions.js +84 -0
  174. package/dist/prosemirror/conversion/fromProseDoc.js +54 -23
  175. package/dist/prosemirror/conversion/markInterner.d.ts +21 -0
  176. package/dist/prosemirror/conversion/markInterner.js +90 -0
  177. package/dist/prosemirror/conversion/toProseDoc.js +117 -58
  178. package/dist/prosemirror/extensions/core/DocExtension.js +2 -1
  179. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  180. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
  181. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
  182. package/dist/prosemirror/extensions/marks/markUtils.d.ts +6 -2
  183. package/dist/prosemirror/extensions/marks/markUtils.js +39 -51
  184. package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
  185. package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
  186. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
  187. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +5 -1
  188. package/dist/prosemirror/listMarker.d.ts +1 -0
  189. package/dist/prosemirror/listMarker.js +4 -2
  190. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  191. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  192. package/dist/prosemirror/paragraphDirection.d.ts +29 -2
  193. package/dist/prosemirror/paragraphDirection.js +21 -2
  194. package/dist/prosemirror/plugins/suggestionMode.js +37 -8
  195. package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
  196. package/dist/prosemirror/rejoinRunCarriers.js +66 -0
  197. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
  198. package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
  199. package/dist/prosemirror/schema/nodes.d.ts +50 -6
  200. package/dist/prosemirror/textBoxHostParagraph.d.ts +13 -0
  201. package/dist/prosemirror/textBoxHostParagraph.js +21 -0
  202. package/dist/prosemirror/validation.js +91 -80
  203. package/dist/types/content.d.ts +2 -2
  204. package/dist/utils/createDocument.js +10 -1
  205. package/dist/utils/scriptSegments.d.ts +33 -4
  206. package/dist/utils/scriptSegments.js +39 -6
  207. package/dist/utils/textFormattingMerge.d.ts +17 -3
  208. package/dist/utils/textFormattingMerge.js +18 -12
  209. package/dist/utils/trailingText.d.ts +22 -0
  210. package/dist/utils/trailingText.js +32 -0
  211. package/package.json +2 -2
  212. package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
  213. package/dist/layout-engine/justifiedLineFit.js +0 -6
@@ -1,8 +1,8 @@
1
1
  import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
2
2
  import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
3
3
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
4
- import { hasCjk, hasComplexScript } from "../../utils/scriptSegments.js";
5
- import { JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO, supportsJustifiedFinalLineContraction } from "../justifiedLineFit.js";
4
+ import { hasCjk, hasComplexScript, hasEastAsiaSlotText } from "../../utils/scriptSegments.js";
5
+ import { splitTrailingToken } from "../../utils/trailingText.js";
6
6
  import { measuredLineAdvance } from "../lineFlow.js";
7
7
  import { isFloatingImageRun } from "../types.js";
8
8
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
@@ -10,9 +10,9 @@ import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./comp
10
10
  import { resolveEffectiveLineBreakPolicy } from "./effectiveLineBreakPolicy.js";
11
11
  import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
12
12
  import { defaultLineBreakProvider, getLineBreakProvider } from "./lineBreakProvider.js";
13
- import { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakChar, isHangingPunctuation } from "./lineBreaks.js";
13
+ import { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakBetween, isBreakChar, isHangingPunctuation } from "./lineBreaks.js";
14
14
  import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
15
- import { buildRunFontStyle, ptToPx, twipsToPx } from "./measureHelpers.js";
15
+ import { DEFAULT_FONT_FAMILY, buildRunFontStyle, ptToPx, twipsToPx } from "./measureHelpers.js";
16
16
  import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
17
17
  import { calculateTabWidth, pixelsToTwips } from "./tabCalculator.js";
18
18
  import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
@@ -24,16 +24,14 @@ import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
24
24
  * Converts runs into measured lines with typography metrics.
25
25
  */
26
26
  const DEFAULT_FONT_SIZE = 11;
27
- const DEFAULT_FONT_FAMILY = "Calibri";
28
27
  const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
29
28
  const WIDTH_TOLERANCE = .5;
30
29
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
31
- const JUSTIFY_SPACE_CONTRACTION_RATIO = .075;
32
- const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
33
- const JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO = .022;
30
+ const JUSTIFY_SPACE_CONTRACTION_RATIO = .25;
31
+ const JUSTIFY_STRETCH_PER_SHRINK = 1.7;
32
+ const JUSTIFY_ALWAYS_SHRINK_STRETCH = 1.5;
34
33
  const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
35
34
  const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
36
- const DEFAULT_LIST_HANGING_INDENT_PX = 24;
37
35
  const ALL_CAPS_RATIO_THRESHOLD = .8;
38
36
  /**
39
37
  * Find the longest prefix of `text` that fits within `maxWidth` pixels.
@@ -85,9 +83,11 @@ function runToFontStyle(run) {
85
83
  * keeps the base style so wrapping is untouched.
86
84
  */
87
85
  function cjkLineHeightStyle(run, baseStyle) {
88
- if (!run.text || !hasCjk(run.text)) return baseStyle;
86
+ if (!run.text) return baseStyle;
89
87
  const eastAsia = baseStyle.eastAsiaFontFamily;
90
- if (eastAsia !== void 0 && isCjkFont(eastAsia)) {
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
91
  const result = {
92
92
  ...baseStyle,
93
93
  fontFamily: eastAsia
@@ -96,7 +96,7 @@ function cjkLineHeightStyle(run, baseStyle) {
96
96
  if (baseStyle.eastAsiaAlternateFontFamily !== void 0) result.alternateFontFamily = baseStyle.eastAsiaAlternateFontFamily;
97
97
  return result;
98
98
  }
99
- if (isCjkFont(baseStyle.fontFamily ?? DEFAULT_FONT_FAMILY)) return baseStyle;
99
+ if (isCjkFont(baseStyle.fontFamily ?? "Times New Roman")) return baseStyle;
100
100
  return {
101
101
  ...baseStyle,
102
102
  fontFamily: CJK_FALLBACK_FONT_FAMILY
@@ -153,7 +153,7 @@ const WORD_SINGLE_LINE_FLOOR = 1.15;
153
153
  function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily, attrs) {
154
154
  const result = calculateTypographyMetrics(fontSize, spacing, getFontMetrics({
155
155
  fontSize,
156
- fontFamily: fontFamily ?? DEFAULT_FONT_FAMILY,
156
+ fontFamily: fontFamily ?? "Times New Roman",
157
157
  ...attrs?.defaultAlternateFontFamily !== void 0 ? { alternateFontFamily: attrs.defaultAlternateFontFamily } : {}
158
158
  }));
159
159
  const lineRule = spacing?.lineRule ?? "auto";
@@ -369,22 +369,6 @@ function uppercaseLetterRatio(text) {
369
369
  }
370
370
  function resolveJustifyFitStrategy(block, isFirstLine, profile) {
371
371
  if (block.attrs?.justificationCompatibility?.type === "legacy") return { type: "rounding" };
372
- if (isShallowFullHangingListContinuation(block, isFirstLine)) return { type: "rounding" };
373
- if (block.attrs?.listMarker !== void 0) {
374
- const hanging = block.attrs.indent?.hanging ?? 0;
375
- if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ? {
376
- type: "space",
377
- ratio: JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO
378
- } : {
379
- type: "width",
380
- ratio: JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO
381
- };
382
- return {
383
- type: "space",
384
- ratio: JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO,
385
- maxWidthRatio: JUSTIFY_SHRINK_TOLERANCE_RATIO
386
- };
387
- }
388
372
  const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
389
373
  if (isFirstLine && profile.hasTabRuns && (block.attrs?.indent?.hanging ?? 0) > 0) return {
390
374
  type: "width",
@@ -407,19 +391,6 @@ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
407
391
  ratio: JUSTIFY_SPACE_CONTRACTION_RATIO
408
392
  };
409
393
  }
410
- function resolveFinalLineJustifyFitStrategy(block, profile) {
411
- if (supportsJustifiedFinalLineContraction(block)) return {
412
- type: "space",
413
- ratio: JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO,
414
- maxWidthRatio: JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO
415
- };
416
- return resolveJustifyFitStrategy(block, false, profile);
417
- }
418
- function resolveCandidateJustifyFitStrategy({ isFirstLine, isFinalCandidate, firstLineStrategy, continuationStrategy, finalLineStrategy }) {
419
- if (isFirstLine) return firstLineStrategy;
420
- if (isFinalCandidate) return finalLineStrategy;
421
- return continuationStrategy;
422
- }
423
394
  const isIgnorableFinalTailRun = (run) => run.kind === "renderedPageBreak" || isTextRun(run) && run.text.length === 0;
424
395
  function isFinalTextCandidate(block, runIndex, nextBreak) {
425
396
  const currentRun = block.runs[runIndex];
@@ -431,27 +402,34 @@ function isFinalTextCandidate(block, runIndex, nextBreak) {
431
402
  }
432
403
  return true;
433
404
  }
434
- function resolveTextCandidateFit({ block, line, isFirstLine, isFinalCandidate, candidateWidth, candidateSpaceWidth, fallbackTolerancePx, continuationStrategy, finalStrategy }) {
435
- if (!isFinalCandidate || !supportsJustifiedFinalLineContraction(block)) return {
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 {
436
411
  type: "ordinary",
437
- tolerancePx: fallbackTolerancePx
412
+ tolerancePx
438
413
  };
439
- const ordinaryTolerancePx = isFirstLine ? fallbackTolerancePx : justifyFitTolerance(line, continuationStrategy, candidateSpaceWidth);
440
- if (candidateWidth <= line.availableWidth + ordinaryTolerancePx) return {
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 {
441
424
  type: "ordinary",
442
- tolerancePx: ordinaryTolerancePx
443
- };
444
- const finalTolerancePx = justifyFitTolerance(line, finalStrategy, candidateSpaceWidth);
445
- if (candidateWidth > line.availableWidth + finalTolerancePx) return {
446
- type: "final-contraction-rejected",
447
- tolerancePx: finalTolerancePx
425
+ tolerancePx
448
426
  };
449
427
  return {
450
428
  type: "final-contraction-admitted",
451
- tolerancePx: finalTolerancePx,
429
+ tolerancePx,
452
430
  paint: {
453
431
  type: "space-contraction",
454
- contractionPx: candidateWidth - line.availableWidth
432
+ contractionPx
455
433
  }
456
434
  };
457
435
  }
@@ -459,18 +437,35 @@ function compressibleSpaceWidth(text, style) {
459
437
  const count = countCompressibleSpaces(text);
460
438
  return count === 0 ? 0 : count * measureTextWidth(" ", style);
461
439
  }
462
- function justifyFitTolerance(line, strategy, candidateSpaceWidth) {
440
+ function justifyFitTolerance(line, strategy, candidateSpaceWidth, candidate) {
463
441
  if (strategy.type === "rounding") return WIDTH_TOLERANCE;
464
442
  if (strategy.type === "width") return Math.max(WIDTH_TOLERANCE, line.availableWidth * strategy.ratio);
465
- const spaceTolerance = (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio;
466
- const boundedTolerance = strategy.maxWidthRatio === void 0 ? spaceTolerance : Math.min(spaceTolerance, line.availableWidth * strategy.maxWidthRatio);
467
- return Math.max(WIDTH_TOLERANCE, boundedTolerance);
443
+ if (candidate !== void 0 && !prefersSpaceShrink(line, candidate)) return WIDTH_TOLERANCE;
444
+ return Math.max(WIDTH_TOLERANCE, (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio);
468
445
  }
469
- function isShallowFullHangingListContinuation(block, isFirstLine) {
470
- if (isFirstLine || block.attrs?.listMarker === void 0) return false;
471
- const hanging = block.attrs.indent?.hanging ?? 0;
472
- const left = block.attrs.indent?.left ?? 0;
473
- return hanging > 0 && hanging < DEFAULT_LIST_HANGING_INDENT_PX && Math.abs(left - hanging) <= WIDTH_TOLERANCE;
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;
474
469
  }
475
470
  function trimTrailingSpacesAndTabs(text) {
476
471
  let end = text.length;
@@ -503,6 +498,21 @@ function measureWordWithTrailingWhitespace(word, style) {
503
498
  fullWordWidth: wordWidth + trailingWhitespaceWidth + boundarySpacing
504
499
  };
505
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
+ }
506
516
  function trailingCodePoint(text) {
507
517
  if (text.length === 0) return;
508
518
  const trailing = text.charCodeAt(text.length - 1);
@@ -548,6 +558,21 @@ function computeTrailingGlueWidths(block) {
548
558
  }
549
559
  return widths;
550
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
+ }
551
576
  function computeProtectedCrossRunGlueWidths(block) {
552
577
  const widths = Array.from({ length: block.runs.length }, () => 0);
553
578
  for (let index = 0; index < block.runs.length; index++) {
@@ -558,10 +583,10 @@ function computeProtectedCrossRunGlueWidths(block) {
558
583
  run
559
584
  }).provider;
560
585
  if (!policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
561
- const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
562
- const token = trailing?.[1];
563
- if (!token || token.length !== 1) continue;
564
- let separator = trailing[2] ?? "";
586
+ const trailing = splitTrailingToken(run.text ?? "");
587
+ if (!trailing || trailing.token.length !== 1) continue;
588
+ const { token } = trailing;
589
+ let separator = trailing.separator;
565
590
  let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
566
591
  let followingWord = "";
567
592
  let unseparatedFollowingText = false;
@@ -720,7 +745,6 @@ function measureParagraph(block, maxWidth, options) {
720
745
  };
721
746
  const firstLineJustifyFitStrategy = resolveJustifyFitStrategy(block, true, justificationProfile);
722
747
  const continuationJustifyFitStrategy = resolveJustifyFitStrategy(block, false, justificationProfile);
723
- const finalLineJustifyFitStrategy = resolveFinalLineJustifyFitStrategy(block, justificationProfile);
724
748
  const floatingZones = options?.floatingZones;
725
749
  const paragraphYOffset = options?.paragraphYOffset ?? 0;
726
750
  const indent = attrs?.indent;
@@ -772,7 +796,7 @@ function measureParagraph(block, maxWidth, options) {
772
796
  };
773
797
  }
774
798
  if (runs.length === 0) {
775
- const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
799
+ const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? "Times New Roman", attrs);
776
800
  const outlineLineHeight = attrs?.reserveEmptyOutlineHeight ? emptyMetrics.lineHeight * 2 : emptyMetrics.lineHeight;
777
801
  lines.push({
778
802
  fromRun: 0,
@@ -793,7 +817,7 @@ function measureParagraph(block, maxWidth, options) {
793
817
  };
794
818
  }
795
819
  if (runs.length > 0 && runs.every((run) => isTextRun(run) && isEmptyTextRun(run))) {
796
- const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
820
+ const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? "Times New Roman", attrs);
797
821
  lines.push({
798
822
  fromRun: 0,
799
823
  fromChar: 0,
@@ -813,6 +837,7 @@ function measureParagraph(block, maxWidth, options) {
813
837
  }
814
838
  const trailingGlueWidths = computeTrailingGlueWidths(block);
815
839
  const protectedCrossRunGlueWidths = computeProtectedCrossRunGlueWidths(block);
840
+ const followingTextLeads = computeFollowingTextLeads(block);
816
841
  let currentLine = {
817
842
  fromRun: 0,
818
843
  fromChar: 0,
@@ -823,6 +848,8 @@ function measureParagraph(block, maxWidth, options) {
823
848
  regularSpaceWidth: 0,
824
849
  maxFontSize: DEFAULT_FONT_SIZE,
825
850
  maxFontMetrics: null,
851
+ maxTabFontSize: DEFAULT_FONT_SIZE,
852
+ maxTabFontMetrics: null,
826
853
  maxImageHeightPx: 0,
827
854
  maxExactImageHeightPx: 0,
828
855
  maxMathHeightPx: 0,
@@ -833,9 +860,14 @@ function measureParagraph(block, maxWidth, options) {
833
860
  };
834
861
  const calculateLineTypography = (line) => {
835
862
  const paragraphFontSize = attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE;
836
- const paragraphFontFamily = attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY;
837
- const fontSize = line.maxFontMetrics ? line.maxFontSize : paragraphFontSize;
838
- const metrics = line.maxFontMetrics ?? getFontMetrics({
863
+ const paragraphFontFamily = attrs?.defaultFontFamily ?? "Times New Roman";
864
+ let lineFontMetrics = line.maxFontMetrics;
865
+ let fontSize = line.maxFontSize;
866
+ if (!lineFontMetrics) {
867
+ lineFontMetrics = line.maxTabFontMetrics;
868
+ fontSize = lineFontMetrics ? line.maxTabFontSize : paragraphFontSize;
869
+ }
870
+ const metrics = lineFontMetrics ?? getFontMetrics({
839
871
  fontSize: paragraphFontSize,
840
872
  fontFamily: paragraphFontFamily,
841
873
  ...attrs?.defaultAlternateFontFamily !== void 0 ? { alternateFontFamily: attrs.defaultAlternateFontFamily } : {}
@@ -919,6 +951,8 @@ function measureParagraph(block, maxWidth, options) {
919
951
  regularSpaceWidth: 0,
920
952
  maxFontSize: DEFAULT_FONT_SIZE,
921
953
  maxFontMetrics: null,
954
+ maxTabFontSize: DEFAULT_FONT_SIZE,
955
+ maxTabFontMetrics: null,
922
956
  maxImageHeightPx: 0,
923
957
  maxExactImageHeightPx: 0,
924
958
  maxMathHeightPx: 0,
@@ -942,6 +976,19 @@ function measureParagraph(block, maxWidth, options) {
942
976
  const metrics = getFontMetrics(style);
943
977
  if (metrics.singleLineRatio > currentLine.maxFontMetrics.singleLineRatio) currentLine.maxFontMetrics = metrics;
944
978
  };
979
+ /**
980
+ * Track the tallest tab on the line separately from content runs: a tab's
981
+ * font does not raise a line that carries other content, so a tab run
982
+ * formatted larger than its neighbouring text (a TOC entry whose tab keeps
983
+ * the paragraph-mark size) leaves the line at the text's height.
984
+ */
985
+ const updateMaxTabFont = (style) => {
986
+ const fontSize = style.fontSize ?? DEFAULT_FONT_SIZE;
987
+ if (!currentLine.maxTabFontMetrics || fontSize > currentLine.maxTabFontSize) {
988
+ currentLine.maxTabFontSize = fontSize;
989
+ currentLine.maxTabFontMetrics = getFontMetrics(style);
990
+ }
991
+ };
945
992
  let crossRunResume;
946
993
  for (let runIndex = 0; runIndex < runs.length; runIndex++) {
947
994
  if (crossRunResume && runIndex < crossRunResume.runIndex) continue;
@@ -970,7 +1017,7 @@ function measureParagraph(block, maxWidth, options) {
970
1017
  }
971
1018
  if (isTabRun(run)) {
972
1019
  const style = runToFontStyle(run);
973
- updateMaxFont(style);
1020
+ updateMaxTabFont(style);
974
1021
  const followingWidth = measureInlineWidthAfterTab(runs, runIndex, options?.fieldValues);
975
1022
  const decimalPrefixWidth = measureDecimalPrefixWidthAfterTab(runs, runIndex, options?.fieldValues);
976
1023
  const lineX = currentLine.width + currentLine.leftOffset;
@@ -987,14 +1034,14 @@ function measureParagraph(block, maxWidth, options) {
987
1034
  let tabWidth = tabResult.width;
988
1035
  const authoredEndpoint = contentX + tabWidth + followingWidth;
989
1036
  const activeContentRightEdge = maxWidth - currentLine.rightOffset;
990
- const preservesAuthoredEndStop = tabResult.alignment === "end" && (options?.allowEndTabOverflow === true || authoredEndpoint <= activeContentRightEdge + WIDTH_TOLERANCE);
1037
+ const preservesAuthoredStop = tabResult.alignment === "end" && options?.allowEndTabOverflow === true || tabResult.explicit === true && authoredEndpoint <= activeContentRightEdge + WIDTH_TOLERANCE;
991
1038
  const landsOnLeftIndent = tabResult.alignment === "start" && indentLeft > 0 && Math.abs(contentX + tabWidth - indentLeft) <= WIDTH_TOLERANCE;
992
1039
  const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
993
- if (!preservesAuthoredEndStop && !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);
994
- if (preservesAuthoredEndStop) currentLine.availableWidth = Math.max(currentLine.availableWidth, currentLine.width + tabWidth + followingWidth);
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);
1041
+ if (preservesAuthoredStop) currentLine.availableWidth = Math.max(currentLine.availableWidth, currentLine.width + tabWidth + followingWidth);
995
1042
  if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
996
1043
  startNewLine(runIndex, 0);
997
- updateMaxFont(style);
1044
+ updateMaxTabFont(style);
998
1045
  }
999
1046
  currentLine.width += tabWidth;
1000
1047
  currentLine.trailingWhitespaceWidth = 0;
@@ -1039,7 +1086,10 @@ function measureParagraph(block, maxWidth, options) {
1039
1086
  updateMaxFont(style);
1040
1087
  const fieldWidth = measureTextWidth(fallback, style);
1041
1088
  const fieldSpaceWidth = compressibleSpaceWidth(fallback, style);
1042
- const fieldTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, lines.length === 0 ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, fieldSpaceWidth) : WIDTH_TOLERANCE;
1089
+ const fieldTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, lines.length === 0 ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, fieldSpaceWidth, {
1090
+ width: currentLine.width + fieldWidth,
1091
+ innerSpaceWidth: fieldSpaceWidth
1092
+ }) : WIDTH_TOLERANCE;
1043
1093
  if (currentLine.width > 0 && currentLine.width + fieldWidth > currentLine.availableWidth + fieldTolerance) {
1044
1094
  startNewLine(runIndex, 0);
1045
1095
  updateMaxFont(style);
@@ -1098,18 +1148,20 @@ function measureParagraph(block, maxWidth, options) {
1098
1148
  const nextBreak = wordBreaks[wordBreakIndex] ?? text.length;
1099
1149
  const word = text.slice(charIndex, nextBreak);
1100
1150
  const { measuredWord, wordWidth, fullWordWidth } = measureWordWithTrailingWhitespace(word, style);
1101
- let leadingLetterSpacing = (currentLine.fromRun !== currentLine.toRun || currentLine.fromChar !== currentLine.toChar) && currentLine.toRun === runIndex && currentLine.toChar === charIndex ? (style.letterSpacing ?? 0) * ((style.horizontalScale ?? 100) / 100) : 0;
1151
+ let leadingLetterSpacing = precedingLetterSpacing({
1152
+ block,
1153
+ line: currentLine,
1154
+ runIndex,
1155
+ charIndex,
1156
+ runStyle: style
1157
+ });
1102
1158
  const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, effectiveLineBreakPolicy.hangingPunctuation);
1103
1159
  const isFirstLine = lines.length === 0;
1104
1160
  const regularSpaceWidth = compressibleSpaceWidth(measuredWord, style);
1105
- const justifyFitStrategy = resolveCandidateJustifyFitStrategy({
1106
- isFirstLine,
1107
- isFinalCandidate: isFinalTextCandidate(block, runIndex, nextBreak),
1108
- firstLineStrategy: firstLineJustifyFitStrategy,
1109
- continuationStrategy: continuationJustifyFitStrategy,
1110
- finalLineStrategy: finalLineJustifyFitStrategy
1111
- });
1112
- const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, justifyFitStrategy, regularSpaceWidth) : WIDTH_TOLERANCE;
1161
+ const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, isFirstLine ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, regularSpaceWidth, {
1162
+ width: currentLine.width + leadingLetterSpacing + wordWidth,
1163
+ innerSpaceWidth: 0
1164
+ }) : WIDTH_TOLERANCE;
1113
1165
  const automaticHyphenation = effectiveLineBreakPolicy.automaticHyphenation;
1114
1166
  const mayHyphenateLine = automaticHyphenation.type === "enabled" && (automaticHyphenation.consecutiveLineLimit === 0 || consecutiveHyphenatedLines < automaticHyphenation.consecutiveLineLimit) && exceedsHyphenationZone(currentLine, automaticHyphenation.hyphenationZoneTwips);
1115
1167
  const isRunTail = nextBreak === text.length;
@@ -1223,20 +1275,10 @@ function measureParagraph(block, maxWidth, options) {
1223
1275
  }
1224
1276
  const wordTail = word.at(-1);
1225
1277
  const protectedGlueWidth = protectedCrossRunGlueWidths[runIndex] ?? 0;
1226
- const rawGlueWidth = isRunTail && wordTail !== void 0 && (!isBreakChar(wordTail) || protectedGlueWidth > 0) ? Math.max(trailingGlueWidths[runIndex] ?? 0, protectedGlueWidth) : 0;
1278
+ const rawGlueWidth = isRunTail && wordTail !== void 0 && (!isBreakBetween(wordTail, followingTextLeads[runIndex]) || protectedGlueWidth > 0) ? Math.max(trailingGlueWidths[runIndex] ?? 0, protectedGlueWidth) : 0;
1227
1279
  const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + widthTolerance ? rawGlueWidth : 0;
1228
1280
  const hangingAllowance = glueWidth === 0 ? hangingPunctuationWidth : 0;
1229
- const candidateFit = isJustifiedParagraph ? resolveTextCandidateFit({
1230
- block,
1231
- line: currentLine,
1232
- isFirstLine,
1233
- isFinalCandidate: isFinalTextCandidate(block, runIndex, nextBreak),
1234
- candidateWidth: currentLine.width + leadingLetterSpacing + wordWidth + glueWidth - hangingAllowance,
1235
- candidateSpaceWidth: regularSpaceWidth,
1236
- fallbackTolerancePx: widthTolerance,
1237
- continuationStrategy: continuationJustifyFitStrategy,
1238
- finalStrategy: finalLineJustifyFitStrategy
1239
- }) : void 0;
1281
+ const candidateFit = isJustifiedParagraph ? resolveTextCandidateFit(isFinalTextCandidate(block, runIndex, nextBreak), currentLine.width + leadingLetterSpacing + wordWidth + glueWidth - hangingAllowance, currentLine.availableWidth, widthTolerance) : void 0;
1240
1282
  const finalWrapTolerance = candidateFit?.tolerancePx ?? widthTolerance;
1241
1283
  if (wordWidth > 0 && currentLine.width > 0 && currentLine.width + leadingLetterSpacing + wordWidth + glueWidth > currentLine.availableWidth + finalWrapTolerance + hangingAllowance) {
1242
1284
  startNewLine(runIndex, charIndex);
@@ -23,6 +23,11 @@ type FontStyle = {
23
23
  */
24
24
  eastAsiaFontFamily?: string;
25
25
  eastAsiaAlternateFontFamily?: string;
26
+ /**
27
+ * The run's effective `w:hint="eastAsia"`: the code points it moves measure
28
+ * with `eastAsiaFontFamily` too.
29
+ */
30
+ eastAsiaHint?: boolean;
26
31
  /**
27
32
  * Complex-script font for Arabic, Hebrew, Indic and South-East Asian code
28
33
  * points. Same contract as `eastAsiaFontFamily`, over a different slot.
@@ -0,0 +1,56 @@
1
+ //#region src/layout-engine/measure/smallCapsCasing.d.ts
2
+ /**
3
+ * Casing rules for synthesized `w:smallCaps` glyphs.
4
+ *
5
+ * A face that carries no `smcp`/`c2sc` substitution has no true small-capital
6
+ * glyph, so it is faked: a lowercase letter is drawn as its uppercase form at
7
+ * a smaller point size, on the run's own baseline, at a fixed fraction of the
8
+ * run's declared size — 0.8, an exact ratio observed to repeat across mixed
9
+ * two-size runs at every point size and never round to a half-point `w:sz`
10
+ * step. That is not a canvas or DOM backend's own `font-variant: small-caps`,
11
+ * which synthesizes at its own, uncontrollable, narrower ratio (Blink and
12
+ * WebKit use 0.7).
13
+ *
14
+ * Pure: no canvas, no DOM. Shared by every measurement and paint backend so a
15
+ * run's advances and its glyphs agree on which characters shrink.
16
+ */
17
+ /** Fraction of the run's declared size a synthesized small capital draws at. */
18
+ declare const SMALL_CAPS_SCALE = 0.8;
19
+ /**
20
+ * Whether `char` is a cased letter in its lowercase form — the class
21
+ * `w:smallCaps` draws as a shrunken, uppercased capital. Digits, punctuation,
22
+ * symbols and already-uppercase letters keep the run's full size; combining
23
+ * `char`'s upper and lower forms this way (rather than comparing to a
24
+ * separate "is uppercase" test) is what correctly leaves caseless scripts
25
+ * (CJK, most punctuation) at full size too, since neither form differs from
26
+ * the character itself there.
27
+ */
28
+ declare function isSmallCapsLetter(char: string): boolean;
29
+ /**
30
+ * Per-code-point small-caps classification for `text`, one entry per code
31
+ * point (iterated the same way `[...text]`/`for...of` does, so it lines up
32
+ * with any other code-point walk of the same string).
33
+ *
34
+ * Whitespace carries no case of its own; a run's space draws at the size of
35
+ * the character immediately before it — the trailing space after a
36
+ * synthesized word is itself synthesized — so a whitespace code point here
37
+ * takes the previous code point's class, and only falls back to full size
38
+ * when it opens the string (nothing to inherit from).
39
+ */
40
+ declare function smallCapsMask(text: string): boolean[];
41
+ /** A maximal run of `text` drawn at one size: `small` selects which. */
42
+ type SmallCapsSegment = {
43
+ readonly text: string;
44
+ readonly small: boolean;
45
+ };
46
+ /**
47
+ * `text` split into maximal same-size segments for painting, each already
48
+ * uppercased where `small` — the shrunken capital a `smcp`-less face
49
+ * substitutes for the source lowercase letter. A run with no lowercase letter
50
+ * at all (already-uppercase headings, digits, punctuation) comes back as one
51
+ * `small: false` segment, so a caller can skip the extra painting work its
52
+ * mixed-size runs need.
53
+ */
54
+ declare function smallCapsSegments(text: string): SmallCapsSegment[];
55
+ //#endregion
56
+ export { SMALL_CAPS_SCALE, SmallCapsSegment, isSmallCapsLetter, smallCapsMask, smallCapsSegments };
@@ -0,0 +1,88 @@
1
+ //#region src/layout-engine/measure/smallCapsCasing.ts
2
+ /**
3
+ * Casing rules for synthesized `w:smallCaps` glyphs.
4
+ *
5
+ * A face that carries no `smcp`/`c2sc` substitution has no true small-capital
6
+ * glyph, so it is faked: a lowercase letter is drawn as its uppercase form at
7
+ * a smaller point size, on the run's own baseline, at a fixed fraction of the
8
+ * run's declared size — 0.8, an exact ratio observed to repeat across mixed
9
+ * two-size runs at every point size and never round to a half-point `w:sz`
10
+ * step. That is not a canvas or DOM backend's own `font-variant: small-caps`,
11
+ * which synthesizes at its own, uncontrollable, narrower ratio (Blink and
12
+ * WebKit use 0.7).
13
+ *
14
+ * Pure: no canvas, no DOM. Shared by every measurement and paint backend so a
15
+ * run's advances and its glyphs agree on which characters shrink.
16
+ */
17
+ /** Fraction of the run's declared size a synthesized small capital draws at. */
18
+ const SMALL_CAPS_SCALE = .8;
19
+ /** Whitespace never gets its own small-caps class; see `smallCapsMask`. */
20
+ const WHITESPACE_PATTERN = /\s/u;
21
+ /**
22
+ * Whether `char` is a cased letter in its lowercase form — the class
23
+ * `w:smallCaps` draws as a shrunken, uppercased capital. Digits, punctuation,
24
+ * symbols and already-uppercase letters keep the run's full size; combining
25
+ * `char`'s upper and lower forms this way (rather than comparing to a
26
+ * separate "is uppercase" test) is what correctly leaves caseless scripts
27
+ * (CJK, most punctuation) at full size too, since neither form differs from
28
+ * the character itself there.
29
+ */
30
+ function isSmallCapsLetter(char) {
31
+ return char.toLocaleUpperCase() !== char;
32
+ }
33
+ /**
34
+ * Per-code-point small-caps classification for `text`, one entry per code
35
+ * point (iterated the same way `[...text]`/`for...of` does, so it lines up
36
+ * with any other code-point walk of the same string).
37
+ *
38
+ * Whitespace carries no case of its own; a run's space draws at the size of
39
+ * the character immediately before it — the trailing space after a
40
+ * synthesized word is itself synthesized — so a whitespace code point here
41
+ * takes the previous code point's class, and only falls back to full size
42
+ * when it opens the string (nothing to inherit from).
43
+ */
44
+ function smallCapsMask(text) {
45
+ const mask = [];
46
+ let previous = false;
47
+ for (const char of text) {
48
+ const small = WHITESPACE_PATTERN.test(char) ? previous : isSmallCapsLetter(char);
49
+ mask.push(small);
50
+ previous = small;
51
+ }
52
+ return mask;
53
+ }
54
+ /**
55
+ * `text` split into maximal same-size segments for painting, each already
56
+ * uppercased where `small` — the shrunken capital a `smcp`-less face
57
+ * substitutes for the source lowercase letter. A run with no lowercase letter
58
+ * at all (already-uppercase headings, digits, punctuation) comes back as one
59
+ * `small: false` segment, so a caller can skip the extra painting work its
60
+ * mixed-size runs need.
61
+ */
62
+ function smallCapsSegments(text) {
63
+ const mask = smallCapsMask(text);
64
+ const segments = [];
65
+ let current = "";
66
+ let currentSmall;
67
+ let index = 0;
68
+ for (const char of text) {
69
+ const small = mask[index] ?? false;
70
+ if (currentSmall !== void 0 && small !== currentSmall) {
71
+ segments.push({
72
+ text: current,
73
+ small: currentSmall
74
+ });
75
+ current = "";
76
+ }
77
+ current += small ? char.toLocaleUpperCase() : char;
78
+ currentSmall = small;
79
+ index += 1;
80
+ }
81
+ if (current) segments.push({
82
+ text: current,
83
+ small: currentSmall ?? false
84
+ });
85
+ return segments;
86
+ }
87
+ //#endregion
88
+ export { SMALL_CAPS_SCALE, isSmallCapsLetter, smallCapsMask, smallCapsSegments };
@@ -50,6 +50,8 @@ type TabWidthResult = {
50
50
  leader?: TabLeader;
51
51
  /** Alignment that was applied */
52
52
  alignment: TabAlignment | "default";
53
+ /** Set when the tab advanced to one of the context's explicit stops. */
54
+ explicit?: true;
53
55
  };
54
56
  /** Default tab interval: 720 twips = 0.5 inch */
55
57
  declare const DEFAULT_TAB_INTERVAL_TWIPS = 720;
@@ -122,7 +122,8 @@ function calculateTabWidth(currentXPx, context, following = {}) {
122
122
  return {
123
123
  width,
124
124
  ...nextStop.leader !== void 0 ? { leader: nextStop.leader } : {},
125
- alignment: nextStop.val
125
+ alignment: nextStop.val,
126
+ ...context.explicitStops?.includes(nextStop) === true ? { explicit: true } : {}
126
127
  };
127
128
  }
128
129
  /**
@@ -22,6 +22,8 @@ type TableCellFloatingImage = {
22
22
  distLeft: number;
23
23
  distRight: number;
24
24
  wrapText?: "bothSides" | "left" | "right" | "largest";
25
+ /** Authored wrap mode; decides whether and how the picture excludes text. */
26
+ wrapType?: ImageRun["wrapType"];
25
27
  pmStart?: number;
26
28
  pmEnd?: number;
27
29
  };