@stll/folio-core 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/ai-edits/apply.js +565 -121
  2. package/dist/ai-edits/headless.d.ts +37 -1
  3. package/dist/ai-edits/headless.js +47 -16
  4. package/dist/ai-edits/minimal-replacement.d.ts +64 -0
  5. package/dist/ai-edits/minimal-replacement.js +282 -0
  6. package/dist/controller/layoutPipeline.js +11 -1
  7. package/dist/display-list/build/paragraphPrimitives.js +63 -7
  8. package/dist/display-list/build/textBoxPrimitives.js +1 -1
  9. package/dist/display-list/build/unsupported.d.ts +2 -2
  10. package/dist/display-list/build/unsupported.js +0 -0
  11. package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
  12. package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
  13. package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
  14. package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
  15. package/dist/display-list/selectDisplayPages.d.ts +6 -0
  16. package/dist/display-list/selectDisplayPages.js +44 -0
  17. package/dist/docx/blockContentParser.js +2 -2
  18. package/dist/docx/bulletMarkers.d.ts +18 -2
  19. package/dist/docx/bulletMarkers.js +27 -2
  20. package/dist/docx/drawingGroupChildren.d.ts +60 -0
  21. package/dist/docx/drawingGroupChildren.js +176 -0
  22. package/dist/docx/drawingUtils.js +3 -0
  23. package/dist/docx/groupDrawingParser.d.ts +7 -1
  24. package/dist/docx/groupDrawingParser.js +123 -32
  25. package/dist/docx/packageParts.d.ts +45 -1
  26. package/dist/docx/packageParts.js +93 -2
  27. package/dist/docx/paraIdAttribute.d.ts +3 -2
  28. package/dist/docx/paragraphParser.js +91 -7
  29. package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
  30. package/dist/docx/runParser.js +10 -0
  31. package/dist/docx/selectiveSaveFlags.d.ts +10 -3
  32. package/dist/docx/selectiveSaveFlags.js +1 -1
  33. package/dist/docx/selectiveXmlPatch.d.ts +56 -34
  34. package/dist/docx/selectiveXmlPatch.js +222 -115
  35. package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
  36. package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
  37. package/dist/docx/serializer/paragraphSerializer.js +5 -4
  38. package/dist/docx/serializer/runSerializer.d.ts +5 -1
  39. package/dist/docx/serializer/runSerializer.js +4 -3
  40. package/dist/docx/shapeAlternateContent.d.ts +25 -0
  41. package/dist/docx/shapeAlternateContent.js +52 -0
  42. package/dist/docx/textBoxParser.d.ts +10 -2
  43. package/dist/docx/textBoxParser.js +9 -3
  44. package/dist/export-pdf.d.ts +35 -2
  45. package/dist/export-pdf.js +51 -20
  46. package/dist/fonts/fontsourceFaces.d.ts +47 -0
  47. package/dist/fonts/fontsourceFaces.js +230 -0
  48. package/dist/internal/headlessRevisionResolution.js +50 -13
  49. package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
  50. package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
  51. package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
  52. package/dist/layout-bridge/convert/flowBorders.js +31 -0
  53. package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
  54. package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
  55. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
  56. package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
  57. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
  58. package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
  59. package/dist/layout-bridge/convert/imageConversion.js +118 -0
  60. package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
  61. package/dist/layout-bridge/convert/listMarkers.js +98 -0
  62. package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
  63. package/dist/layout-bridge/convert/noteReferences.js +29 -0
  64. package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
  65. package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
  66. package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
  67. package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
  68. package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
  69. package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
  70. package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
  71. package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
  72. package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
  73. package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
  74. package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
  75. package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
  76. package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
  77. package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
  78. package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
  79. package/dist/layout-bridge/convert/tableConversion.js +303 -0
  80. package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
  81. package/dist/layout-bridge/convert/textBoxFill.js +42 -0
  82. package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
  83. package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
  84. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
  85. package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2060
  86. package/dist/layout-bridge/engine/hitTest.js +1 -1
  87. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  88. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  89. package/dist/layout-bridge/engine/selectionRects.js +1 -1
  90. package/dist/layout-engine/imageLayout.d.ts +9 -0
  91. package/dist/layout-engine/imageLayout.js +48 -0
  92. package/dist/layout-engine/index.d.ts +4 -19
  93. package/dist/layout-engine/index.js +14 -910
  94. package/dist/layout-engine/keep-together.d.ts +5 -2
  95. package/dist/layout-engine/keep-together.js +5 -1
  96. package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
  97. package/dist/layout-engine/layoutFlowShared.js +8 -0
  98. package/dist/layout-engine/measure/advanceComposition.js +49 -3
  99. package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
  100. package/dist/layout-engine/measure/crossRunWords.js +172 -0
  101. package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
  102. package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
  103. package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
  104. package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
  105. package/dist/layout-engine/measure/justification.d.ts +32 -0
  106. package/dist/layout-engine/measure/justification.js +83 -0
  107. package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
  108. package/dist/layout-engine/measure/lineFitting.js +145 -0
  109. package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
  110. package/dist/layout-engine/measure/lineTypography.js +124 -0
  111. package/dist/layout-engine/measure/measureBlocks.js +7 -4
  112. package/dist/layout-engine/measure/measureContainer.js +46 -3
  113. package/dist/layout-engine/measure/measureHelpers.js +13 -3
  114. package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
  115. package/dist/layout-engine/measure/measureParagraph.js +41 -780
  116. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
  117. package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
  118. package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
  119. package/dist/layout-engine/measure/paragraphTabs.js +78 -0
  120. package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
  121. package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
  122. package/dist/layout-engine/paginator.d.ts +2 -0
  123. package/dist/layout-engine/paginator.js +5 -1
  124. package/dist/layout-engine/paragraphLayout.d.ts +23 -0
  125. package/dist/layout-engine/paragraphLayout.js +204 -0
  126. package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
  127. package/dist/layout-engine/paragraphSpacing.js +2 -1
  128. package/dist/layout-engine/sectionLayout.d.ts +45 -0
  129. package/dist/layout-engine/sectionLayout.js +216 -0
  130. package/dist/layout-engine/tableLayout.d.ts +17 -0
  131. package/dist/layout-engine/tableLayout.js +369 -0
  132. package/dist/layout-engine/tableRowBreak.d.ts +8 -1
  133. package/dist/layout-engine/tableRowBreak.js +22 -1
  134. package/dist/layout-engine/textBoxLayout.d.ts +15 -0
  135. package/dist/layout-engine/textBoxLayout.js +137 -0
  136. package/dist/layout-engine/types.d.ts +21 -2
  137. package/dist/layout-painter/renderPage.js +1 -0
  138. package/dist/layout-painter/renderParagraph.js +41 -4
  139. package/dist/layout-painter/renderTextBox.d.ts +12 -2
  140. package/dist/layout-painter/renderTextBox.js +26 -2
  141. package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
  142. package/dist/paged-layout/sectionBlockWidths.js +1 -1
  143. package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
  144. package/dist/prosemirror/alternateContentAttrs.js +35 -0
  145. package/dist/prosemirror/attrs/index.js +90 -0
  146. package/dist/prosemirror/commands/comments.js +51 -41
  147. package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
  148. package/dist/prosemirror/contentControlRevisions.js +84 -0
  149. package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
  150. package/dist/prosemirror/conversion/toProseDoc.js +42 -19
  151. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
  152. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
  153. package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
  154. package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
  155. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
  156. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
  157. package/dist/prosemirror/listMarker.js +2 -2
  158. package/dist/prosemirror/paragraphDirection.d.ts +29 -2
  159. package/dist/prosemirror/paragraphDirection.js +21 -2
  160. package/dist/prosemirror/plugins/suggestionMode.js +37 -8
  161. package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
  162. package/dist/prosemirror/rejoinRunCarriers.js +66 -0
  163. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
  164. package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
  165. package/dist/prosemirror/schema/nodes.d.ts +37 -5
  166. package/dist/server.d.ts +3 -2
  167. package/dist/types/content.d.ts +2 -2
  168. package/package.json +2 -2
  169. package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
  170. package/dist/layout-engine/justifiedLineFit.js +0 -6
