@stll/folio-core 0.47.7 → 0.48.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 (160) hide show
  1. package/dist/controller/fontReadiness.js +3 -3
  2. package/dist/controller/hyphenationReadiness.d.ts +19 -0
  3. package/dist/controller/hyphenationReadiness.js +47 -0
  4. package/dist/controller/layoutPipeline.d.ts +3 -0
  5. package/dist/controller/layoutPipeline.js +59 -17
  6. package/dist/display-list/build/buildDisplayList.js +39 -12
  7. package/dist/display-list/build/pageFurniture.d.ts +7 -1
  8. package/dist/display-list/build/pageFurniture.js +21 -8
  9. package/dist/display-list/build/paragraphPrimitives.js +5 -4
  10. package/dist/display-list/build/tablePrimitives.js +10 -6
  11. package/dist/docx/attributeRemainder.js +10 -2
  12. package/dist/docx/blockContentParser.d.ts +2 -0
  13. package/dist/docx/blockContentParser.js +77 -62
  14. package/dist/docx/commentParser.d.ts +2 -1
  15. package/dist/docx/commentParser.js +57 -45
  16. package/dist/docx/containerChildren.d.ts +21 -8
  17. package/dist/docx/containerChildren.js +39 -9
  18. package/dist/docx/documentParser.d.ts +5 -1
  19. package/dist/docx/documentParser.js +8 -5
  20. package/dist/docx/documentSectionFacts.d.ts +22 -0
  21. package/dist/docx/documentSectionFacts.js +43 -0
  22. package/dist/docx/fieldParser.d.ts +8 -1
  23. package/dist/docx/fieldParser.js +47 -3
  24. package/dist/docx/fontTableParser.js +34 -27
  25. package/dist/docx/footnoteParser.d.ts +3 -2
  26. package/dist/docx/footnoteParser.js +15 -12
  27. package/dist/docx/groupDrawingParser.d.ts +2 -1
  28. package/dist/docx/groupDrawingParser.js +3 -6
  29. package/dist/docx/headerFooterParser.d.ts +7 -2
  30. package/dist/docx/headerFooterParser.js +15 -5
  31. package/dist/docx/hyperlinkParser.d.ts +48 -36
  32. package/dist/docx/hyperlinkParser.js +97 -82
  33. package/dist/docx/numberingParser.js +108 -84
  34. package/dist/docx/paragraphParser.d.ts +5 -1
  35. package/dist/docx/paragraphParser.js +381 -362
  36. package/dist/docx/paragraphProperties.js +123 -109
  37. package/dist/docx/paragraphPropertySource.d.ts +6 -1
  38. package/dist/docx/paragraphPropertySource.js +9 -1
  39. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  40. package/dist/docx/paragraphTextBoxEnrichment.js +17 -6
  41. package/dist/docx/parser.d.ts +7 -1
  42. package/dist/docx/parser.js +33 -16
  43. package/dist/docx/previewBudget.d.ts +55 -33
  44. package/dist/docx/previewBudget.js +59 -65
  45. package/dist/docx/rezip.js +41 -58
  46. package/dist/docx/runParser.d.ts +4 -1
  47. package/dist/docx/runParser.js +172 -184
  48. package/dist/docx/sdtProperties.js +87 -84
  49. package/dist/docx/sectionParser.js +169 -160
  50. package/dist/docx/serializer/paragraphSerializer.js +42 -38
  51. package/dist/docx/serializer/settingsSerializer.js +19 -1
  52. package/dist/docx/settingsParser.js +8 -0
  53. package/dist/docx/streamingXmlParser.d.ts +6 -2
  54. package/dist/docx/streamingXmlParser.js +12 -7
  55. package/dist/docx/styleParser.js +34 -8
  56. package/dist/docx/tableParser.d.ts +21 -12
  57. package/dist/docx/tableParser.js +518 -441
  58. package/dist/docx/textBoxParser.d.ts +8 -3
  59. package/dist/docx/textBoxParser.js +3 -3
  60. package/dist/docx/verbatimCapture.d.ts +10 -8
  61. package/dist/docx/verbatimCapture.js +45 -2
  62. package/dist/docx/vmlImageParser.d.ts +15 -13
  63. package/dist/docx/vmlImageParser.js +75 -60
  64. package/dist/docx/vmlPreview.d.ts +3 -3
  65. package/dist/docx/vmlPreview.js +3 -4
  66. package/dist/docx/xmlNamespaceContext.d.ts +9 -0
  67. package/dist/docx/xmlNamespaceContext.js +54 -0
  68. package/dist/docx/xmlParser.d.ts +6 -3
  69. package/dist/docx/xmlParser.js +11 -41
  70. package/dist/docx/xmlResourceLimits.d.ts +2 -1
  71. package/dist/docx/xmlResourceLimits.js +4 -1
  72. package/dist/fonts/headlessMeasure.js +0 -0
  73. package/dist/headless-layout.d.ts +11 -2
  74. package/dist/headless-layout.js +153 -107
  75. package/dist/internal/paragraphFormattingSerialization.js +3 -3
  76. package/dist/layout-bridge/convert/endnoteLayout.d.ts +65 -0
  77. package/dist/layout-bridge/convert/endnoteLayout.js +227 -0
  78. package/dist/layout-bridge/convert/footnoteLayout.d.ts +19 -3
  79. package/dist/layout-bridge/convert/footnoteLayout.js +40 -31
  80. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -7
  81. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -20
  82. package/dist/layout-bridge/convert/paragraphMarkFormatting.d.ts +10 -0
  83. package/dist/layout-bridge/convert/paragraphMarkFormatting.js +59 -0
  84. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +29 -1
  85. package/dist/layout-bridge/convert/toFlowBlocks.js +258 -35
  86. package/dist/layout-bridge/engine/hitTest.js +5 -3
  87. package/dist/layout-bridge/engine/selectionRects.js +5 -3
  88. package/dist/layout-engine/anchorLayoutInCellCompatibility.d.ts +10 -0
  89. package/dist/layout-engine/anchorLayoutInCellCompatibility.js +22 -0
  90. package/dist/layout-engine/index.d.ts +2 -2
  91. package/dist/layout-engine/index.js +70 -11
  92. package/dist/layout-engine/justificationCompatibility.d.ts +1 -1
  93. package/dist/layout-engine/justificationCompatibility.js +9 -5
  94. package/dist/layout-engine/keep-together.d.ts +11 -5
  95. package/dist/layout-engine/keep-together.js +39 -16
  96. package/dist/layout-engine/layoutInstrumentation.d.ts +8 -2
  97. package/dist/layout-engine/layoutInstrumentation.js +8 -1
  98. package/dist/layout-engine/measure/advanceComposition.js +4 -7
  99. package/dist/layout-engine/measure/hyphenationDictionaries.d.ts +74 -0
  100. package/dist/layout-engine/measure/hyphenationDictionaries.js +138 -0
  101. package/dist/layout-engine/measure/hyphenationPreload.d.ts +11 -0
  102. package/dist/layout-engine/measure/hyphenationPreload.js +24 -0
  103. package/dist/layout-engine/measure/lineBreakProvider.d.ts +12 -2
  104. package/dist/layout-engine/measure/lineBreakProvider.js +30 -30
  105. package/dist/layout-engine/measure/lineBreaks.d.ts +6 -1
  106. package/dist/layout-engine/measure/lineBreaks.js +10 -3
  107. package/dist/layout-engine/measure/listMarkerWidth.d.ts +9 -6
  108. package/dist/layout-engine/measure/listMarkerWidth.js +20 -22
  109. package/dist/layout-engine/measure/measureBlocks.js +42 -8
  110. package/dist/layout-engine/measure/measureContainer.js +5 -5
  111. package/dist/layout-engine/measure/measureHelpers.d.ts +15 -3
  112. package/dist/layout-engine/measure/measureHelpers.js +21 -5
  113. package/dist/layout-engine/measure/measureParagraph.js +109 -30
  114. package/dist/layout-engine/measure/measureTypes.d.ts +5 -0
  115. package/dist/layout-engine/measure/tabCalculator.d.ts +2 -0
  116. package/dist/layout-engine/measure/tabCalculator.js +2 -1
  117. package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
  118. package/dist/layout-engine/measure/tableCellFloating.js +51 -19
  119. package/dist/layout-engine/measure/tableCellGrid.d.ts +28 -3
  120. package/dist/layout-engine/measure/tableCellGrid.js +45 -5
  121. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +2 -1
  122. package/dist/layout-engine/noteAreaFlow.d.ts +19 -0
  123. package/dist/layout-engine/noteAreaFlow.js +83 -0
  124. package/dist/layout-engine/paginator.d.ts +10 -1
  125. package/dist/layout-engine/paginator.js +13 -1
  126. package/dist/layout-engine/tableRowBreak.js +3 -1
  127. package/dist/layout-engine/types.d.ts +89 -3
  128. package/dist/layout-engine/types.js +96 -1
  129. package/dist/layout-painter/index.d.ts +3 -0
  130. package/dist/layout-painter/renderPage.js +32 -83
  131. package/dist/layout-painter/renderParagraph.d.ts +6 -1
  132. package/dist/layout-painter/renderParagraph.js +34 -22
  133. package/dist/layout-painter/renderTable.js +26 -13
  134. package/dist/prosemirror/attrs/index.d.ts +9 -9
  135. package/dist/prosemirror/attrs/index.js +43 -11
  136. package/dist/prosemirror/conversion/fromProseDoc.js +8 -1
  137. package/dist/prosemirror/conversion/markInterner.d.ts +21 -0
  138. package/dist/prosemirror/conversion/markInterner.js +90 -0
  139. package/dist/prosemirror/conversion/toProseDoc.js +77 -41
  140. package/dist/prosemirror/extensions/core/DocExtension.js +2 -1
  141. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  142. package/dist/prosemirror/extensions/marks/markUtils.d.ts +6 -2
  143. package/dist/prosemirror/extensions/marks/markUtils.js +39 -51
  144. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +2 -1
  145. package/dist/prosemirror/listMarker.d.ts +1 -0
  146. package/dist/prosemirror/listMarker.js +2 -0
  147. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  148. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  149. package/dist/prosemirror/schema/nodes.d.ts +13 -1
  150. package/dist/prosemirror/textBoxHostParagraph.d.ts +13 -0
  151. package/dist/prosemirror/textBoxHostParagraph.js +21 -0
  152. package/dist/prosemirror/validation.js +91 -80
  153. package/dist/utils/createDocument.js +10 -1
  154. package/dist/utils/scriptSegments.d.ts +33 -4
  155. package/dist/utils/scriptSegments.js +39 -6
  156. package/dist/utils/textFormattingMerge.d.ts +17 -3
  157. package/dist/utils/textFormattingMerge.js +18 -12
  158. package/dist/utils/trailingText.d.ts +22 -0
  159. package/dist/utils/trailingText.js +32 -0
  160. package/package.json +2 -2
