@stll/folio-core 0.48.0 → 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 (86) 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/layoutPipeline.js +11 -1
  5. package/dist/display-list/build/paragraphPrimitives.js +63 -7
  6. package/dist/display-list/build/textBoxPrimitives.js +1 -1
  7. package/dist/display-list/build/unsupported.d.ts +2 -2
  8. package/dist/display-list/build/unsupported.js +0 -0
  9. package/dist/display-list/dom/renderDisplayListToDom.js +0 -1
  10. package/dist/docx/blockContentParser.js +2 -2
  11. package/dist/docx/bulletMarkers.d.ts +18 -2
  12. package/dist/docx/bulletMarkers.js +27 -2
  13. package/dist/docx/drawingGroupChildren.d.ts +60 -0
  14. package/dist/docx/drawingGroupChildren.js +176 -0
  15. package/dist/docx/drawingUtils.js +3 -0
  16. package/dist/docx/groupDrawingParser.d.ts +7 -1
  17. package/dist/docx/groupDrawingParser.js +123 -32
  18. package/dist/docx/packageParts.d.ts +45 -1
  19. package/dist/docx/packageParts.js +93 -2
  20. package/dist/docx/paraIdAttribute.d.ts +3 -2
  21. package/dist/docx/paragraphParser.js +91 -7
  22. package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
  23. package/dist/docx/runParser.js +10 -0
  24. package/dist/docx/selectiveSaveFlags.d.ts +10 -3
  25. package/dist/docx/selectiveSaveFlags.js +1 -1
  26. package/dist/docx/selectiveXmlPatch.d.ts +56 -34
  27. package/dist/docx/selectiveXmlPatch.js +222 -115
  28. package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
  29. package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
  30. package/dist/docx/serializer/paragraphSerializer.js +5 -4
  31. package/dist/docx/serializer/runSerializer.d.ts +5 -1
  32. package/dist/docx/serializer/runSerializer.js +4 -3
  33. package/dist/docx/shapeAlternateContent.d.ts +25 -0
  34. package/dist/docx/shapeAlternateContent.js +52 -0
  35. package/dist/docx/textBoxParser.d.ts +10 -2
  36. package/dist/docx/textBoxParser.js +9 -3
  37. package/dist/internal/headlessRevisionResolution.js +50 -13
  38. package/dist/layout-bridge/convert/toFlowBlocks.js +26 -4
  39. package/dist/layout-engine/index.d.ts +2 -2
  40. package/dist/layout-engine/index.js +33 -4
  41. package/dist/layout-engine/keep-together.d.ts +5 -2
  42. package/dist/layout-engine/keep-together.js +5 -1
  43. package/dist/layout-engine/measure/advanceComposition.js +49 -3
  44. package/dist/layout-engine/measure/measureBlocks.js +5 -3
  45. package/dist/layout-engine/measure/measureContainer.js +46 -3
  46. package/dist/layout-engine/measure/measureHelpers.js +13 -3
  47. package/dist/layout-engine/measure/measureParagraph.js +44 -81
  48. package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
  49. package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
  50. package/dist/layout-engine/paginator.d.ts +2 -0
  51. package/dist/layout-engine/paginator.js +5 -1
  52. package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
  53. package/dist/layout-engine/paragraphSpacing.js +2 -1
  54. package/dist/layout-engine/tableRowBreak.d.ts +8 -1
  55. package/dist/layout-engine/tableRowBreak.js +22 -1
  56. package/dist/layout-engine/types.d.ts +21 -2
  57. package/dist/layout-painter/renderParagraph.js +41 -4
  58. package/dist/layout-painter/renderTextBox.d.ts +12 -2
  59. package/dist/layout-painter/renderTextBox.js +26 -2
  60. package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
  61. package/dist/prosemirror/alternateContentAttrs.js +35 -0
  62. package/dist/prosemirror/attrs/index.js +90 -0
  63. package/dist/prosemirror/commands/comments.js +51 -41
  64. package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
  65. package/dist/prosemirror/contentControlRevisions.js +84 -0
  66. package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
  67. package/dist/prosemirror/conversion/toProseDoc.js +42 -19
  68. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
  69. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
  70. package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
  71. package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
  72. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
  73. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
  74. package/dist/prosemirror/listMarker.js +2 -2
  75. package/dist/prosemirror/paragraphDirection.d.ts +29 -2
  76. package/dist/prosemirror/paragraphDirection.js +21 -2
  77. package/dist/prosemirror/plugins/suggestionMode.js +37 -8
  78. package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
  79. package/dist/prosemirror/rejoinRunCarriers.js +66 -0
  80. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
  81. package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
  82. package/dist/prosemirror/schema/nodes.d.ts +37 -5
  83. package/dist/types/content.d.ts +2 -2
  84. package/package.json +2 -2
  85. package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
  86. package/dist/layout-engine/justifiedLineFit.js +0 -6