@@ -0,0 +1,145 @@
1
+ import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
2
+ import { defaultLineBreakProvider, getLineBreakProvider } from "./lineBreakProvider.js";
3
+ import { findGraphemeBreaks, isHangingPunctuation } from "./lineBreaks.js";
4
+ import { twipsToPx } from "./measureHelpers.js";
5
+ import { measureTextWidth } from "./measureProvider.js";
6
+ import { WIDTH_TOLERANCE, isTextRun, runToFontStyle } from "./paragraphMeasureShared.js";
7
+ //#region src/layout-engine/measure/lineFitting.ts
8
+ /**
9
+ * Line-fitting helpers: character-level fit search, the hyphenation zone,
10
+ * final-candidate admission, and the trailing whitespace, letter-spacing and
11
+ * hanging-punctuation widths a line break may exclude.
12
+ */
13
+ /**
14
+ * Find the longest prefix of `text` that fits within `maxWidth` pixels.
15
+ * Returns the number of characters that fit (at least 1 if `forceMin` is true).
16
+ */
17
+ function findMaxFittingLength(text, style, maxWidth, forceMin = false, policy) {
18
+ const boundaries = findGraphemeBreaks(text, policy);
19
+ let lo = 0;
20
+ let hi = boundaries.length - 1;
21
+ let best = 0;
22
+ while (lo <= hi) {
23
+ const mid = Math.floor((lo + hi) / 2);
24
+ const boundary = boundaries[mid];
25
+ if (measureTextWidth(text.slice(0, boundary), style) <= maxWidth) {
26
+ best = boundary;
27
+ lo = mid + 1;
28
+ } else hi = mid - 1;
29
+ }
30
+ return forceMin && best === 0 ? boundaries.at(0) ?? 0 : best;
31
+ }
32
+ function exceedsHyphenationZone(line, zoneTwips) {
33
+ if (line.width <= 0) return true;
34
+ const visibleWidth = Math.max(0, line.width - line.trailingWhitespaceWidth);
35
+ return Math.max(0, line.availableWidth - visibleWidth) > twipsToPx(zoneTwips) + WIDTH_TOLERANCE;
36
+ }
37
+ const isIgnorableFinalTailRun = (run) => run.kind === "renderedPageBreak" || isTextRun(run) && run.text.length === 0;
38
+ function isFinalTextCandidate(block, runIndex, nextBreak) {
39
+ const currentRun = block.runs[runIndex];
40
+ if (!currentRun || !isTextRun(currentRun) || nextBreak !== currentRun.text.length) return false;
41
+ for (let index = runIndex + 1; index < block.runs.length; index += 1) {
42
+ const run = block.runs[index];
43
+ if (run && isIgnorableFinalTailRun(run)) continue;
44
+ return false;
45
+ }
46
+ return true;
47
+ }
48
+ /**
49
+ * Fit of a text candidate on a justified line: the paragraph's final word may
50
+ * be admitted by shrinking spaces, which then needs a paint plan.
51
+ */
52
+ function resolveTextCandidateFit(isFinalCandidate, candidateWidth, availableWidth, tolerancePx) {
53
+ if (!isFinalCandidate) return {
54
+ type: "ordinary",
55
+ tolerancePx
56
+ };
57
+ return admitFinalCandidate(candidateWidth, availableWidth, tolerancePx);
58
+ }
59
+ /**
60
+ * A paragraph's last line is not stretched, so a final word admitted by space
61
+ * contraction needs an explicit paint plan; otherwise it would paint past the
62
+ * measure. Sub-pixel rounding overflow stays on the ordinary path.
63
+ */
64
+ function admitFinalCandidate(candidateWidth, availableWidth, tolerancePx) {
65
+ const contractionPx = candidateWidth - availableWidth;
66
+ if (contractionPx <= .5 || contractionPx > tolerancePx) return {
67
+ type: "ordinary",
68
+ tolerancePx
69
+ };
70
+ return {
71
+ type: "final-contraction-admitted",
72
+ tolerancePx,
73
+ paint: {
74
+ type: "space-contraction",
75
+ contractionPx
76
+ }
77
+ };
78
+ }
79
+ function trimTrailingSpacesAndTabs(text) {
80
+ let end = text.length;
81
+ while (end > 0) {
82
+ const char = text[end - 1];
83
+ if (char !== " " && char !== " ") break;
84
+ end--;
85
+ }
86
+ return text.slice(0, end);
87
+ }
88
+ function measureWordWithTrailingWhitespace(word, style) {
89
+ const measuredWord = trimTrailingSpacesAndTabs(word);
90
+ const wordWidth = measureTextWidth(measuredWord, style);
91
+ const trailingWhitespace = word.slice(measuredWord.length);
92
+ if (trailingWhitespace.length === 0) return {
93
+ measuredWord,
94
+ wordWidth,
95
+ fullWordWidth: wordWidth
96
+ };
97
+ if (style.kerning || trailingWhitespace.includes(" ")) return {
98
+ measuredWord,
99
+ wordWidth,
100
+ fullWordWidth: measureTextWidth(word, style)
101
+ };
102
+ const trailingWhitespaceWidth = measureTextWidth(trailingWhitespace, style);
103
+ const boundarySpacing = measuredWord.length > 0 ? (style.letterSpacing ?? 0) * getHorizontalScaleFactor(style.horizontalScale) : 0;
104
+ return {
105
+ measuredWord,
106
+ wordWidth,
107
+ fullWordWidth: wordWidth + trailingWhitespaceWidth + boundarySpacing
108
+ };
109
+ }
110
+ const scaledLetterSpacing = (style) => (style.letterSpacing ?? 0) * getHorizontalScaleFactor(style.horizontalScale);
111
+ /**
112
+ * Character spacing still owed after the last character placed on the line,
113
+ * or 0 when the line has no preceding character at `charIndex` of `runIndex`.
114
+ * Within a run it is the run's own spacing; at a boundary between two text
115
+ * runs it is the preceding run's spacing on its final character.
116
+ */
117
+ function precedingLetterSpacing({ block, line, runIndex, charIndex, runStyle }) {
118
+ if (line.fromRun === line.toRun && line.fromChar === line.toChar) return 0;
119
+ if (line.toRun === runIndex && line.toChar === charIndex) return scaledLetterSpacing(runStyle);
120
+ if (charIndex !== 0 || line.toRun !== runIndex - 1) return 0;
121
+ const previousRun = block.runs[runIndex - 1];
122
+ if (!previousRun || !isTextRun(previousRun) || previousRun.text.length === 0 || line.toChar !== previousRun.text.length) return 0;
123
+ return scaledLetterSpacing(runToFontStyle(previousRun));
124
+ }
125
+ function trailingCodePoint(text) {
126
+ if (text.length === 0) return;
127
+ const trailing = text.charCodeAt(text.length - 1);
128
+ const preceding = text.charCodeAt(text.length - 2);
129
+ const start = trailing >= 56320 && trailing <= 57343 && preceding >= 55296 && preceding <= 56319 ? text.length - 2 : text.length - 1;
130
+ return text.slice(start);
131
+ }
132
+ const usesDefaultHangingPunctuationClassifier = () => {
133
+ return (getLineBreakProvider().isHangingPunctuation ?? defaultLineBreakProvider.isHangingPunctuation) === defaultLineBreakProvider.isHangingPunctuation;
134
+ };
135
+ function trailingHangingPunctuationWidth(text, style, policy, enabled) {
136
+ if (!enabled || text.length === 0) return 0;
137
+ const trailing = trailingCodePoint(text);
138
+ if (usesDefaultHangingPunctuationClassifier() && !isHangingPunctuation(trailing ?? "", policy)) return 0;
139
+ const lastStart = findGraphemeBreaks(text, policy).at(-2) ?? 0;
140
+ const lastGrapheme = text.slice(lastStart);
141
+ if (!isHangingPunctuation(lastGrapheme, policy)) return 0;
142
+ return measureTextWidth(lastGrapheme, style);
143
+ }
144
+ //#endregion
145
+ export { exceedsHyphenationZone, findMaxFittingLength, isFinalTextCandidate, isIgnorableFinalTailRun, measureWordWithTrailingWhitespace, precedingLetterSpacing, resolveTextCandidateFit, trailingHangingPunctuationWidth, trimTrailingSpacesAndTabs };
@@ -0,0 +1,44 @@
1
+ import { ParagraphAttrs, ParagraphSpacing, TextRun } from "../types.js";
2
+ import { FontMetrics, FontStyle } from "./measureTypes.js";
3
+ import { LineTypography } from "./paragraphMeasureShared.js";
4
+ //#region src/layout-engine/measure/lineTypography.d.ts
5
+ /**
6
+ * Line-HEIGHT style for a text run. Word derives a CJK line's height from an
7
+ * East-Asian face, not the run's ascii font: real documents routinely put CJK
8
+ * glyphs in runs whose ascii/eastAsia fonts are Latin (e.g. `w:eastAsia`
9
+ * "Century"), and Word font-links those glyphs to the default East-Asian face
10
+ * and uses its taller single-line height (≈1.303 vs the ≈1.15 Latin default).
11
+ *
12
+ * When the run holds CJK text, swap `fontFamily` to the face whose
13
+ * `singleLineRatio` Word would actually use: the run's `w:eastAsia` font when
14
+ * it is a real CJK face, the run's own ascii font when THAT is a CJK face, or
15
+ * `CJK_FALLBACK_FONT_FAMILY` otherwise. Non-CJK runs return `baseStyle`
16
+ * unchanged, so Latin-only documents are unaffected by construction.
17
+ *
18
+ * The result feeds `updateMaxFont` (line height) ONLY — width measurement
19
+ * keeps the base style so wrapping is untouched.
20
+ */
21
+ declare function cjkLineHeightStyle(run: TextRun, baseStyle: FontStyle): FontStyle;
22
+ /** Pick the dominant typography metrics for a run containing complex script. */
23
+ declare function complexScriptLineHeightStyle(run: TextRun, baseStyle: FontStyle): FontStyle;
24
+ /**
25
+ * Calculate typography metrics from font size and spacing settings
26
+ *
27
+ * @param fontSize - Font size in points
28
+ * @param spacing - Paragraph spacing settings
29
+ * @param metrics - Pre-calculated font metrics (in pixels)
30
+ */
31
+ declare function calculateTypographyMetrics(fontSize: number, spacing?: ParagraphSpacing, metrics?: FontMetrics | null): LineTypography;
32
+ /**
33
+ * Calculate metrics for an empty paragraph.
34
+ *
35
+ * Word renders an empty paragraph as a single readable line — its line
36
+ * height never collapses below 1.15 × font size, even when the doc
37
+ * explicitly writes `<w:line w:val="240"/>` (1.0×). The floor is scoped to
38
+ * `auto`/`atLeast` line rules; `exact` means exact (per OOXML §17.3.1.33)
39
+ * and stays untouched.
40
+ */
41
+ declare function calculateEmptyParagraphMetrics(fontSize: number, spacing?: ParagraphSpacing, fontFamily?: string, attrs?: ParagraphAttrs): LineTypography;
42
+ declare function applyDocumentGrid(typography: LineTypography, attrs: ParagraphAttrs | undefined, spacing: ParagraphSpacing | undefined): LineTypography;
43
+ //#endregion
44
+ export { applyDocumentGrid, calculateEmptyParagraphMetrics, calculateTypographyMetrics, cjkLineHeightStyle, complexScriptLineHeightStyle };
@@ -0,0 +1,124 @@
1
+ import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
2
+ import { hasCjk, hasComplexScript, hasEastAsiaSlotText } from "../../utils/scriptSegments.js";
3
+ import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./complexScriptFormatting.js";
4
+ import { ptToPx } from "./measureHelpers.js";
5
+ import { getFontMetrics } from "./measureProvider.js";
6
+ import "./paragraphMeasureShared.js";
7
+ //#region src/layout-engine/measure/lineTypography.ts
8
+ /**
9
+ * Line height and typography metrics: font-driven line heights, East Asian
10
+ * and complex-script line height styles, empty-paragraph metrics, and
11
+ * document-grid line pitch.
12
+ */
13
+ /**
14
+ * Line-HEIGHT style for a text run. Word derives a CJK line's height from an
15
+ * East-Asian face, not the run's ascii font: real documents routinely put CJK
16
+ * glyphs in runs whose ascii/eastAsia fonts are Latin (e.g. `w:eastAsia`
17
+ * "Century"), and Word font-links those glyphs to the default East-Asian face
18
+ * and uses its taller single-line height (≈1.303 vs the ≈1.15 Latin default).
19
+ *
20
+ * When the run holds CJK text, swap `fontFamily` to the face whose
21
+ * `singleLineRatio` Word would actually use: the run's `w:eastAsia` font when
22
+ * it is a real CJK face, the run's own ascii font when THAT is a CJK face, or
23
+ * `CJK_FALLBACK_FONT_FAMILY` otherwise. Non-CJK runs return `baseStyle`
24
+ * unchanged, so Latin-only documents are unaffected by construction.
25
+ *
26
+ * The result feeds `updateMaxFont` (line height) ONLY — width measurement
27
+ * keeps the base style so wrapping is untouched.
28
+ */
29
+ function cjkLineHeightStyle(run, baseStyle) {
30
+ if (!run.text) return baseStyle;
31
+ const eastAsia = baseStyle.eastAsiaFontFamily;
32
+ const eastAsiaFace = eastAsia !== void 0 && isCjkFont(eastAsia);
33
+ if (!hasCjk(run.text) && !(eastAsiaFace && hasEastAsiaSlotText(run.text, baseStyle.eastAsiaHint))) return baseStyle;
34
+ if (eastAsia !== void 0 && eastAsiaFace) {
35
+ const result = {
36
+ ...baseStyle,
37
+ fontFamily: eastAsia
38
+ };
39
+ delete result.alternateFontFamily;
40
+ if (baseStyle.eastAsiaAlternateFontFamily !== void 0) result.alternateFontFamily = baseStyle.eastAsiaAlternateFontFamily;
41
+ return result;
42
+ }
43
+ if (isCjkFont(baseStyle.fontFamily ?? "Times New Roman")) return baseStyle;
44
+ return {
45
+ ...baseStyle,
46
+ fontFamily: CJK_FALLBACK_FONT_FAMILY
47
+ };
48
+ }
49
+ /** Pick the dominant typography metrics for a run containing complex script. */
50
+ function complexScriptLineHeightStyle(run, baseStyle) {
51
+ if (!hasComplexScriptFormatting(run) || !run.forceComplexScript && !hasComplexScript(run.text)) return baseStyle;
52
+ const complexStyle = applyComplexScriptFormatting(baseStyle, run);
53
+ return (complexStyle.fontSize ?? 11) >= (baseStyle.fontSize ?? 11) ? complexStyle : baseStyle;
54
+ }
55
+ /**
56
+ * Calculate typography metrics from font size and spacing settings
57
+ *
58
+ * @param fontSize - Font size in points
59
+ * @param spacing - Paragraph spacing settings
60
+ * @param metrics - Pre-calculated font metrics (in pixels)
61
+ */
62
+ function calculateTypographyMetrics(fontSize, spacing, metrics) {
63
+ const fontSizePx = ptToPx(fontSize);
64
+ const ascent = metrics?.ascent ?? fontSizePx * .8;
65
+ const descent = metrics?.descent ?? fontSizePx * .2;
66
+ const singleLineBase = fontSizePx * (metrics?.singleLineRatio ?? 1.15);
67
+ let lineHeight;
68
+ if (spacing?.lineRule === "exact" && spacing.line !== void 0) lineHeight = spacing.line;
69
+ else if (spacing?.lineRule === "atLeast" && spacing.line !== void 0) {
70
+ const defaultHeight = singleLineBase * 1;
71
+ lineHeight = Math.max(spacing.line, defaultHeight);
72
+ } else if (spacing?.line !== void 0 && spacing.lineUnit === "multiplier") lineHeight = singleLineBase * spacing.line;
73
+ else if (spacing?.line !== void 0 && spacing.lineUnit === "px") lineHeight = spacing.line;
74
+ else lineHeight = singleLineBase * 1;
75
+ return {
76
+ ascent,
77
+ descent,
78
+ lineHeight
79
+ };
80
+ }
81
+ /**
82
+ * Word's "single line spacing" floor (≈ 1.15×) applied to empty paragraphs
83
+ * with `auto`/`atLeast` line rules. Without this, narrow-metric fonts
84
+ * (Arial Narrow, OS/2 ratio ≈ 1.117) collapse empty rows visibly tighter
85
+ * than Word renders them. See eigenpal #391/#394.
86
+ */
87
+ const WORD_SINGLE_LINE_FLOOR = 1.15;
88
+ /**
89
+ * Calculate metrics for an empty paragraph.
90
+ *
91
+ * Word renders an empty paragraph as a single readable line — its line
92
+ * height never collapses below 1.15 × font size, even when the doc
93
+ * explicitly writes `<w:line w:val="240"/>` (1.0×). The floor is scoped to
94
+ * `auto`/`atLeast` line rules; `exact` means exact (per OOXML §17.3.1.33)
95
+ * and stays untouched.
96
+ */
97
+ function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily, attrs) {
98
+ const result = calculateTypographyMetrics(fontSize, spacing, getFontMetrics({
99
+ fontSize,
100
+ fontFamily: fontFamily ?? "Times New Roman",
101
+ ...attrs?.defaultAlternateFontFamily !== void 0 ? { alternateFontFamily: attrs.defaultAlternateFontFamily } : {}
102
+ }));
103
+ const lineRule = spacing?.lineRule ?? "auto";
104
+ if (lineRule === "auto" || lineRule === "atLeast") {
105
+ const floor = ptToPx(fontSize) * WORD_SINGLE_LINE_FLOOR;
106
+ if (result.lineHeight < floor) return applyDocumentGrid({
107
+ ...result,
108
+ lineHeight: floor
109
+ }, attrs, spacing);
110
+ }
111
+ return applyDocumentGrid(result, attrs, spacing);
112
+ }
113
+ function applyDocumentGrid(typography, attrs, spacing) {
114
+ const pitch = attrs?.documentGridLinePitch;
115
+ if (pitch === void 0 || pitch <= 0 || attrs?.snapToGrid === false) return typography;
116
+ if (spacing?.lineRule === "exact") return typography;
117
+ const lineHeight = Math.ceil(typography.lineHeight / pitch - 1e-9) * pitch;
118
+ return lineHeight === typography.lineHeight ? typography : {
119
+ ...typography,
120
+ lineHeight
121
+ };
122
+ }
123
+ //#endregion
124
+ export { applyDocumentGrid, calculateEmptyParagraphMetrics, calculateTypographyMetrics, cjkLineHeightStyle, complexScriptLineHeightStyle };
@@ -1,14 +1,16 @@
1
1
  import { hasPageBreakBefore } from "../keep-together.js";
2
2
  import { recordMeasureBlock, recordMeasureBlockError } from "../layoutInstrumentation.js";
3
3
  import { isParagraphFrameTextBox } from "../paragraphFrame.js";
4
+ import { resolveTableSpacing } from "../paragraphSpacing.js";
4
5
  import { sectionStartAdvanceOf } from "../section-breaks.js";
5
6
  import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "../textBoxFlow.js";
6
7
  import { getTextBoxGroupId } from "../textBoxGroup.js";
7
8
  import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, floatingTextBoxWrapsText, resolveTableCellPadding, resolveTableWidthPx, tableColumnsArePinned } from "../types.js";
8
9
  import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
10
+ import { findClearLineY } from "./floatingLineClearance.js";
9
11
  import { resolveFloatingTablePageX } from "./floatingTablePosition.js";
10
12
  import { getHyphenationDictionaryMisses } from "./hyphenationDictionaries.js";
11
- import { findClearLineY, measureParagraph } from "./measureParagraph.js";
13
+ import { measureParagraph } from "./measureParagraph.js";
12
14
  import { buildTableCellFloatingZones, getTableCellFloatingImages } from "./tableCellFloating.js";
13
15
  import { createTableCellFlowState, finishTableCellFlow, placeTableCellBlock } from "./tableCellFlow.js";