@@ -2,8 +2,20 @@ import { RunFormatting } from "../types.js";
2
2
  import { FontStyle } from "./measureTypes.js";
3
3
  //#region src/layout-engine/measure/measureHelpers.d.ts
4
4
  declare const DEFAULT_FONT_SIZE = 11;
5
- declare const DEFAULT_FONT_FAMILY = "Calibri";
6
- declare const DOCX_SCRIPT_FONT_SCALE = 0.75;
5
+ /**
6
+ * The face for text whose run properties name no font anywhere in the cascade:
7
+ * no `w:rFonts` family or theme reference in the run, its styles or
8
+ * `w:docDefaults`. ECMA-376 §17.3.2.26 leaves this to the application; its
9
+ * default is Times New Roman. A document that names a font at any level never
10
+ * reaches it.
11
+ */
12
+ declare const DEFAULT_FONT_FAMILY = "Times New Roman";
13
+ /**
14
+ * Point size of `w:vertAlign="superscript"`/`"subscript"` text: two thirds
15
+ * of the run's size, truncated to whole half-points (the `w:sz` unit) and
16
+ * never below one half-point.
17
+ */
18
+ declare function docxScriptFontSize(fontSize: number): number;
7
19
  /**
8
20
  * Build a measurement `FontStyle` from a run's formatting. Single source of
9
21
  * truth for run → FontStyle so every measurement path (layout line-breaking,
@@ -88,4 +100,4 @@ declare function halfPtToPx(halfPt: number): number;
88
100
  */
89
101
  declare function pxToHalfPt(px: number): number;
90
102
  //#endregion
91
- export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DOCX_SCRIPT_FONT_SCALE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
103
+ export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, clearFontResolvedCache, docxScriptFontSize, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
@@ -1,6 +1,7 @@
1
1
  import { resolveFontFamily } from "../../utils/fontResolver.js";
2
2
  import "../../utils/fontWeights.js";
3
3
  import { normalizeHorizontalScalePercent } from "../../utils/horizontalScale.js";
4
+ import { eastAsiaHintApplies } from "../../utils/scriptSegments.js";
4
5
  import { FONT_KERNING_MODE, getRunFontKerningMode } from "./textMeasurementPolicy.js";
5
6
  //#region src/layout-engine/measure/measureHelpers.ts
6
7
  /**
@@ -16,8 +17,22 @@ const TWIPS_PER_INCH = 1440;
16
17
  const PX_PER_INCH = 96;
17
18
  const TWIPS_PER_PX = TWIPS_PER_INCH / PX_PER_INCH;
18
19
  const DEFAULT_FONT_SIZE = 11;
19
- const DEFAULT_FONT_FAMILY = "Calibri";
20
- const DOCX_SCRIPT_FONT_SCALE = .75;
20
+ /**
21
+ * The face for text whose run properties name no font anywhere in the cascade:
22
+ * no `w:rFonts` family or theme reference in the run, its styles or
23
+ * `w:docDefaults`. ECMA-376 §17.3.2.26 leaves this to the application; its
24
+ * default is Times New Roman. A document that names a font at any level never
25
+ * reaches it.
26
+ */
27
+ const DEFAULT_FONT_FAMILY = "Times New Roman";
28
+ /**
29
+ * Point size of `w:vertAlign="superscript"`/`"subscript"` text: two thirds
30
+ * of the run's size, truncated to whole half-points (the `w:sz` unit) and
31
+ * never below one half-point.
32
+ */
33
+ function docxScriptFontSize(fontSize) {
34
+ return Math.max(1, Math.floor(fontSize * 2 * 2 / 3)) / 2;
35
+ }
21
36
  /**
22
37
  * Build a measurement `FontStyle` from a run's formatting. Single source of
23
38
  * truth for run → FontStyle so every measurement path (layout line-breaking,
@@ -28,13 +43,14 @@ const DOCX_SCRIPT_FONT_SCALE = .75;
28
43
  */
29
44
  function buildRunFontStyle(run, fallbackFontFamily, fallbackFontSize) {
30
45
  const baseFontSize = run.fontSize ?? fallbackFontSize;
31
- const fontSize = run.superscript || run.subscript ? baseFontSize * DOCX_SCRIPT_FONT_SCALE : baseFontSize;
46
+ const fontSize = run.superscript || run.subscript ? docxScriptFontSize(baseFontSize) : baseFontSize;
32
47
  const horizontalScale = normalizeHorizontalScalePercent(run.horizontalScale);
33
48
  return {
34
49
  fontFamily: run.fontFamily ?? fallbackFontFamily,
35
50
  ...run.alternateFontFamily !== void 0 ? { alternateFontFamily: run.alternateFontFamily } : {},
36
51
  ...run.eastAsiaFontFamily !== void 0 ? { eastAsiaFontFamily: run.eastAsiaFontFamily } : {},
37
52
  ...run.eastAsiaAlternateFontFamily !== void 0 ? { eastAsiaAlternateFontFamily: run.eastAsiaAlternateFontFamily } : {},
53
+ ...eastAsiaHintApplies(run) ? { eastAsiaHint: true } : {},
38
54
  ...run.complexScriptFontFamily !== void 0 ? { complexScriptFontFamily: run.complexScriptFontFamily } : {},
39
55
  ...run.complexScriptAlternateFontFamily !== void 0 ? { complexScriptAlternateFontFamily: run.complexScriptAlternateFontFamily } : {},
40
56
  ...run.complexScriptFontSize !== void 0 ? { complexScriptFontSize: run.complexScriptFontSize } : {},
@@ -107,7 +123,7 @@ function buildFontString(style) {
107
123
  if (style.bold) parts.push("700");
108
124
  const fontSizePx = ptToPx(style.fontSize ?? 11);
109
125
  parts.push(`${fontSizePx}px`);
110
- const fontFamily = style.fontFamily ?? "Calibri";
126
+ const fontFamily = style.fontFamily ?? "Times New Roman";
111
127
  parts.push(getResolvedFallback(fontFamily, style.alternateFontFamily));
112
128
  return parts.join(" ");
113
129
  }
@@ -181,4 +197,4 @@ function pxToHalfPt(px) {
181
197
  return pxToPt(px) * 2;
182
198
  }
183
199
  //#endregion
184
- export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DOCX_SCRIPT_FONT_SCALE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
200
+ export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, clearFontResolvedCache, docxScriptFontSize, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
@@ -1,7 +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";
4
+ import { hasCjk, hasComplexScript, hasEastAsiaSlotText } from "../../utils/scriptSegments.js";
5
+ import { splitTrailingToken } from "../../utils/trailingText.js";
5
6
  import { JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO, supportsJustifiedFinalLineContraction } from "../justifiedLineFit.js";
6
7
  import { measuredLineAdvance } from "../lineFlow.js";
7
8
  import { isFloatingImageRun } from "../types.js";
@@ -10,9 +11,9 @@ import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./comp
10
11
  import { resolveEffectiveLineBreakPolicy } from "./effectiveLineBreakPolicy.js";
11
12
  import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
12
13
  import { defaultLineBreakProvider, getLineBreakProvider } from "./lineBreakProvider.js";
13
- import { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakChar, isHangingPunctuation } from "./lineBreaks.js";
14
+ import { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakBetween, isBreakChar, isHangingPunctuation } from "./lineBreaks.js";
14
15
  import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
15
- import { buildRunFontStyle, ptToPx, twipsToPx } from "./measureHelpers.js";
16
+ import { DEFAULT_FONT_FAMILY, buildRunFontStyle, ptToPx, twipsToPx } from "./measureHelpers.js";
16
17
  import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
17
18
  import { calculateTabWidth, pixelsToTwips } from "./tabCalculator.js";
18
19
  import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
@@ -24,11 +25,10 @@ import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
24
25
  * Converts runs into measured lines with typography metrics.
25
26
  */
26
27
  const DEFAULT_FONT_SIZE = 11;
27
- const DEFAULT_FONT_FAMILY = "Calibri";
28
28
  const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
29
29
  const WIDTH_TOLERANCE = .5;
30
30
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
31
- const JUSTIFY_SPACE_CONTRACTION_RATIO = .075;
31
+ const JUSTIFY_SPACE_CONTRACTION_RATIO = .25;
32
32
  const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
33
33
  const JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO = .022;
34
34
  const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
@@ -85,9 +85,11 @@ function runToFontStyle(run) {
85
85
  * keeps the base style so wrapping is untouched.
86
86
  */
87
87
  function cjkLineHeightStyle(run, baseStyle) {
88
- if (!run.text || !hasCjk(run.text)) return baseStyle;
88
+ if (!run.text) return baseStyle;
89
89
  const eastAsia = baseStyle.eastAsiaFontFamily;
90
- if (eastAsia !== void 0 && isCjkFont(eastAsia)) {
90
+ const eastAsiaFace = eastAsia !== void 0 && isCjkFont(eastAsia);
91
+ if (!hasCjk(run.text) && !(eastAsiaFace && hasEastAsiaSlotText(run.text, baseStyle.eastAsiaHint))) return baseStyle;
92
+ if (eastAsia !== void 0 && eastAsiaFace) {
91
93
  const result = {
92
94
  ...baseStyle,
93
95
  fontFamily: eastAsia
@@ -96,7 +98,7 @@ function cjkLineHeightStyle(run, baseStyle) {
96
98
  if (baseStyle.eastAsiaAlternateFontFamily !== void 0) result.alternateFontFamily = baseStyle.eastAsiaAlternateFontFamily;
97
99
  return result;
98
100
  }
99
- if (isCjkFont(baseStyle.fontFamily ?? DEFAULT_FONT_FAMILY)) return baseStyle;
101
+ if (isCjkFont(baseStyle.fontFamily ?? "Times New Roman")) return baseStyle;
100
102
  return {
101
103
  ...baseStyle,
102
104
  fontFamily: CJK_FALLBACK_FONT_FAMILY
@@ -153,7 +155,7 @@ const WORD_SINGLE_LINE_FLOOR = 1.15;
153
155
  function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily, attrs) {
154
156
  const result = calculateTypographyMetrics(fontSize, spacing, getFontMetrics({
155
157
  fontSize,
156
- fontFamily: fontFamily ?? DEFAULT_FONT_FAMILY,
158
+ fontFamily: fontFamily ?? "Times New Roman",
157
159
  ...attrs?.defaultAlternateFontFamily !== void 0 ? { alternateFontFamily: attrs.defaultAlternateFontFamily } : {}
158
160
  }));
159
161
  const lineRule = spacing?.lineRule ?? "auto";
@@ -432,15 +434,13 @@ function isFinalTextCandidate(block, runIndex, nextBreak) {
432
434
  return true;
433
435
  }
434
436
  function resolveTextCandidateFit({ block, line, isFirstLine, isFinalCandidate, candidateWidth, candidateSpaceWidth, fallbackTolerancePx, continuationStrategy, finalStrategy }) {
435
- if (!isFinalCandidate || !supportsJustifiedFinalLineContraction(block)) return {
437
+ if (!isFinalCandidate) return {
436
438
  type: "ordinary",
437
439
  tolerancePx: fallbackTolerancePx
438
440
  };
441
+ if (!supportsJustifiedFinalLineContraction(block)) return admitFinalCandidate(candidateWidth, line.availableWidth, fallbackTolerancePx);
439
442
  const ordinaryTolerancePx = isFirstLine ? fallbackTolerancePx : justifyFitTolerance(line, continuationStrategy, candidateSpaceWidth);
440
- if (candidateWidth <= line.availableWidth + ordinaryTolerancePx) return {
441
- type: "ordinary",
442
- tolerancePx: ordinaryTolerancePx
443
- };
443
+ if (candidateWidth <= line.availableWidth + ordinaryTolerancePx) return admitFinalCandidate(candidateWidth, line.availableWidth, ordinaryTolerancePx);
444
444
  const finalTolerancePx = justifyFitTolerance(line, finalStrategy, candidateSpaceWidth);
445
445
  if (candidateWidth > line.availableWidth + finalTolerancePx) return {
446
446
  type: "final-contraction-rejected",
@@ -455,6 +455,26 @@ function resolveTextCandidateFit({ block, line, isFirstLine, isFinalCandidate, c
455
455
  }
456
456
  };
457
457
  }
458
+ /**
459
+ * A paragraph's last line is not stretched, so a final word admitted by space
460
+ * contraction needs an explicit paint plan; otherwise it would paint past the
461
+ * measure. Sub-pixel rounding overflow stays on the ordinary path.
462
+ */
463
+ function admitFinalCandidate(candidateWidth, availableWidth, tolerancePx) {
464
+ const contractionPx = candidateWidth - availableWidth;
465
+ if (contractionPx <= WIDTH_TOLERANCE || contractionPx > tolerancePx) return {
466
+ type: "ordinary",
467
+ tolerancePx
468
+ };
469
+ return {
470
+ type: "final-contraction-admitted",
471
+ tolerancePx,
472
+ paint: {
473
+ type: "space-contraction",
474
+ contractionPx
475
+ }
476
+ };
477
+ }
458
478
  function compressibleSpaceWidth(text, style) {
459
479
  const count = countCompressibleSpaces(text);
460
480
  return count === 0 ? 0 : count * measureTextWidth(" ", style);
@@ -503,6 +523,21 @@ function measureWordWithTrailingWhitespace(word, style) {
503
523
  fullWordWidth: wordWidth + trailingWhitespaceWidth + boundarySpacing
504
524
  };
505
525
  }
526
+ const scaledLetterSpacing = (style) => (style.letterSpacing ?? 0) * getHorizontalScaleFactor(style.horizontalScale);
527
+ /**
528
+ * Character spacing still owed after the last character placed on the line,
529
+ * or 0 when the line has no preceding character at `charIndex` of `runIndex`.
530
+ * Within a run it is the run's own spacing; at a boundary between two text
531
+ * runs it is the preceding run's spacing on its final character.
532
+ */
533
+ function precedingLetterSpacing({ block, line, runIndex, charIndex, runStyle }) {
534
+ if (line.fromRun === line.toRun && line.fromChar === line.toChar) return 0;
535
+ if (line.toRun === runIndex && line.toChar === charIndex) return scaledLetterSpacing(runStyle);
536
+ if (charIndex !== 0 || line.toRun !== runIndex - 1) return 0;
537
+ const previousRun = block.runs[runIndex - 1];
538
+ if (!previousRun || !isTextRun(previousRun) || previousRun.text.length === 0 || line.toChar !== previousRun.text.length) return 0;
539
+ return scaledLetterSpacing(runToFontStyle(previousRun));
540
+ }
506
541
  function trailingCodePoint(text) {
507
542
  if (text.length === 0) return;
508
543
  const trailing = text.charCodeAt(text.length - 1);
@@ -548,6 +583,21 @@ function computeTrailingGlueWidths(block) {
548
583
  }
549
584
  return widths;
550
585
  }
586
+ /**
587
+ * First character of the text that follows each run, looking through empty
588
+ * text runs; undefined before a non-text run or at the paragraph end.
589
+ */
590
+ function computeFollowingTextLeads(block) {
591
+ const leads = Array.from({ length: block.runs.length }, () => void 0);
592
+ let lead;
593
+ for (let index = block.runs.length - 1; index >= 0; index--) {
594
+ leads[index] = lead;
595
+ const run = block.runs[index];
596
+ if (!run || !isTextRun(run)) lead = void 0;
597
+ else if (run.text) lead = run.text[0];
598
+ }
599
+ return leads;
600
+ }
551
601
  function computeProtectedCrossRunGlueWidths(block) {
552
602
  const widths = Array.from({ length: block.runs.length }, () => 0);
553
603
  for (let index = 0; index < block.runs.length; index++) {
@@ -558,10 +608,10 @@ function computeProtectedCrossRunGlueWidths(block) {
558
608
  run
559
609
  }).provider;
560
610
  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] ?? "";
611
+ const trailing = splitTrailingToken(run.text ?? "");
612
+ if (!trailing || trailing.token.length !== 1) continue;
613
+ const { token } = trailing;
614
+ let separator = trailing.separator;
565
615
  let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
566
616
  let followingWord = "";
567
617
  let unseparatedFollowingText = false;
@@ -772,7 +822,7 @@ function measureParagraph(block, maxWidth, options) {
772
822
  };
773
823
  }
774
824
  if (runs.length === 0) {
775
- const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
825
+ const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? "Times New Roman", attrs);
776
826
  const outlineLineHeight = attrs?.reserveEmptyOutlineHeight ? emptyMetrics.lineHeight * 2 : emptyMetrics.lineHeight;
777
827
  lines.push({
778
828
  fromRun: 0,
@@ -793,7 +843,7 @@ function measureParagraph(block, maxWidth, options) {
793
843
  };
794
844
  }
795
845
  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);
846
+ const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? "Times New Roman", attrs);
797
847
  lines.push({
798
848
  fromRun: 0,
799
849
  fromChar: 0,
@@ -813,6 +863,7 @@ function measureParagraph(block, maxWidth, options) {
813
863
  }
814
864
  const trailingGlueWidths = computeTrailingGlueWidths(block);
815
865
  const protectedCrossRunGlueWidths = computeProtectedCrossRunGlueWidths(block);
866
+ const followingTextLeads = computeFollowingTextLeads(block);
816
867
  let currentLine = {
817
868
  fromRun: 0,
818
869
  fromChar: 0,
@@ -823,6 +874,8 @@ function measureParagraph(block, maxWidth, options) {
823
874
  regularSpaceWidth: 0,
824
875
  maxFontSize: DEFAULT_FONT_SIZE,
825
876
  maxFontMetrics: null,
877
+ maxTabFontSize: DEFAULT_FONT_SIZE,
878
+ maxTabFontMetrics: null,
826
879
  maxImageHeightPx: 0,
827
880
  maxExactImageHeightPx: 0,
828
881
  maxMathHeightPx: 0,
@@ -833,9 +886,14 @@ function measureParagraph(block, maxWidth, options) {
833
886
  };
834
887
  const calculateLineTypography = (line) => {
835
888
  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({
889
+ const paragraphFontFamily = attrs?.defaultFontFamily ?? "Times New Roman";
890
+ let lineFontMetrics = line.maxFontMetrics;
891
+ let fontSize = line.maxFontSize;
892
+ if (!lineFontMetrics) {
893
+ lineFontMetrics = line.maxTabFontMetrics;
894
+ fontSize = lineFontMetrics ? line.maxTabFontSize : paragraphFontSize;
895
+ }
896
+ const metrics = lineFontMetrics ?? getFontMetrics({
839
897
  fontSize: paragraphFontSize,
840
898
  fontFamily: paragraphFontFamily,
841
899
  ...attrs?.defaultAlternateFontFamily !== void 0 ? { alternateFontFamily: attrs.defaultAlternateFontFamily } : {}
@@ -919,6 +977,8 @@ function measureParagraph(block, maxWidth, options) {
919
977
  regularSpaceWidth: 0,
920
978
  maxFontSize: DEFAULT_FONT_SIZE,
921
979
  maxFontMetrics: null,
980
+ maxTabFontSize: DEFAULT_FONT_SIZE,
981
+ maxTabFontMetrics: null,
922
982
  maxImageHeightPx: 0,
923
983
  maxExactImageHeightPx: 0,
924
984
  maxMathHeightPx: 0,
@@ -942,6 +1002,19 @@ function measureParagraph(block, maxWidth, options) {
942
1002
  const metrics = getFontMetrics(style);
943
1003
  if (metrics.singleLineRatio > currentLine.maxFontMetrics.singleLineRatio) currentLine.maxFontMetrics = metrics;
944
1004
  };
1005
+ /**
1006
+ * Track the tallest tab on the line separately from content runs: a tab's
1007
+ * font does not raise a line that carries other content, so a tab run
1008
+ * formatted larger than its neighbouring text (a TOC entry whose tab keeps
1009
+ * the paragraph-mark size) leaves the line at the text's height.
1010
+ */
1011
+ const updateMaxTabFont = (style) => {
1012
+ const fontSize = style.fontSize ?? DEFAULT_FONT_SIZE;
1013
+ if (!currentLine.maxTabFontMetrics || fontSize > currentLine.maxTabFontSize) {
1014
+ currentLine.maxTabFontSize = fontSize;
1015
+ currentLine.maxTabFontMetrics = getFontMetrics(style);
1016
+ }
1017
+ };
945
1018
  let crossRunResume;
946
1019
  for (let runIndex = 0; runIndex < runs.length; runIndex++) {
947
1020
  if (crossRunResume && runIndex < crossRunResume.runIndex) continue;
@@ -970,7 +1043,7 @@ function measureParagraph(block, maxWidth, options) {
970
1043
  }
971
1044
  if (isTabRun(run)) {
972
1045
  const style = runToFontStyle(run);
973
- updateMaxFont(style);
1046
+ updateMaxTabFont(style);
974
1047
  const followingWidth = measureInlineWidthAfterTab(runs, runIndex, options?.fieldValues);
975
1048
  const decimalPrefixWidth = measureDecimalPrefixWidthAfterTab(runs, runIndex, options?.fieldValues);
976
1049
  const lineX = currentLine.width + currentLine.leftOffset;
@@ -987,14 +1060,14 @@ function measureParagraph(block, maxWidth, options) {
987
1060
  let tabWidth = tabResult.width;
988
1061
  const authoredEndpoint = contentX + tabWidth + followingWidth;
989
1062
  const activeContentRightEdge = maxWidth - currentLine.rightOffset;
990
- const preservesAuthoredEndStop = tabResult.alignment === "end" && (options?.allowEndTabOverflow === true || authoredEndpoint <= activeContentRightEdge + WIDTH_TOLERANCE);
1063
+ const preservesAuthoredStop = tabResult.alignment === "end" && options?.allowEndTabOverflow === true || tabResult.explicit === true && authoredEndpoint <= activeContentRightEdge + WIDTH_TOLERANCE;
991
1064
  const landsOnLeftIndent = tabResult.alignment === "start" && indentLeft > 0 && Math.abs(contentX + tabWidth - indentLeft) <= WIDTH_TOLERANCE;
992
1065
  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);
1066
+ if (!preservesAuthoredStop && !landsOnLeftIndent && !hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
1067
+ if (preservesAuthoredStop) currentLine.availableWidth = Math.max(currentLine.availableWidth, currentLine.width + tabWidth + followingWidth);
995
1068
  if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
996
1069
  startNewLine(runIndex, 0);
997
- updateMaxFont(style);
1070
+ updateMaxTabFont(style);
998
1071
  }
999
1072
  currentLine.width += tabWidth;
1000
1073
  currentLine.trailingWhitespaceWidth = 0;
@@ -1098,7 +1171,13 @@ function measureParagraph(block, maxWidth, options) {
1098
1171
  const nextBreak = wordBreaks[wordBreakIndex] ?? text.length;
1099
1172
  const word = text.slice(charIndex, nextBreak);
1100
1173
  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;
1174
+ let leadingLetterSpacing = precedingLetterSpacing({
1175
+ block,
1176
+ line: currentLine,
1177
+ runIndex,
1178
+ charIndex,
1179
+ runStyle: style
1180
+ });
1102
1181
  const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, effectiveLineBreakPolicy.hangingPunctuation);
1103
1182
  const isFirstLine = lines.length === 0;
1104
1183
  const regularSpaceWidth = compressibleSpaceWidth(measuredWord, style);
@@ -1223,7 +1302,7 @@ function measureParagraph(block, maxWidth, options) {
1223
1302
  }
1224
1303
  const wordTail = word.at(-1);
1225
1304
  const protectedGlueWidth = protectedCrossRunGlueWidths[runIndex] ?? 0;
1226
- const rawGlueWidth = isRunTail && wordTail !== void 0 && (!isBreakChar(wordTail) || protectedGlueWidth > 0) ? Math.max(trailingGlueWidths[runIndex] ?? 0, protectedGlueWidth) : 0;
1305
+ const rawGlueWidth = isRunTail && wordTail !== void 0 && (!isBreakBetween(wordTail, followingTextLeads[runIndex]) || protectedGlueWidth > 0) ? Math.max(trailingGlueWidths[runIndex] ?? 0, protectedGlueWidth) : 0;
1227
1306
  const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + widthTolerance ? rawGlueWidth : 0;
1228
1307
  const hangingAllowance = glueWidth === 0 ? hangingPunctuationWidth : 0;
1229
1308
  const candidateFit = isJustifiedParagraph ? resolveTextCandidateFit({
@@ -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.
@@ -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
  };
@@ -53,6 +53,7 @@ function getTableCellFloatingImages(cell, cellMeasure, contentWidth, resolvePosi
53
53
  const distLeft = run.distLeft ?? 12;
54
54
  const distRight = run.distRight ?? 12;
55
55
  const verticalOriginY = run.position?.vertical?.relativeTo === "paragraph" ? placement.top : placement.contentTop;
56
+ if (run.layoutInCell === false && !resolvePosition) continue;
56
57
  const resolved = run.layoutInCell === false && resolvePosition ? resolvePosition(run, verticalOriginY) : resolveCellScopedPosition({
57
58
  run,
58
59
  paragraphY: verticalOriginY,
@@ -82,6 +83,7 @@ function getTableCellFloatingImages(cell, cellMeasure, contentWidth, resolvePosi
82
83
  distLeft,
83
84
  distRight,
84
85
  wrapText,
86
+ ...run.wrapType !== void 0 ? { wrapType: run.wrapType } : {},
85
87
  ...run.pmStart !== void 0 ? { pmStart: run.pmStart } : {},
86
88
  ...run.pmEnd !== void 0 ? { pmEnd: run.pmEnd } : {}
87
89
  });
@@ -89,26 +91,56 @@ function getTableCellFloatingImages(cell, cellMeasure, contentWidth, resolvePosi
89
91
  }
90
92
  return result;
91
93
  }
94
+ /**
95
+ * The text exclusion one cell-scoped picture casts, in cell content
96
+ * coordinates, or `undefined` when text ignores it.
97
+ *
98
+ * - `wp:wrapNone` (`behind` / `inFront`): text paints over or under the
99
+ * picture, so nothing is excluded.
100
+ * - `wp:wrapTopAndBottom`: no text beside the picture; lines it overlaps move
101
+ * below it.
102
+ * - `wp:wrapSquare` and the contour modes: text runs on the picture's open
103
+ * side. When the picture and its wrap distances cover the cell's content
104
+ * width there is no open side, and the text moves below the picture as for
105
+ * `wrapTopAndBottom`.
106
+ */
107
+ function tableCellFloatingZone(img, contentWidth) {
108
+ if (img.wrapType === "behind" || img.wrapType === "inFront") return;
109
+ const rectLeft = img.x - img.distLeft;
110
+ const rectRight = img.x + img.width + img.distRight;
111
+ const topY = img.y - img.distTop;
112
+ const bottomY = img.y + img.height + img.distBottom;
113
+ const belowPicture = {
114
+ leftMargin: 0,
115
+ rightMargin: 0,
116
+ topY,
117
+ bottomY,
118
+ fullWidthBlock: true
119
+ };
120
+ if (img.wrapType === "topAndBottom") return belowPicture;
121
+ let leftMargin = 0;
122
+ let rightMargin = 0;
123
+ const wrapText = img.wrapText ?? "bothSides";
124
+ if (wrapText === "right") leftMargin = rectRight;
125
+ else if (wrapText === "left") rightMargin = contentWidth - rectLeft;
126
+ else if (img.side === "left") leftMargin = rectRight;
127
+ else rightMargin = contentWidth - rectLeft;
128
+ if (rectRight > 0 && rectLeft < contentWidth && Math.max(leftMargin, rightMargin) >= contentWidth) return belowPicture;
129
+ const clamped = clampFloatingWrapMargins(leftMargin, rightMargin, contentWidth);
130
+ return {
131
+ leftMargin: clamped.leftMargin,
132
+ rightMargin: clamped.rightMargin,
133
+ topY,
134
+ bottomY
135
+ };
136
+ }
92
137
  function buildTableCellFloatingZones(floatingImages, contentWidth) {
93
- return floatingImages.map((img) => {
94
- const rectRight = img.x + img.width + img.distRight;
95
- const rectTop = img.y - img.distTop;
96
- const rectBottom = img.y + img.height + img.distBottom;
97
- let leftMargin = 0;
98
- let rightMargin = 0;
99
- const wrapText = img.wrapText ?? "bothSides";
100
- if (wrapText === "right") leftMargin = rectRight;
101
- else if (wrapText === "left") rightMargin = contentWidth - (img.x - img.distLeft);
102
- else if (img.side === "left") leftMargin = rectRight;
103
- else rightMargin = contentWidth - (img.x - img.distLeft);
104
- const clamped = clampFloatingWrapMargins(leftMargin, rightMargin, contentWidth);
105
- return {
106
- leftMargin: clamped.leftMargin,
107
- rightMargin: clamped.rightMargin,
108
- topY: rectTop,
109
- bottomY: rectBottom
110
- };
111
- });
138
+ const zones = [];
139
+ for (const img of floatingImages) {
140
+ const zone = tableCellFloatingZone(img, contentWidth);
141
+ if (zone !== void 0) zones.push(zone);
142
+ }
143
+ return zones;
112
144
  }
113
145
  //#endregion
114
146
  export { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages };
@@ -16,13 +16,38 @@ type BuildTableCellPlacementsOptions = {
16
16
  grid: TableCellGrid;
17
17
  columnWidths: readonly number[];
18
18
  bidi: boolean;
19
+ /** `w:tblCellSpacing` in pixels; see {@link getTableCellSpacingInsetsX}. */
20
+ cellSpacing?: number | undefined;
19
21
  };
22
+ type TableCellSpacingInsets = {
23
+ readonly start: number;
24
+ readonly end: number;
25
+ };
26
+ type TableCellSpacingInsetsY = {
27
+ readonly top: number;
28
+ readonly bottom: number;
29
+ };
30
+ /**
31
+ * Logical inline insets of a cell box inside its grid slot under
32
+ * `w:tblCellSpacing` (§17.4.43). The spacing separates every cell from its
33
+ * neighbours and from the table edge: each cell keeps one unit on each side of
34
+ * its slot, and a slot at the table's leading or trailing grid edge keeps one
35
+ * more for the edge itself.
36
+ */
37
+ declare const getTableCellSpacingInsetsX: (cellSpacing: number | undefined, sourceColumn: number, columnSpan: number, columnCount: number) => TableCellSpacingInsets;
38
+ /**
39
+ * Block-direction insets of a cell box inside the rows it spans, the
40
+ * `w:tblCellSpacing` counterpart of {@link getTableCellSpacingInsetsX}: one
41
+ * unit above and below every cell, plus one more at the table's first and
42
+ * last rows.
43
+ */
44
+ declare const getTableCellSpacingInsetsY: (cellSpacing: number | undefined, rowIndex: number, rowSpan: number, rowCount: number) => TableCellSpacingInsetsY;
20
45
  declare const buildTableCellGrid: (rows: readonly TableRow[], columnCount: number) => TableCellGrid;
21
46
  declare const getFirstAvailableColumn: (grid: TableCellGrid, rowIndex: number, startingColumn: number) => number;
22
47
  declare const getSourceCellAt: (grid: TableCellGrid, rowIndex: number, columnIndex: number) => TableCell | undefined;
23
48
  declare const getSourceCellColumn: (grid: TableCellGrid, cell: TableCell) => number | undefined;
24
49
  /** Resolve all source cells to canonical logical columns and physical boxes. */
25
- declare const buildTableCellPlacements: ({ grid, columnWidths, bidi }: BuildTableCellPlacementsOptions) => TableCellPlacements;
26
- declare const getTableCellVerticalBorderHeight: (grid: TableCellGrid, cell: TableCell | undefined, rowIndex: number) => number;
50
+ declare const buildTableCellPlacements: ({ grid, columnWidths, bidi, cellSpacing }: BuildTableCellPlacementsOptions) => TableCellPlacements;
51
+ declare const getTableCellVerticalBorderHeight: (grid: TableCellGrid, cell: TableCell | undefined, rowIndex: number, separated?: boolean) => number;
27
52
  //#endregion
28
- export { TableCellGrid, TableCellPlacement, TableCellPlacements, buildTableCellGrid, buildTableCellPlacements, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
53
+ export { TableCellGrid, TableCellPlacement, TableCellPlacements, TableCellSpacingInsets, TableCellSpacingInsetsY, buildTableCellGrid, buildTableCellPlacements, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellSpacingInsetsX, getTableCellSpacingInsetsY, getTableCellVerticalBorderHeight };
@@ -1,5 +1,41 @@
1
1
  import { paintsCssBorder } from "../../utils/borderCss.js";
2
2
  //#region src/layout-engine/measure/tableCellGrid.ts
3
+ const spacingUnit = (cellSpacing) => cellSpacing !== void 0 && Number.isFinite(cellSpacing) && cellSpacing > 0 ? cellSpacing : 0;
4
+ /**
5
+ * Logical inline insets of a cell box inside its grid slot under
6
+ * `w:tblCellSpacing` (§17.4.43). The spacing separates every cell from its
7
+ * neighbours and from the table edge: each cell keeps one unit on each side of
8
+ * its slot, and a slot at the table's leading or trailing grid edge keeps one
9
+ * more for the edge itself.
10
+ */
11
+ const getTableCellSpacingInsetsX = (cellSpacing, sourceColumn, columnSpan, columnCount) => {
12
+ const unit = spacingUnit(cellSpacing);
13
+ if (unit === 0) return {
14
+ start: 0,
15
+ end: 0
16
+ };
17
+ return {
18
+ start: sourceColumn <= 0 ? unit * 2 : unit,
19
+ end: sourceColumn + columnSpan >= columnCount ? unit * 2 : unit
20
+ };
21
+ };
22
+ /**
23
+ * Block-direction insets of a cell box inside the rows it spans, the
24
+ * `w:tblCellSpacing` counterpart of {@link getTableCellSpacingInsetsX}: one
25
+ * unit above and below every cell, plus one more at the table's first and
26
+ * last rows.
27
+ */
28
+ const getTableCellSpacingInsetsY = (cellSpacing, rowIndex, rowSpan, rowCount) => {
29
+ const unit = spacingUnit(cellSpacing);
30
+ if (unit === 0) return {
31
+ top: 0,
32
+ bottom: 0
33
+ };
34
+ return {
35
+ top: rowIndex <= 0 ? unit * 2 : unit,
36
+ bottom: rowIndex + Math.max(1, rowSpan) >= rowCount ? unit * 2 : unit
37
+ };
38
+ };
3
39
  const buildTableCellGrid = (rows, columnCount) => {
4
40
  const occupiedColumnsByRow = /* @__PURE__ */ new Map();
5
41
  const sourceCellsByRow = /* @__PURE__ */ new Map();
@@ -36,7 +72,7 @@ const getFirstAvailableColumn = (grid, rowIndex, startingColumn) => firstAvailab
36
72
  const getSourceCellAt = (grid, rowIndex, columnIndex) => grid.sourceCellsByRow.get(rowIndex)?.get(columnIndex);
37
73
  const getSourceCellColumn = (grid, cell) => grid.sourceColumnsByCell.get(cell);
38
74
  /** Resolve all source cells to canonical logical columns and physical boxes. */
39
- const buildTableCellPlacements = ({ grid, columnWidths, bidi }) => {
75
+ const buildTableCellPlacements = ({ grid, columnWidths, bidi, cellSpacing }) => {
40
76
  const columnOffsets = [0];
41
77
  for (const columnWidth of columnWidths) columnOffsets.push((columnOffsets.at(-1) ?? 0) + columnWidth);
42
78
  const tableWidth = columnOffsets.at(-1) ?? 0;
@@ -45,8 +81,11 @@ const buildTableCellPlacements = ({ grid, columnWidths, bidi }) => {
45
81
  if (sourceColumn < 0 || sourceColumn >= columnWidths.length) continue;
46
82
  const declaredColumnSpan = Math.max(1, Math.trunc(cell.colSpan ?? 1));
47
83
  const columnSpan = Math.min(declaredColumnSpan, columnWidths.length - sourceColumn);
48
- const logicalLeft = columnOffsets.at(sourceColumn) ?? 0;
49
- const logicalRight = columnOffsets.at(sourceColumn + columnSpan) ?? logicalLeft;
84
+ const slotLeft = columnOffsets.at(sourceColumn) ?? 0;
85
+ const slotRight = columnOffsets.at(sourceColumn + columnSpan) ?? slotLeft;
86
+ const insets = getTableCellSpacingInsetsX(cellSpacing, sourceColumn, columnSpan, columnWidths.length);
87
+ const logicalLeft = Math.min(slotRight, slotLeft + insets.start);
88
+ const logicalRight = Math.max(logicalLeft, slotRight - insets.end);
50
89
  const width = logicalRight - logicalLeft;
51
90
  placements.set(cell, {
52
91
  sourceColumn,
@@ -57,7 +96,8 @@ const buildTableCellPlacements = ({ grid, columnWidths, bidi }) => {
57
96
  }
58
97
  return placements;
59
98
  };
60
- const getTableCellVerticalBorderHeight = (grid, cell, rowIndex) => {
99
+ const getTableCellVerticalBorderHeight = (grid, cell, rowIndex, separated = false) => {
100
+ if (separated) return (cell?.borders?.top?.width ?? 0) + (cell?.borders?.bottom?.width ?? 0);
61
101
  const sourceColumn = cell ? getSourceCellColumn(grid, cell) : void 0;
62
102
  const aboveBottom = (sourceColumn === void 0 ? void 0 : getSourceCellAt(grid, rowIndex - 1, sourceColumn))?.borders?.bottom;
63
103
  const aboveOwnsEdge = aboveBottom !== void 0 && paintsCssBorder(aboveBottom.style);
@@ -83,4 +123,4 @@ const getOrCreateOccupiedRow = (occupiedColumnsByRow, rowIndex) => {
83
123
  return occupiedColumns;
84
124
  };
85
125
  //#endregion
86
- export { buildTableCellGrid, buildTableCellPlacements, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
126
+ export { buildTableCellGrid, buildTableCellPlacements, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellSpacingInsetsX, getTableCellSpacingInsetsY, getTableCellVerticalBorderHeight };