@@ -0,0 +1,25 @@
1
+ import { Shape, ShapeAlternateContent, ShapeContent } from "../types/content.js";
2
+ import { XmlElement } from "./xmlParser.js";
3
+ //#region src/docx/shapeAlternateContent.d.ts
4
+ /**
5
+ * Capture the `mc:AlternateContent` a shape was read from, when the shape's
6
+ * drawing is the sole child of the branch it came from.
7
+ */
8
+ declare const captureShapeAlternateContent: ({ shape, alternateContent, branch, drawing }: {
9
+ shape: Shape;
10
+ alternateContent: XmlElement;
11
+ branch: XmlElement;
12
+ drawing: XmlElement;
13
+ }) => ShapeAlternateContent | undefined;
14
+ /** Re-fingerprint a capture for a shape rebuilt from an unedited projection. */
15
+ declare const refingerprintShapeAlternateContent: (shape: Shape, verbatimXml: string) => ShapeAlternateContent;
16
+ /**
17
+ * The captured `mc:AlternateContent` to write in place of the shape's drawing,
18
+ * or undefined when the shape must be regenerated.
19
+ *
20
+ * Replay requires the modeled shape to still match the capture. An edited shape
21
+ * is regenerated as a bare `w:drawing` without the stale Fallback.
22
+ */
23
+ declare const replayableShapeAlternateContent: (content: ShapeContent) => string | undefined;
24
+ //#endregion
25
+ export { captureShapeAlternateContent, refingerprintShapeAlternateContent, replayableShapeAlternateContent };
@@ -0,0 +1,52 @@
1
+ import { canonicalJson } from "../utils/canonicalJson.js";
2
+ import { captureVerbatimXml } from "./verbatimCapture.js";
3
+ import { getChildElements, getLocalName } from "./xmlParser.js";
4
+ //#region src/docx/shapeAlternateContent.ts
5
+ /**
6
+ * Every modeled field, text body included: the Fallback repeats the shape's
7
+ * geometry and its text, so an edit to either makes the captured element stale.
8
+ */
9
+ const shapeFingerprint = (shape) => canonicalJson(shape);
10
+ /**
11
+ * Whether `element` is the only child element of its `mc:Choice` or
12
+ * `mc:Fallback` branch.
13
+ *
14
+ * A branch holding more than the one drawing cannot be written back by the
15
+ * drawing's shape alone: the siblings are parsed into their own run content,
16
+ * and replaying the whole element would write them twice.
17
+ */
18
+ const isSoleBranchChild = (branch, element) => {
19
+ const children = getChildElements(branch);
20
+ return children.length === 1 && children[0] === element;
21
+ };
22
+ /**
23
+ * Capture the `mc:AlternateContent` a shape was read from, when the shape's
24
+ * drawing is the sole child of the branch it came from.
25
+ */
26
+ const captureShapeAlternateContent = ({ shape, alternateContent, branch, drawing }) => {
27
+ if (getLocalName(alternateContent.name ?? "") !== "AlternateContent") return;
28
+ if (!isSoleBranchChild(branch, drawing)) return;
29
+ return {
30
+ verbatimXml: captureVerbatimXml(alternateContent),
31
+ verbatimFingerprint: shapeFingerprint(shape)
32
+ };
33
+ };
34
+ /** Re-fingerprint a capture for a shape rebuilt from an unedited projection. */
35
+ const refingerprintShapeAlternateContent = (shape, verbatimXml) => ({
36
+ verbatimXml,
37
+ verbatimFingerprint: shapeFingerprint(shape)
38
+ });
39
+ /**
40
+ * The captured `mc:AlternateContent` to write in place of the shape's drawing,
41
+ * or undefined when the shape must be regenerated.
42
+ *
43
+ * Replay requires the modeled shape to still match the capture. An edited shape
44
+ * is regenerated as a bare `w:drawing` without the stale Fallback.
45
+ */
46
+ const replayableShapeAlternateContent = (content) => {
47
+ const captured = content.alternateContent;
48
+ if (captured === void 0) return;
49
+ return captured.verbatimFingerprint === shapeFingerprint(content.shape) ? captured.verbatimXml : void 0;
50
+ };
51
+ //#endregion
52
+ export { captureShapeAlternateContent, refingerprintShapeAlternateContent, replayableShapeAlternateContent };
@@ -119,8 +119,16 @@ declare function resolveTextBoxOutlineColor(textBox: document_d_exports.TextBox)
119
119
  * Get outline width in pixels
120
120
  */
121
121
  declare function getTextBoxOutlineWidthPx(textBox: document_d_exports.TextBox): number;
122
+ /** A text-box `w:drawing`, with the `mc:AlternateContent` branch it sits in, if any. */
123
+ type TextBoxDrawing = {
124
+ drawing: XmlElement;
125
+ alternateContent?: {
126
+ element: XmlElement;
127
+ branch: XmlElement;
128
+ };
129
+ };
122
130
  type TextBoxRunScan = {
123
- textBoxDrawings: XmlElement[];
131
+ textBoxDrawings: TextBoxDrawing[];
124
132
  vmlTextBoxes: XmlElement[];
125
133
  hasNonTextBoxContent: boolean;
126
134
  };
@@ -143,4 +151,4 @@ type ScanRunForTextBoxDrawingsOptions = {
143
151
  };
144
152
  declare const scanRunForTextBoxDrawings: ({ xmlRun, claimedByRunParser }: ScanRunForTextBoxDrawingsOptions) => TextBoxRunScan;
145
153
  //#endregion