14
16
  import { buildTableCellGrid, getFirstAvailableColumn, getTableCellSpacingInsetsX, getTableCellSpacingInsetsY, getTableCellVerticalBorderHeight } from "./tableCellGrid.js";
@@ -97,6 +99,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
97
99
  }
98
100
  }
99
101
  const cellGrid = buildTableCellGrid(tableBlock.rows, columnWidths.length);
102
+ const spacingRows = resolveTableSpacing(tableBlock).rows;
100
103
  const cellSpacing = tableBlock.cellSpacing;
101
104
  const bordersSeparated = (cellSpacing ?? 0) > 0;
102
105
  const rowCount = tableBlock.rows.length;
@@ -104,7 +107,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
104
107
  const rows = tableBlock.rows.map((row, rowIdx) => {
105
108
  let columnIndex = getFirstAvailableColumn(cellGrid, rowIdx, row.gridBefore ?? 0);
106
109
  return {
107
- cells: row.cells.map((cell) => {
110
+ cells: row.cells.map((cell, cellIdx) => {
108
111
  const colSpan = cell.colSpan ?? 1;
109
112
  let cellWidth = 0;
110
113
  for (let c = 0; c < colSpan && columnIndex + c < columnWidths.length; c++) cellWidth += columnWidths[columnIndex + c] ?? 0;
@@ -121,7 +124,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
121
124
  width: cellWidth,
122
125
  height: 0
123
126
  };
124
- if (!keepSingleLine) wrapTableCellParagraphsAroundFloats(cell, cellMeasure, cellContentWidth, fieldValues);
127
+ if (!keepSingleLine) wrapTableCellParagraphsAroundFloats(spacingRows[rowIdx]?.cells[cellIdx] ?? cell, cellMeasure, cellContentWidth, fieldValues);
125
128
  if (cell.colSpan !== void 0) cellMeasure.colSpan = cell.colSpan;
126
129
  if (cell.rowSpan !== void 0) cellMeasure.rowSpan = cell.rowSpan;
127
130
  return cellMeasure;
@@ -141,7 +144,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
141
144
  }));
142
145
  continue;
143
146
  }
144
- const sourceRowCells = tableBlock.rows[rowIdx]?.cells;
147
+ const sourceRowCells = spacingRows[rowIdx]?.cells;
145
148
  let maxCellHeightWithBorders = 0;
146
149
  let maxCellInsets = 0;
147
150
  for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
@@ -6,6 +6,7 @@ import { buildFontString, getResolvedData, ptToPx } from "./measureHelpers.js";
6
6
  import { setMeasureProvider } from "./measureProvider.js";
7
7
  import { canPrefetchMeasurement, prefetchMeasurement } from "./measureWorker.js";
8
8
  import { WORKER_FONT_FINGERPRINT_TEXT, countCodePoints } from "./measureWorkerProtocol.js";
9
+ import { SMALL_CAPS_SCALE, smallCapsMask, smallCapsSegments } from "./smallCapsCasing.js";
9
10
  import { getFontKerningMode } from "./textMeasurementPolicy.js";
10
11
  import { panic } from "better-result";
11
12
  //#region src/layout-engine/measure/measureContainer.ts
@@ -136,6 +137,7 @@ function canvasMeasureTextWidth(text, sourceStyle) {
136
137
  if (!text) return 0;
137
138
  const style = sourceStyle.forceComplexScript ? applyComplexScriptFormatting(sourceStyle, sourceStyle) : sourceStyle;
138
139
  const measuredText = applyTextTransform(text, style);
140
+ if (!sourceStyle.forceComplexScript && style.fontVariant === "small-caps") return measureSmallCapsWidth(measuredText, style);
139
141
  if (!sourceStyle.forceComplexScript && !style.letterSpacing && needsPerScriptFonts(style, measuredText)) return measureMixedScriptWidth(measuredText, style);
140
142
  const ctx = getCanvasContext();
141
143
  const font = buildFontString(style);
@@ -228,6 +230,38 @@ function measureMixedScriptWidth(measuredText, style) {
228
230
  return width * horizontalScale;
229
231
  }
230
232
  /**
233
+ * Width of a `w:smallCaps` run: its lowercase letters (and the whitespace
234
+ * that follows them — see smallCapsCasing.ts) draw at `SMALL_CAPS_SCALE` of
235
+ * the run's size, everything else at full size. A small segment is measured
236
+ * at that literal, smaller font string — not the full-size advance scaled by
237
+ * arithmetic — so hinting and sub-pixel snapping agree with the painted DOM
238
+ * span, which sets this same font-size. Each segment still recurses into
239
+ * {@link canvasMeasureTextWidth} (so one that itself needs the East-Asian or
240
+ * complex-script font still gets it); letter spacing and horizontal scale
241
+ * apply once over the whole string, as {@link measureMixedScriptWidth} does
242
+ * for a script split.
243
+ */
244
+ function measureSmallCapsWidth(measuredText, style) {
245
+ const letterSpacing = style.letterSpacing ?? 0;
246
+ const horizontalScale = getHorizontalScaleFactor(style.horizontalScale);
247
+ const segmentStyle = { ...style };
248
+ delete segmentStyle.fontVariant;
249
+ delete segmentStyle.letterSpacing;
250
+ delete segmentStyle.horizontalScale;
251
+ const smallSegmentStyle = {
252
+ ...segmentStyle,
253
+ fontSize: (segmentStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
254
+ };
255
+ let glyphWidth = 0;
256
+ for (const segment of smallCapsSegments(measuredText)) glyphWidth += canvasMeasureTextWidth(segment.text, segment.small ? smallSegmentStyle : segmentStyle);
257
+ let width = glyphWidth;
258
+ if (letterSpacing) {
259
+ const codePoints = countCodePoints(measuredText);
260
+ if (codePoints > 1) width += letterSpacing * (codePoints - 1);
261
+ }
262
+ return width * horizontalScale;
263
+ }
264
+ /**
231
265
  * Speculatively enqueue the slice lengths that a subsequent
232
266
  * `findMaxFittingLength` binary search is likely to probe. We pick the
233
267
  * geometric series (full, half, quarter, eighth) which covers the
@@ -293,18 +327,26 @@ function canvasMeasureRun(text, sourceStyle) {
293
327
  ...style.eastAsiaAlternateFontFamily !== void 0 ? { alternateFontFamily: style.eastAsiaAlternateFontFamily } : {}
294
328
  }) : void 0;
295
329
  const complexScriptFont = hasComplexScriptFormatting(style) && !style.letterSpacing ? buildFontString(scriptStyle(style, SCRIPT_CLASS.complex)) : void 0;
330
+ const smallCapsFont = style.fontVariant === "small-caps" ? buildFontString({
331
+ ...style,
332
+ fontSize: (style.fontSize ?? 11) * SMALL_CAPS_SCALE
333
+ }) : void 0;
296
334
  const letterSpacing = style.letterSpacing ?? 0;
297
335
  const scale = getHorizontalScaleFactor(style.horizontalScale);
298
336
  const charWidths = [];
299
337
  let totalWidth = 0;
338
+ const smallCapsAt = smallCapsFont !== void 0 ? smallCapsMask(text) : void 0;
300
339
  let offset = 0;
340
+ let codePointIndex = 0;
301
341
  for (const char of text) {
302
342
  const cp = char.codePointAt(0);
303
- const measured = applyTextTransform(char, style);
304
- if (eastAsiaFont !== void 0 || complexScriptFont !== void 0) {
343
+ const isSmallCap = smallCapsAt?.[codePointIndex] ?? false;
344
+ const measured = isSmallCap ? char.toLocaleUpperCase() : applyTextTransform(char, style);
345
+ if (isSmallCap && smallCapsFont !== void 0) ctx.font = smallCapsFont;
346
+ else if (eastAsiaFont !== void 0 || complexScriptFont !== void 0) {
305
347
  const script = scriptClassOf(cp, style.eastAsiaHint);
306
348
  ctx.font = (script === SCRIPT_CLASS.eastAsia ? eastAsiaFont : void 0) ?? (script === SCRIPT_CLASS.complex ? complexScriptFont : void 0) ?? baseFont;
307
- }
349
+ } else if (smallCapsFont !== void 0) ctx.font = baseFont;
308
350
  let charWidth = ctx.measureText(measured).width;
309
351
  if (letterSpacing && offset + char.length < text.length) charWidth += letterSpacing;
310
352
  charWidth *= scale;
@@ -312,6 +354,7 @@ function canvasMeasureRun(text, sourceStyle) {
312
354
  if (char.length === 2) charWidths.push(0);
313
355
  totalWidth += charWidth;
314
356
  offset += char.length;
357
+ codePointIndex += 1;
315
358
  }
316
359
  return {
317
360
  width: totalWidth,
@@ -34,6 +34,18 @@ function docxScriptFontSize(fontSize) {
34
34
  return Math.max(1, Math.floor(fontSize * 2 * 2 / 3)) / 2;
35
35
  }
36
36
  /**
37
+ * The `textTransform`/`fontVariant` a run's caps toggles contribute.
38
+ *
39
+ * `w:caps` wins when both are set: full-size capitals, never synthesized
40
+ * small ones (matching the else-if in formatToStyle.ts's live editor
41
+ * styling).
42
+ */
43
+ function capsStyleOf(run) {
44
+ if (run.allCaps) return { textTransform: "uppercase" };
45
+ if (run.smallCaps) return { fontVariant: "small-caps" };
46
+ return {};
47
+ }
48
+ /**
37
49
  * Build a measurement `FontStyle` from a run's formatting. Single source of
38
50
  * truth for run → FontStyle so every measurement path (layout line-breaking,
39
51
  * click-to-position, selection rects) carries the same fields — notably
@@ -61,8 +73,7 @@ function buildRunFontStyle(run, fallbackFontFamily, fallbackFontSize) {
61
73
  ...run.bold !== void 0 ? { bold: run.bold } : {},
62
74
  ...run.italic !== void 0 ? { italic: run.italic } : {},
63
75
  ...run.letterSpacing !== void 0 ? { letterSpacing: run.letterSpacing } : {},
64
- ...run.allCaps ? { textTransform: "uppercase" } : {},
65
- ...run.smallCaps ? { fontVariant: "small-caps" } : {},
76
+ ...capsStyleOf(run),
66
77
  ...horizontalScale !== void 0 ? { horizontalScale } : {},
67
78
  kerning: getRunFontKerningMode(run, fallbackFontSize) === FONT_KERNING_MODE.enabled
68
79
  };
@@ -119,7 +130,6 @@ function clearFontResolvedCache() {
119
130
  function buildFontString(style) {
120
131
  const parts = [];
121
132
  if (style.italic) parts.push("italic");
122
- if (style.fontVariant) parts.push(style.fontVariant);
123
133
  if (style.bold) parts.push("700");
124
134
  const fontSizePx = ptToPx(style.fontSize ?? 11);
125
135
  parts.push(`${fontSizePx}px`);
@@ -1,6 +1,7 @@
1
1
  import { ParagraphBlock, ParagraphMeasure, TextRun } from "../types.js";
2
2
  import { FloatingImageZone } from "./floatingZones.js";
3
3
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
4
+ import { MIN_WRAP_SEGMENT_WIDTH, findClearLineY } from "./floatingLineClearance.js";
4
5
  //#region src/layout-engine/measure/measureParagraph.d.ts
5
6
  /**
6
7
  * Options for paragraph measurement
@@ -16,26 +17,6 @@ type MeasureParagraphOptions = {
16
17
  /** Header/footer tabs may be authored in the page margin, beyond body width. */
17
18
  allowEndTabOverflow?: boolean;
18
19
  };
19
- /**
20
- * Minimum horizontal room a line must offer before we treat it as usable for
21
- * body text. Below this threshold the line is bumped past obstructing floats
22
- * via `findClearLineY` instead of being rendered into the unusable sliver.
23
- * Without this guard, a near-full-width float (e.g. floating table) produces
24
- * a ~2px segment that collapses every wrap line to one glyph per row.
25
- */
26
- declare const MIN_WRAP_SEGMENT_WIDTH = 24;
27
- /**
28
- * Find the next vertical position at or below `startY` where the available
29
- * text width is at least `minWidth`. Used to skip lines past stacked floats
30
- * when there is no horizontal room for meaningful text at the current Y.
31
- *
32
- * Returns `startY` if the current position already has enough room, otherwise
33
- * the lowest `bottomY` of any zone currently obstructing the line. The caller
34
- * is expected to re-query margins at the returned Y.
35
- *
36
- * Coordinates are absolute (i.e., already include any paragraphYOffset).
37
- */
38
- declare function findClearLineY(startY: number, lineHeight: number, zones: FloatingImageZone[] | undefined, contentWidth: number, minWidth: number): number;
39
20
  /**
40
21
  * Measure a paragraph block and compute line breaks
41
22
  *