146
- export { ParagraphParserFn, ScanRunForTextBoxDrawingsOptions, TableParserFn, TextBoxRunScan, extractTextBoxContentElements, getTextBoxContentElement, getTextBoxDimensionsPx, getTextBoxHeightPx, getTextBoxMarginsPx, getTextBoxOutlineWidthPx, getTextBoxText, getTextBoxWidthPx, hasTextBoxContent, hasTextBoxFill, hasTextBoxOutline, isFloatingTextBox, isShapeTextBox, isTextBoxDrawing, parseTextBox, parseTextBoxContent, parseTextBoxFromShape, resolveTextBoxFillColor, resolveTextBoxOutlineColor, scanRunForTextBoxDrawings };
154
+ export { ParagraphParserFn, ScanRunForTextBoxDrawingsOptions, TableParserFn, TextBoxDrawing, TextBoxRunScan, extractTextBoxContentElements, getTextBoxContentElement, getTextBoxDimensionsPx, getTextBoxHeightPx, getTextBoxMarginsPx, getTextBoxOutlineWidthPx, getTextBoxText, getTextBoxWidthPx, hasTextBoxContent, hasTextBoxFill, hasTextBoxOutline, isFloatingTextBox, isShapeTextBox, isTextBoxDrawing, parseTextBox, parseTextBoxContent, parseTextBoxFromShape, resolveTextBoxFillColor, resolveTextBoxOutlineColor, scanRunForTextBoxDrawings };
@@ -328,9 +328,12 @@ const scanRunForTextBoxDrawings = ({ xmlRun, claimedByRunParser }) => {
328
328
  const textBoxDrawings = [];
329
329
  const vmlTextBoxes = [];
330
330
  let hasNonTextBoxContent = false;
331
- const visitDrawing = (drawingEl) => {
331
+ const visitDrawing = (drawingEl, alternateContent) => {
332
332
  if (isTextBoxDrawing(drawingEl)) {
333
- textBoxDrawings.push(drawingEl);
333
+ textBoxDrawings.push({
334
+ drawing: drawingEl,
335
+ ...alternateContent === void 0 ? {} : { alternateContent }
336
+ });
334
337
  return;
335
338
  }
336
339
  hasNonTextBoxContent = true;
@@ -355,7 +358,10 @@ const scanRunForTextBoxDrawings = ({ xmlRun, claimedByRunParser }) => {
355
358
  if (!branch) return false;
356
359
  let found = false;
357
360
  for (const innerEl of getChildElements(branch)) if (getLocalName(innerEl.name ?? "") === "drawing") {
358
- visitDrawing(innerEl);
361
+ visitDrawing(innerEl, {
362
+ element: el,
363
+ branch
364
+ });
359
365
  found = true;
360
366
  }
361
367
  return found;
@@ -1,6 +1,8 @@
1
1
  import { recreateProseNodeWithParagraphPropertySource } from "../docx/paragraphPropertySource.js";
2
2
  import { expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
3
+ import { resolutionRemovesControl, withoutResolvedEnclosures } from "../prosemirror/contentControlRevisions.js";
3
4
  import "../prosemirror/extensions/nodes/SdtExtension.js";
5
+ import { continuedRunMarks } from "../prosemirror/rejoinRunCarriers.js";
4
6
  import { RUN_FORMATTING_MARK_NAMES } from "../prosemirror/runFormattingMarkNames.js";
5
7
  import { reconstructRejectedRunFormattingMarks } from "../prosemirror/runPropertyChangeResolution.js";
6
8
  import { paragraphRunStyleContext } from "../prosemirror/runStyleFormatting.js";
@@ -75,8 +77,26 @@ const resolveInlineNode = ({ node, context, paragraphScope }) => {
75
77
  if (context.keepType) marks = marks.filter((mark) => mark.type !== context.keepType);
76
78
  return marksEqual(marks, node.marks) ? node : node.mark(marks);
77
79
  };
80
+ /**
81
+ * The pieces a revision split off its run, one run again: the headless half of
82
+ * `rejoinRunsAt`, over the children of one inline container.
83
+ */
84
+ const rejoinResolvedRuns = ({ children, resolvedBoundaries, paragraphScope, context }) => {
85
+ for (const index of [...resolvedBoundaries].toSorted((left, right) => left - right)) {
86
+ const left = children[index - 1];
87
+ const right = children[index];
88
+ if (!left || !right) continue;
89
+ const marks = continuedRunMarks({
90
+ left,
91
+ right,
92
+ context: () => resolveParagraphRunStyleScope(paragraphScope, context.styleResolver),
93
+ styleResolver: context.styleResolver
94
+ });
95
+ if (marks !== null) children[index] = right.mark(marks);
96
+ }
97
+ };
78
98
  const resolveInlineContent = ({ node, position, context, inheritedParagraphScope }) => {
79
- const paragraphScope = node.type.name === "paragraph" && context.mode === "reject" ? { paragraph: node } : inheritedParagraphScope;
99
+ const paragraphScope = node.type.name === "paragraph" ? { paragraph: node } : inheritedParagraphScope;
80
100
  const replacementRangeStart = context.replacementRanges.length;
81
101
  let resolvedNode = node;
82
102
  if (node.isInline) {
@@ -96,7 +116,21 @@ const resolveInlineContent = ({ node, position, context, inheritedParagraphScope
96
116
  resolvedNode = resolved;
97
117
  if (resolvedNode.isLeaf) return resolvedNode;
98
118
  }
119
+ if (resolutionRemovesControl({
120
+ control: resolvedNode,
121
+ removeType: context.removeType,
122
+ resolves: () => true
123
+ })) {
124
+ context.replacementRanges.push({
125
+ from: position,
126
+ to: position + node.nodeSize,
127
+ newSize: 0
128
+ });
129
+ return null;
130
+ }
99
131
  const children = [];
132
+ /** Indices into `children` where resolved content began or ended. */
133
+ const resolvedBoundaries = /* @__PURE__ */ new Set();
100
134
  const contentStart = resolvedNode.type.name === "doc" ? 0 : position + 1;
101
135
  let contentChanged = false;
102
136
  resolvedNode.forEach((child, offset) => {
@@ -106,20 +140,23 @@ const resolveInlineContent = ({ node, position, context, inheritedParagraphScope
106
140
  context,
107
141
  ...paragraphScope !== void 0 ? { inheritedParagraphScope: paragraphScope } : {}
108
142
  });
143
+ if (resolved !== child) {
144
+ contentChanged = true;
145
+ resolvedBoundaries.add(children.length);
146
+ }
109
147
  if (resolved) {
110
148
  children.push(resolved);
111
- contentChanged ||= resolved !== child;
112
- } else contentChanged = true;
149
+ if (resolved !== child) resolvedBoundaries.add(children.length);
150
+ }
151
+ });
152
+ const resolvedAttrs = resolvedNode.type.name === "sdt" ? withoutResolvedEnclosures(resolvedNode, () => true) ?? resolvedNode.attrs : resolvedNode.attrs;
153
+ if (!contentChanged && resolvedAttrs === resolvedNode.attrs) return resolvedNode;
154
+ if (contentChanged && resolvedNode.inlineContent) rejoinResolvedRuns({
155
+ children,
156
+ resolvedBoundaries,
157
+ paragraphScope,
158
+ context
113
159
  });
114
- if (!contentChanged) return resolvedNode;
115
- if (children.length === 0 && node.childCount > 0 && resolvedNode.type.name === "sdt") {
116
- context.replacementRanges.splice(replacementRangeStart, context.replacementRanges.length - replacementRangeStart, {
117
- from: position,
118
- to: position + node.nodeSize,
119
- newSize: 0
120
- });
121
- return null;
122
- }
123
160
  let resolvedContent = Fragment.fromArray(children);
124
161
  if (!resolvedNode.type.validContent(resolvedContent)) {
125
162
  const fitted = resolvedNode.type.createAndFill(resolvedNode.attrs, resolvedContent, resolvedNode.marks);
@@ -143,7 +180,7 @@ const resolveInlineContent = ({ node, position, context, inheritedParagraphScope
143
180
  from: position,
144
181
  to: position + resolvedNode.nodeSize
145
182
  });
146
- return rebuildNode(node, resolvedNode.attrs, resolvedContent, resolvedNode.marks);
183
+ return rebuildNode(node, resolvedAttrs, resolvedContent, resolvedNode.marks);
147
184
  };
148
185
  const coalesceReplacementRanges = (replacementRanges) => {
149
186
  const coalesced = [];
@@ -1,4 +1,4 @@
1
- import { convertBulletToUnicode } from "../../docx/bulletMarkers.js";
1
+ import { bulletMarkerFontName, convertBulletToUnicode } from "../../docx/bulletMarkers.js";
2
2
  import { resolveDocumentGridLinePitch } from "../../docx/documentGrid.js";
3
3
  import { getFontAlternate } from "../../fonts/fontAlternates.js";
4
4
  import { buildPageBreakRunDescendantIndex } from "../../internal/pageBreakRunDescendantIndex.js";
@@ -1053,7 +1053,7 @@ function resolveDeletedListMarker(previousListAttrs, listCounterState) {
1053
1053
  const marker = computeListMarker(previousListAttrs, listCounterState);
1054
1054
  if (marker) return marker;
1055
1055
  }
1056
- if (previousListAttrs.listMarker) return previousListAttrs.listIsBullet ? convertBulletToUnicode(previousListAttrs.listMarker) : previousListAttrs.listMarker;
1056
+ if (previousListAttrs.listMarker) return previousListAttrs.listIsBullet ? convertBulletToUnicode(previousListAttrs.listMarker, bulletMarkerFontName(previousListAttrs.listMarkerFormatting)) : previousListAttrs.listMarker;
1057
1057
  if (previousListAttrs.listIsBullet) return "•";
1058
1058
  return null;
1059
1059
  }
@@ -1219,7 +1219,7 @@ function convertParagraphAttrs(pmAttrs, { theme, fontAlternates, listCounterStre
1219
1219
  }
1220
1220
  const resolvedMarker = applyMarkerAllCaps(advanceVisibleListMarker(pmAttrs, listCounterStreams).marker, pmAttrs.listMarkerAllCaps);
1221
1221
  if (resolvedMarker !== null) attrs.listMarker = resolvedMarker;
1222
- else if (pmAttrs.listMarker) attrs.listMarker = pmAttrs.listIsBullet ? convertBulletToUnicode(pmAttrs.listMarker) : pmAttrs.listMarker;
1222
+ else if (pmAttrs.listMarker) attrs.listMarker = pmAttrs.listIsBullet ? convertBulletToUnicode(pmAttrs.listMarker, bulletMarkerFontName(pmAttrs.listMarkerFormatting)) : pmAttrs.listMarker;
1223
1223
  if (pmAttrs.listIsBullet !== void 0) attrs.listIsBullet = pmAttrs.listIsBullet;
1224
1224
  if (pmAttrs.listMarkerHidden) attrs.listMarkerHidden = true;
1225
1225
  if (attrs.listMarker !== void 0 || pmAttrs.listMarkerFormatting) {
@@ -1243,7 +1243,7 @@ function convertParagraphAttrs(pmAttrs, { theme, fontAlternates, listCounterStre
1243
1243
  const dtf = pmAttrs.defaultTextFormatting;
1244
1244
  if (dtf) {
1245
1245
  if (dtf.fontSize !== void 0) attrs.defaultFontSize = dtf.fontSize / 2;
1246
- if (attrs.listMarker !== void 0 && !attrs.listMarkerHidden && dtf.fontSizeCs !== void 0 && (dtf.fontSize === void 0 || dtf.fontSizeCs > dtf.fontSize)) attrs.listParagraphMarkFontSize = dtf.fontSizeCs / 2;
1246
+ if (attrs.listMarker !== void 0 && !attrs.listMarkerHidden && (attrs.bidi === true || dtf.rtl === true || dtf.cs === true) && dtf.fontSizeCs !== void 0 && (dtf.fontSize === void 0 || dtf.fontSizeCs > dtf.fontSize)) attrs.listParagraphMarkFontSize = dtf.fontSizeCs / 2;
1247
1247
  if (dtf.fontFamily) {
1248
1248
  const resolvedFamily = resolveWesternThemeFont(dtf.fontFamily, theme);
1249
1249
  if (resolvedFamily) {
@@ -1847,6 +1847,24 @@ function inlineEffectExtentPx(wrapType, extent) {
1847
1847
  };
1848
1848
  return resolved.top === 0 && resolved.bottom === 0 && resolved.left === 0 && resolved.right === 0 ? void 0 : resolved;
1849
1849
  }
1850
+ const GRADIENT_POSITION_SCALE = 1e5;
1851
+ /**
1852
+ * A linear `a:gradFill` with its stop colors resolved through the theme. A path
1853
+ * gradient (`a:path`) is not painted: nothing here places its focus.
1854
+ */
1855
+ function resolveLinearGradientFill(fill, theme) {
1856
+ const gradient = fill?.gradient;
1857
+ if (gradient === void 0 || gradient.type !== "linear" || gradient.stops.length === 0) return;
1858
+ const stops = gradient.stops.map((stop) => ({
1859
+ offset: Math.min(1, Math.max(0, stop.position / GRADIENT_POSITION_SCALE)),
1860
+ color: resolveColor(stop.color, theme)
1861
+ })).toSorted((left, right) => left.offset - right.offset);
1862
+ return {
1863
+ angle: gradient.angle ?? 0,
1864
+ scaled: gradient.scaled ?? false,
1865
+ stops
1866
+ };
1867
+ }
1850
1868
  /** Convert a textBox PM node to a TextBoxBlock. */
1851
1869
  function convertTextBoxNode(node, startPos, opts) {
1852
1870
  const attrs = expectTextBoxAttrs(node);
@@ -1879,6 +1897,10 @@ function convertTextBoxNode(node, startPos, opts) {
1879
1897
  if (attrs.autoFit !== void 0) textBox.autoFit = attrs.autoFit;
1880
1898
  if (attrs.textWrap !== void 0) textBox.textWrap = attrs.textWrap;
1881
1899
  if (attrs.fillColor !== void 0) textBox.fillColor = attrs.fillColor;
1900
+ else {
1901
+ const fillGradient = resolveLinearGradientFill(attrs.gradientFill, opts.theme);
1902
+ if (fillGradient !== void 0) textBox.fillGradient = fillGradient;
1903
+ }
1882
1904
  if (attrs.outlineWidth !== void 0) textBox.outlineWidth = attrs.outlineWidth;
1883
1905
  if (attrs.outlineColor !== void 0) textBox.outlineColor = attrs.outlineColor;
1884
1906
  if (attrs.outlineStyle !== void 0) textBox.outlineStyle = attrs.outlineStyle;
@@ -1,4 +1,4 @@
1
- import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, MathRun, Measure, MeasuredLine, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, NoteAreaLayout, NoteSeparatorKind, NoteSeparatorRule, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, stripFlowBlockPmAnchors, tableColumnsArePinned } from "./types.js";
1
+ import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, MathRun, Measure, MeasuredLine, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, NoteAreaLayout, NoteSeparatorKind, NoteSeparatorRule, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxGradientFill, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, stripFlowBlockPmAnchors, tableColumnsArePinned } from "./types.js";
2
2
  import { PageState, Paginator, PaginatorOptions, createPaginator } from "./paginator.js";
3
3
  import { KeepNextChain, calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
4
4
  import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
@@ -42,4 +42,4 @@ declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], option
42
42
  */
43
43
  declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;
44
44
  //#endregion
45
- export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, MathRun, Measure, MeasuredLine, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, NoteAreaLayout, NoteSeparatorKind, NoteSeparatorRule, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, SectionPageNumbering, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, resolveTableWidthPx, scheduleSectionBreak, stripFlowBlockPmAnchors, tableColumnsArePinned };
45
+ export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, MathRun, Measure, MeasuredLine, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, NoteAreaLayout, NoteSeparatorKind, NoteSeparatorRule, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, SectionPageNumbering, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxGradientFill, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, resolveTableWidthPx, scheduleSectionBreak, stripFlowBlockPmAnchors, tableColumnsArePinned };
@@ -13,7 +13,7 @@ import { assertExhaustiveFlowBlock, findPageIndexContainingPmPos } from "./pmPag
13
13
  import { INITIAL_RENDERED_BREAK_STATE, PAGE_ADVANCE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary } from "./renderedBreakReconciliation.js";
14
14
  import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, normalizeSectionBreakType, physicalColumnRegionIsShared, scheduleSectionBreak } from "./section-breaks.js";
15
15
  import { applySectionVerticalAlignment } from "./sectionVerticalAlignment.js";
16
- import { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak } from "./tableRowBreak.js";
16
+ import { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak, tableKeepNextOpeningHeight } from "./tableRowBreak.js";
17
17
  import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
18
18
  import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, stripFlowBlockPmAnchors, tableColumnsArePinned } from "./types.js";
19
19
  import { panic } from "better-result";
@@ -194,6 +194,7 @@ function layoutDocumentPass(blocks, measures, options) {
194
194
  ...options.mirrorMargins === true ? { mirrorMargins: true } : {},
195
195
  ...options.firstPageMargins !== void 0 ? { firstPageMargins: options.firstPageMargins } : {},
196
196
  ...options.sectionEvenPageMargins !== void 0 ? { sectionEvenPageMargins: options.sectionEvenPageMargins } : {},
197
+ ...options.sectionFirstPageMargins !== void 0 ? { sectionFirstPageMargins: options.sectionFirstPageMargins } : {},
197
198
  columns: initialConfig.columns ?? DEFAULT_COLUMNS,
198
199
  pageNumbering: initialConfig.pageNumbering,
199
200
  ...options.footnoteReservedHeights !== void 0 ? { footnoteReservedHeights: options.footnoteReservedHeights } : {},
@@ -234,7 +235,7 @@ function layoutDocumentPass(blocks, measures, options) {
234
235
  renderedBreakState = breakDecision.state;
235
236
  const chain = keepNextChains.get(i);
236
237
  if (chain && !midChainIndices.has(i)) {
237
- const chainHeight = calculateChainHeight(chain, blocks, measures, paginator.getCurrentState().trailingSpacing);
238
+ const chainHeight = calculateChainHeight(chain, blocks, measures, paginator.getCurrentState().trailingSpacing, tableKeepNextOpeningHeight);
238
239
  const pageBeforeChainLayout = paginator.getCurrentState().page.number;
239
240
  paginator.ensureFits(chainHeight);
240
241
  if (paginator.getCurrentState().page.number > pageBeforeChainLayout) renderedBreakState = recordReflowBoundary(renderedBreakState, true);
@@ -1070,6 +1071,32 @@ function startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex) {
1070
1071
  paginator.retargetCurrentBlankPage();
1071
1072
  }
1072
1073
  /**
1074
+ * Whether a section opening on `target` would give two consecutive sheets page
1075
+ * numbers that are both odd or both even under `w:evenAndOddHeaders` (§17.10.1). Odd and
1076
+ * even page numbers alternate between right- and left-hand sheets, so a
1077
+ * `w:pgNumType w:start` restart that repeats whether the previous sheet's number is odd or even
1078
+ * needs a blank sheet between them.
1079
+ */
1080
+ function sectionStartRepeatsOddEven(paginator, target) {
1081
+ if (target.page.headerFooterRefs?.evenAndOddHeaders !== true) return false;
1082
+ const previous = paginator.states.at(-2)?.page;
1083
+ if (previous === void 0 || previous.number !== target.page.number - 1) return false;
1084
+ return previous.logicalNumber % 2 === target.page.logicalNumber % 2;
1085
+ }
1086
+ /**
1087
+ * Keep the blank sheet the section break opened as a filler with no page
1088
+ * furniture, and open the section again on the following sheet so its first
1089
+ * page keeps the restarted page number.
1090
+ */
1091
+ function insertOddEvenFillerPage(paginator, filler, sectionIndex, sectionConfig) {
1092
+ filler.page.headerFooterRefs = {};
1093
+ paginator.startSection({
1094
+ sectionIndex,
1095
+ pageNumbering: sectionConfig.pageNumbering
1096
+ });
1097
+ paginator.forcePageBreak();
1098
+ }
1099
+ /**
1073
1100
  * Handle a section break block.
1074
1101
  * @param block - The section break block (current section's properties)
1075
1102
  * @param paginator - The paginator instance
@@ -1078,14 +1105,16 @@ function startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex) {
1078
1105
  */
1079
1106
  function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionType, nextSectionIndex) {
1080
1107
  switch (nextSectionType) {
1081
- case "nextPage":
1108
+ case "nextPage": {
1082
1109
  paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
1083
1110
  if (nextSectionIndex !== void 0) paginator.startSection({
1084
1111
  sectionIndex: nextSectionIndex,
1085
1112
  pageNumbering: nextSectionConfig.pageNumbering
1086
1113
  });
1087
- paginator.forcePageBreak({ coalesceBlankPage: true });
1114
+ const target = paginator.forcePageBreak({ coalesceBlankPage: true });
1115
+ if (nextSectionIndex !== void 0 && sectionStartRepeatsOddEven(paginator, target)) insertOddEvenFillerPage(paginator, target, nextSectionIndex, nextSectionConfig);
1088
1116
  break;
1117
+ }
1089
1118
  case "evenPage":
1090
1119
  if (paginator.forcePageBreak({ coalesceBlankPage: true }).page.number % 2 !== 0) paginator.forcePageBreak();
1091
1120
  paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
@@ -1,4 +1,4 @@
1
- import { FlowBlock, Measure } from "./types.js";
1
+ import { FlowBlock, Measure, TableBlock, TableMeasure } from "./types.js";
2
2
  //#region src/layout-engine/keep-together.d.ts
3
3
  /**
4
4
  * A chain of paragraphs that Word keeps with following content. This includes
@@ -36,12 +36,15 @@ declare function computeKeepNextChains(blocks: FlowBlock[]): Map<number, KeepNex
36
36
  * reservation continues through it; otherwise the chain stops after the
37
37
  * opening, because a later split cannot separate it from its predecessor.
38
38
  *
39
+ * A table anchor contributes `tableOpeningHeight`: the part of the table
40
+ * that must start on the same page as the paragraph before it.
41
+ *
39
42
  * A paragraph measure's `totalHeight` already includes its own spacing before
40
43
  * and after. The chain accounts for spacing separately (collapsing each gap to
41
44
  * the larger side, as the paginator does), so members contribute only their
42
45
  * line advances; otherwise every gap would be reserved twice.
43
46
  */
44
- declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[], incomingSpacing?: number): number;
47
+ declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[], incomingSpacing?: number, tableOpeningHeight?: (block: TableBlock, measure: TableMeasure) => number): number;
45
48
  /**
46
49
  * Get the set of indices that are mid-chain (not chain starters).
47
50
  * These should skip the keepNext check since their chain starter already decided.
@@ -102,12 +102,15 @@ function minimumOpeningLineCount(block, measure) {
102
102
  * reservation continues through it; otherwise the chain stops after the
103
103
  * opening, because a later split cannot separate it from its predecessor.
104
104
  *
105
+ * A table anchor contributes `tableOpeningHeight`: the part of the table
106
+ * that must start on the same page as the paragraph before it.
107
+ *
105
108
  * A paragraph measure's `totalHeight` already includes its own spacing before
106
109
  * and after. The chain accounts for spacing separately (collapsing each gap to
107
110
  * the larger side, as the paginator does), so members contribute only their
108
111
  * line advances; otherwise every gap would be reserved twice.
109
112
  */
110
- function calculateChainHeight(chain, blocks, measures, incomingSpacing = 0) {
113
+ function calculateChainHeight(chain, blocks, measures, incomingSpacing = 0, tableOpeningHeight) {
111
114
  const firstMemberIndex = chain.memberIndices.at(0);
112
115
  if (firstMemberIndex === void 0) return 0;
113
116
  const firstBlock = blocks[firstMemberIndex];
@@ -125,6 +128,7 @@ function calculateChainHeight(chain, blocks, measures, incomingSpacing = 0) {
125
128
  if (successorIndex === void 0) continue;
126
129
  const successorBlock = blocks[successorIndex];
127
130
  const successorMeasure = measures[successorIndex];
131
+ if (tableOpeningHeight !== void 0 && successorBlock?.kind === "table" && successorMeasure?.kind === "table" && successorBlock.floating === void 0) return totalHeight + trailingSpacing + tableOpeningHeight(successorBlock, successorMeasure);
128
132
  if (successorBlock?.kind !== "paragraph" || successorMeasure?.kind !== "paragraph") return totalHeight;
129
133
  totalHeight += collapseParagraphSpacing({
130
134
  before: successorBlock.attrs?.spacing?.before ?? 0,
@@ -1,6 +1,8 @@
1
1
  import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
2
2
  import { SCRIPT_CLASS, hasComplexScript, hasEastAsiaSlotText, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
3
3
  import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./complexScriptFormatting.js";
4
+ import "./measureHelpers.js";
5
+ import { SMALL_CAPS_SCALE, smallCapsMask, smallCapsSegments } from "./smallCapsCasing.js";
4
6
  //#region src/layout-engine/measure/advanceComposition.ts
5
7
  /**
6
8
  * The arithmetic that turns raw glyph advances into measured widths.
@@ -50,6 +52,38 @@ const glyphAdvanceStyle = (style) => {
50
52
  };
51
53
  const needsPerScriptFonts = (style, text) => style.eastAsiaFontFamily !== void 0 && hasEastAsiaSlotText(text, style.eastAsiaHint) || hasComplexScriptFormatting(style) && hasComplexScript(text);
52
54
  /**
55
+ * Width of a `w:smallCaps` string: its lowercase letters (and the whitespace
56
+ * that follows them — see smallCapsCasing.ts) draw at `SMALL_CAPS_SCALE` of
57
+ * the style's size, everything else at full size. A small segment is measured
58
+ * at that literal, smaller size — not the full-size advance scaled by
59
+ * arithmetic — mirroring {@link canvasMeasureTextWidth}'s
60
+ * `measureSmallCapsWidth` so the two backends agree; each segment still
61
+ * resolves its own per-script font, the same as the plain (non-small-caps)
62
+ * path below.
63
+ */
64
+ const smallCapsGlyphWidth = (text, style, advanceOf) => {
65
+ const segmentStyle = { ...style };
66
+ delete segmentStyle.fontVariant;
67
+ delete segmentStyle.letterSpacing;
68
+ delete segmentStyle.horizontalScale;
69
+ const smallSegmentStyle = {
70
+ ...segmentStyle,
71
+ fontSize: (segmentStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
72
+ };
73
+ let total = 0;
74
+ for (const segment of smallCapsSegments(text)) {
75
+ const activeStyle = segment.small ? smallSegmentStyle : segmentStyle;
76
+ total += needsPerScriptFonts(activeStyle, segment.text) ? segmentByScript(segment.text, activeStyle.eastAsiaHint).reduce((sum, scriptSegment) => sum + advanceOf(scriptSegment.text, glyphAdvanceStyle(scriptStyle(activeStyle, scriptSegment.script))), 0) : advanceOf(segment.text, activeStyle);
77
+ }
78
+ return total;
79
+ };
80
+ /** The un-spaced, un-scaled glyph width `composeTextWidth` spaces and scales. */
81
+ const glyphWidthOf = ({ source, style, transformed, letterSpacing, advanceOf }) => {
82
+ if (!source.forceComplexScript && style.fontVariant === "small-caps") return smallCapsGlyphWidth(transformed, style, advanceOf);
83
+ if (!source.forceComplexScript && !letterSpacing && needsPerScriptFonts(style, transformed)) return segmentByScript(transformed, style.eastAsiaHint).reduce((sum, segment) => sum + advanceOf(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script))), 0);
84
+ return advanceOf(transformed, style);
85
+ };
86
+ /**
53
87
  * Width of a string: the advance width used for line breaking, never the
54
88
  * painted (ink) extent, which includes overhang a break decision must ignore.
55
89
  */
@@ -59,7 +93,13 @@ const composeTextWidth = ({ text, style: source, advanceOf }) => {
59
93
  const transformed = applyMeasurementTextTransform(text, style);
60
94
  const letterSpacing = style.letterSpacing ?? 0;
61
95
  const horizontalScale = getHorizontalScaleFactor(style.horizontalScale);
62
- const glyphWidth = !source.forceComplexScript && !letterSpacing && needsPerScriptFonts(style, transformed) ? segmentByScript(transformed, style.eastAsiaHint).reduce((sum, segment) => sum + advanceOf(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script))), 0) : advanceOf(transformed, style);
96
+ const glyphWidth = glyphWidthOf({
97
+ source,
98
+ style,
99
+ transformed,
100
+ letterSpacing,
101
+ advanceOf
102
+ });
63
103
  const codePoints = countCodePoints(transformed);
64
104
  return (letterSpacing && codePoints > 1 ? glyphWidth + letterSpacing * (codePoints - 1) : glyphWidth) * horizontalScale;
65
105
  };
@@ -92,10 +132,16 @@ const composeRunMeasurement = ({ text, style: source, metrics, advanceOf, advanc
92
132
  perScript
93
133
  });
94
134
  let codePointIndex = 0;
135
+ const smallCapsAt = style.fontVariant === "small-caps" ? smallCapsMask(text) : void 0;
95
136
  for (const char of text) {
96
137
  const cp = char.codePointAt(0);
97
- const charStyle = perScript ? scriptStyle(style, scriptClassOf(cp, style.eastAsiaHint)) : style;
98
- let charWidth = shaped.get(codePointIndex) ?? advanceOf(applyMeasurementTextTransform(char, style), charStyle);
138
+ const isSmallCap = smallCapsAt?.[codePointIndex] ?? false;
139
+ const scriptedStyle = perScript ? scriptStyle(style, scriptClassOf(cp, style.eastAsiaHint)) : style;
140
+ const charStyle = isSmallCap ? {
141
+ ...scriptedStyle,
142
+ fontSize: (scriptedStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
143
+ } : scriptedStyle;
144
+ let charWidth = shaped.get(codePointIndex) ?? advanceOf(isSmallCap ? char.toLocaleUpperCase() : applyMeasurementTextTransform(char, style), charStyle);
99
145
  if (letterSpacing && offset + char.length < text.length) charWidth += letterSpacing;
100
146
  charWidth *= scale;
101
147
  charWidths.push(charWidth);
@@ -1,6 +1,7 @@
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";
@@ -97,6 +98,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
97
98
  }
98
99
  }
99
100
  const cellGrid = buildTableCellGrid(tableBlock.rows, columnWidths.length);
101
+ const spacingRows = resolveTableSpacing(tableBlock).rows;
100
102
  const cellSpacing = tableBlock.cellSpacing;
101
103
  const bordersSeparated = (cellSpacing ?? 0) > 0;
102
104
  const rowCount = tableBlock.rows.length;
@@ -104,7 +106,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
104
106
  const rows = tableBlock.rows.map((row, rowIdx) => {
105
107
  let columnIndex = getFirstAvailableColumn(cellGrid, rowIdx, row.gridBefore ?? 0);
106
108
  return {
107
- cells: row.cells.map((cell) => {
109
+ cells: row.cells.map((cell, cellIdx) => {
108
110
  const colSpan = cell.colSpan ?? 1;
109
111
  let cellWidth = 0;
110
112
  for (let c = 0; c < colSpan && columnIndex + c < columnWidths.length; c++) cellWidth += columnWidths[columnIndex + c] ?? 0;
@@ -121,7 +123,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
121
123
  width: cellWidth,
122
124
  height: 0
123
125
  };
124
- if (!keepSingleLine) wrapTableCellParagraphsAroundFloats(cell, cellMeasure, cellContentWidth, fieldValues);
126
+ if (!keepSingleLine) wrapTableCellParagraphsAroundFloats(spacingRows[rowIdx]?.cells[cellIdx] ?? cell, cellMeasure, cellContentWidth, fieldValues);
125
127
  if (cell.colSpan !== void 0) cellMeasure.colSpan = cell.colSpan;
126
128
  if (cell.rowSpan !== void 0) cellMeasure.rowSpan = cell.rowSpan;
127
129
  return cellMeasure;
@@ -141,7 +143,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
141
143
  }));
142
144
  continue;
143
145
  }
144
- const sourceRowCells = tableBlock.rows[rowIdx]?.cells;
146
+ const sourceRowCells = spacingRows[rowIdx]?.cells;
145
147
  let maxCellHeightWithBorders = 0;
146
148
  let maxCellInsets = 0;
147
149
  for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {