@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
@@ -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`);
@@ -3,7 +3,6 @@ import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
3
3
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
4
4
  import { hasCjk, hasComplexScript, hasEastAsiaSlotText } from "../../utils/scriptSegments.js";
5
5
  import { splitTrailingToken } from "../../utils/trailingText.js";
6
- import { JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO, JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO, supportsJustifiedFinalLineContraction } from "../justifiedLineFit.js";
7
6
  import { measuredLineAdvance } from "../lineFlow.js";
8
7
  import { isFloatingImageRun } from "../types.js";
9
8
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
@@ -29,11 +28,10 @@ const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
29
28
  const WIDTH_TOLERANCE = .5;
30
29
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
31
30
  const JUSTIFY_SPACE_CONTRACTION_RATIO = .25;
32
- const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
33
- const JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO = .022;
31
+ const JUSTIFY_STRETCH_PER_SHRINK = 1.7;
32
+ const JUSTIFY_ALWAYS_SHRINK_STRETCH = 1.5;
34
33
  const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
35
34
  const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
36
- const DEFAULT_LIST_HANGING_INDENT_PX = 24;
37
35
  const ALL_CAPS_RATIO_THRESHOLD = .8;
38
36
  /**
39
37
  * Find the longest prefix of `text` that fits within `maxWidth` pixels.
@@ -371,22 +369,6 @@ function uppercaseLetterRatio(text) {
371
369
  }
372
370
  function resolveJustifyFitStrategy(block, isFirstLine, profile) {
373
371
  if (block.attrs?.justificationCompatibility?.type === "legacy") return { type: "rounding" };
374
- if (isShallowFullHangingListContinuation(block, isFirstLine)) return { type: "rounding" };
375
- if (block.attrs?.listMarker !== void 0) {
376
- const hanging = block.attrs.indent?.hanging ?? 0;
377
- if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ? {
378
- type: "space",
379
- ratio: JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO
380
- } : {
381
- type: "width",
382
- ratio: JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO
383
- };
384
- return {
385
- type: "space",
386
- ratio: JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO,
387
- maxWidthRatio: JUSTIFY_SHRINK_TOLERANCE_RATIO
388
- };
389
- }
390
372
  const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
391
373
  if (isFirstLine && profile.hasTabRuns && (block.attrs?.indent?.hanging ?? 0) > 0) return {
392
374
  type: "width",
@@ -409,19 +391,6 @@ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
409
391
  ratio: JUSTIFY_SPACE_CONTRACTION_RATIO
410
392
  };
411
393
  }
412
- function resolveFinalLineJustifyFitStrategy(block, profile) {
413
- if (supportsJustifiedFinalLineContraction(block)) return {
414
- type: "space",
415
- ratio: JUSTIFIED_FINAL_LINE_SPACE_CONTRACTION_RATIO,
416
- maxWidthRatio: JUSTIFIED_FINAL_LINE_MAX_SHRINK_RATIO
417
- };
418
- return resolveJustifyFitStrategy(block, false, profile);
419
- }
420
- function resolveCandidateJustifyFitStrategy({ isFirstLine, isFinalCandidate, firstLineStrategy, continuationStrategy, finalLineStrategy }) {
421
- if (isFirstLine) return firstLineStrategy;
422
- if (isFinalCandidate) return finalLineStrategy;
423
- return continuationStrategy;
424
- }
425
394
  const isIgnorableFinalTailRun = (run) => run.kind === "renderedPageBreak" || isTextRun(run) && run.text.length === 0;
426
395
  function isFinalTextCandidate(block, runIndex, nextBreak) {
427
396
  const currentRun = block.runs[runIndex];
@@ -433,27 +402,16 @@ function isFinalTextCandidate(block, runIndex, nextBreak) {
433
402
  }
434
403
  return true;
435
404
  }
436
- function resolveTextCandidateFit({ block, line, isFirstLine, isFinalCandidate, candidateWidth, candidateSpaceWidth, fallbackTolerancePx, continuationStrategy, finalStrategy }) {
405
+ /**
406
+ * Fit of a text candidate on a justified line: the paragraph's final word may
407
+ * be admitted by shrinking spaces, which then needs a paint plan.
408
+ */
409
+ function resolveTextCandidateFit(isFinalCandidate, candidateWidth, availableWidth, tolerancePx) {
437
410
  if (!isFinalCandidate) return {
438
411
  type: "ordinary",
439
- tolerancePx: fallbackTolerancePx
440
- };
441
- if (!supportsJustifiedFinalLineContraction(block)) return admitFinalCandidate(candidateWidth, line.availableWidth, fallbackTolerancePx);
442
- const ordinaryTolerancePx = isFirstLine ? fallbackTolerancePx : justifyFitTolerance(line, continuationStrategy, candidateSpaceWidth);
443
- if (candidateWidth <= line.availableWidth + ordinaryTolerancePx) return admitFinalCandidate(candidateWidth, line.availableWidth, ordinaryTolerancePx);
444
- const finalTolerancePx = justifyFitTolerance(line, finalStrategy, candidateSpaceWidth);
445
- if (candidateWidth > line.availableWidth + finalTolerancePx) return {
446
- type: "final-contraction-rejected",
447
- tolerancePx: finalTolerancePx
448
- };
449
- return {
450
- type: "final-contraction-admitted",
451
- tolerancePx: finalTolerancePx,
452
- paint: {
453
- type: "space-contraction",
454
- contractionPx: candidateWidth - line.availableWidth
455
- }
412
+ tolerancePx
456
413
  };
414
+ return admitFinalCandidate(candidateWidth, availableWidth, tolerancePx);
457
415
  }
458
416
  /**
459
417
  * A paragraph's last line is not stretched, so a final word admitted by space
@@ -479,18 +437,35 @@ function compressibleSpaceWidth(text, style) {
479
437
  const count = countCompressibleSpaces(text);
480
438
  return count === 0 ? 0 : count * measureTextWidth(" ", style);
481
439
  }
482
- function justifyFitTolerance(line, strategy, candidateSpaceWidth) {
440
+ function justifyFitTolerance(line, strategy, candidateSpaceWidth, candidate) {
483
441
  if (strategy.type === "rounding") return WIDTH_TOLERANCE;
484
442
  if (strategy.type === "width") return Math.max(WIDTH_TOLERANCE, line.availableWidth * strategy.ratio);
485
- const spaceTolerance = (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio;
486
- const boundedTolerance = strategy.maxWidthRatio === void 0 ? spaceTolerance : Math.min(spaceTolerance, line.availableWidth * strategy.maxWidthRatio);
487
- return Math.max(WIDTH_TOLERANCE, boundedTolerance);
443
+ if (candidate !== void 0 && !prefersSpaceShrink(line, candidate)) return WIDTH_TOLERANCE;
444
+ return Math.max(WIDTH_TOLERANCE, (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio);
488
445
  }
489
- function isShallowFullHangingListContinuation(block, isFirstLine) {
490
- if (isFirstLine || block.attrs?.listMarker === void 0) return false;
491
- const hanging = block.attrs.indent?.hanging ?? 0;
492
- const left = block.attrs.indent?.left ?? 0;
493
- return hanging > 0 && hanging < DEFAULT_LIST_HANGING_INDENT_PX && Math.abs(left - hanging) <= WIDTH_TOLERANCE;
446
+ /**
447
+ * A justified line whose next word overflows the measure has two outcomes:
448
+ * keep the word and shrink the line's spaces, or wrap it and stretch the
449
+ * spaces left on the shorter line. Shrinking wins only when the stretch it
450
+ * avoids is large compared with the shrink it costs: the shrink factor may
451
+ * not exceed `1 + (stretch - 1) / JUSTIFY_STRETCH_PER_SHRINK`, and a stretch
452
+ * beyond `JUSTIFY_ALWAYS_SHRINK_STRETCH` always prefers shrinking. The space
453
+ * before the wrapped word hangs at the line end, so it does not stretch; a
454
+ * line left with nothing to stretch defers to the shrink budget alone.
455
+ */
456
+ function prefersSpaceShrink(line, candidate) {
457
+ const overflow = candidate.width - line.availableWidth;
458
+ if (overflow <= WIDTH_TOLERANCE) return true;
459
+ const shrinkableSpaceWidth = line.regularSpaceWidth + candidate.innerSpaceWidth;
460
+ if (shrinkableSpaceWidth <= overflow) return false;
461
+ const hangingSpaceWidth = Math.min(line.trailingWhitespaceWidth, line.regularSpaceWidth);
462
+ const stretchableSpaceWidth = line.regularSpaceWidth - hangingSpaceWidth;
463
+ if (stretchableSpaceWidth <= 0) return true;
464
+ const visibleWidth = line.width - line.trailingWhitespaceWidth;
465
+ const stretch = 1 + Math.max(0, line.availableWidth - visibleWidth) / stretchableSpaceWidth;
466
+ if (stretch > JUSTIFY_ALWAYS_SHRINK_STRETCH) return true;
467
+ const shrink = shrinkableSpaceWidth / (shrinkableSpaceWidth - overflow);
468
+ return 1 + (stretch - 1) / JUSTIFY_STRETCH_PER_SHRINK >= shrink;
494
469
  }
495
470
  function trimTrailingSpacesAndTabs(text) {
496
471
  let end = text.length;
@@ -770,7 +745,6 @@ function measureParagraph(block, maxWidth, options) {
770
745
  };
771
746
  const firstLineJustifyFitStrategy = resolveJustifyFitStrategy(block, true, justificationProfile);
772
747
  const continuationJustifyFitStrategy = resolveJustifyFitStrategy(block, false, justificationProfile);
773
- const finalLineJustifyFitStrategy = resolveFinalLineJustifyFitStrategy(block, justificationProfile);
774
748
  const floatingZones = options?.floatingZones;
775
749
  const paragraphYOffset = options?.paragraphYOffset ?? 0;
776
750
  const indent = attrs?.indent;
@@ -1112,7 +1086,10 @@ function measureParagraph(block, maxWidth, options) {
1112
1086
  updateMaxFont(style);
1113
1087
  const fieldWidth = measureTextWidth(fallback, style);
1114
1088
  const fieldSpaceWidth = compressibleSpaceWidth(fallback, style);
1115
- const fieldTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, lines.length === 0 ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, fieldSpaceWidth) : WIDTH_TOLERANCE;
1089
+ const fieldTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, lines.length === 0 ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, fieldSpaceWidth, {
1090
+ width: currentLine.width + fieldWidth,
1091
+ innerSpaceWidth: fieldSpaceWidth
1092
+ }) : WIDTH_TOLERANCE;
1116
1093
  if (currentLine.width > 0 && currentLine.width + fieldWidth > currentLine.availableWidth + fieldTolerance) {
1117
1094
  startNewLine(runIndex, 0);
1118
1095
  updateMaxFont(style);
@@ -1181,14 +1158,10 @@ function measureParagraph(block, maxWidth, options) {
1181
1158
  const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, effectiveLineBreakPolicy.hangingPunctuation);
1182
1159
  const isFirstLine = lines.length === 0;
1183
1160
  const regularSpaceWidth = compressibleSpaceWidth(measuredWord, style);
1184
- const justifyFitStrategy = resolveCandidateJustifyFitStrategy({
1185
- isFirstLine,
1186
- isFinalCandidate: isFinalTextCandidate(block, runIndex, nextBreak),
1187
- firstLineStrategy: firstLineJustifyFitStrategy,
1188
- continuationStrategy: continuationJustifyFitStrategy,
1189
- finalLineStrategy: finalLineJustifyFitStrategy
1190
- });
1191
- const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, justifyFitStrategy, regularSpaceWidth) : WIDTH_TOLERANCE;
1161
+ const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, isFirstLine ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, regularSpaceWidth, {
1162
+ width: currentLine.width + leadingLetterSpacing + wordWidth,
1163
+ innerSpaceWidth: 0
1164
+ }) : WIDTH_TOLERANCE;
1192
1165
  const automaticHyphenation = effectiveLineBreakPolicy.automaticHyphenation;
1193
1166
  const mayHyphenateLine = automaticHyphenation.type === "enabled" && (automaticHyphenation.consecutiveLineLimit === 0 || consecutiveHyphenatedLines < automaticHyphenation.consecutiveLineLimit) && exceedsHyphenationZone(currentLine, automaticHyphenation.hyphenationZoneTwips);
1194
1167
  const isRunTail = nextBreak === text.length;
@@ -1305,17 +1278,7 @@ function measureParagraph(block, maxWidth, options) {
1305
1278
  const rawGlueWidth = isRunTail && wordTail !== void 0 && (!isBreakBetween(wordTail, followingTextLeads[runIndex]) || protectedGlueWidth > 0) ? Math.max(trailingGlueWidths[runIndex] ?? 0, protectedGlueWidth) : 0;
1306
1279
  const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + widthTolerance ? rawGlueWidth : 0;
1307
1280
  const hangingAllowance = glueWidth === 0 ? hangingPunctuationWidth : 0;
1308
- const candidateFit = isJustifiedParagraph ? resolveTextCandidateFit({
1309
- block,
1310
- line: currentLine,
1311
- isFirstLine,
1312
- isFinalCandidate: isFinalTextCandidate(block, runIndex, nextBreak),
1313
- candidateWidth: currentLine.width + leadingLetterSpacing + wordWidth + glueWidth - hangingAllowance,
1314
- candidateSpaceWidth: regularSpaceWidth,
1315
- fallbackTolerancePx: widthTolerance,
1316
- continuationStrategy: continuationJustifyFitStrategy,
1317
- finalStrategy: finalLineJustifyFitStrategy
1318
- }) : void 0;
1281
+ const candidateFit = isJustifiedParagraph ? resolveTextCandidateFit(isFinalTextCandidate(block, runIndex, nextBreak), currentLine.width + leadingLetterSpacing + wordWidth + glueWidth - hangingAllowance, currentLine.availableWidth, widthTolerance) : void 0;
1319
1282
  const finalWrapTolerance = candidateFit?.tolerancePx ?? widthTolerance;
1320
1283
  if (wordWidth > 0 && currentLine.width > 0 && currentLine.width + leadingLetterSpacing + wordWidth + glueWidth > currentLine.availableWidth + finalWrapTolerance + hangingAllowance) {
1321
1284
  startNewLine(runIndex, charIndex);
@@ -0,0 +1,56 @@
1
+ //#region src/layout-engine/measure/smallCapsCasing.d.ts
2
+ /**
3
+ * Casing rules for synthesized `w:smallCaps` glyphs.
4
+ *
5
+ * A face that carries no `smcp`/`c2sc` substitution has no true small-capital
6
+ * glyph, so it is faked: a lowercase letter is drawn as its uppercase form at
7
+ * a smaller point size, on the run's own baseline, at a fixed fraction of the
8
+ * run's declared size — 0.8, an exact ratio observed to repeat across mixed
9
+ * two-size runs at every point size and never round to a half-point `w:sz`
10
+ * step. That is not a canvas or DOM backend's own `font-variant: small-caps`,
11
+ * which synthesizes at its own, uncontrollable, narrower ratio (Blink and
12
+ * WebKit use 0.7).
13
+ *
14
+ * Pure: no canvas, no DOM. Shared by every measurement and paint backend so a
15
+ * run's advances and its glyphs agree on which characters shrink.
16
+ */
17
+ /** Fraction of the run's declared size a synthesized small capital draws at. */
18
+ declare const SMALL_CAPS_SCALE = 0.8;
19
+ /**
20
+ * Whether `char` is a cased letter in its lowercase form — the class
21
+ * `w:smallCaps` draws as a shrunken, uppercased capital. Digits, punctuation,
22
+ * symbols and already-uppercase letters keep the run's full size; combining
23
+ * `char`'s upper and lower forms this way (rather than comparing to a
24
+ * separate "is uppercase" test) is what correctly leaves caseless scripts
25
+ * (CJK, most punctuation) at full size too, since neither form differs from
26
+ * the character itself there.
27
+ */
28
+ declare function isSmallCapsLetter(char: string): boolean;
29
+ /**
30
+ * Per-code-point small-caps classification for `text`, one entry per code
31
+ * point (iterated the same way `[...text]`/`for...of` does, so it lines up
32
+ * with any other code-point walk of the same string).
33
+ *
34
+ * Whitespace carries no case of its own; a run's space draws at the size of
35
+ * the character immediately before it — the trailing space after a
36
+ * synthesized word is itself synthesized — so a whitespace code point here
37
+ * takes the previous code point's class, and only falls back to full size
38
+ * when it opens the string (nothing to inherit from).
39
+ */
40
+ declare function smallCapsMask(text: string): boolean[];
41
+ /** A maximal run of `text` drawn at one size: `small` selects which. */
42
+ type SmallCapsSegment = {
43
+ readonly text: string;
44
+ readonly small: boolean;
45
+ };
46
+ /**
47
+ * `text` split into maximal same-size segments for painting, each already
48
+ * uppercased where `small` — the shrunken capital a `smcp`-less face
49
+ * substitutes for the source lowercase letter. A run with no lowercase letter
50
+ * at all (already-uppercase headings, digits, punctuation) comes back as one
51
+ * `small: false` segment, so a caller can skip the extra painting work its
52
+ * mixed-size runs need.
53
+ */
54
+ declare function smallCapsSegments(text: string): SmallCapsSegment[];
55
+ //#endregion
56
+ export { SMALL_CAPS_SCALE, SmallCapsSegment, isSmallCapsLetter, smallCapsMask, smallCapsSegments };
@@ -0,0 +1,88 @@
1
+ //#region src/layout-engine/measure/smallCapsCasing.ts
2
+ /**
3
+ * Casing rules for synthesized `w:smallCaps` glyphs.
4
+ *
5
+ * A face that carries no `smcp`/`c2sc` substitution has no true small-capital
6
+ * glyph, so it is faked: a lowercase letter is drawn as its uppercase form at
7
+ * a smaller point size, on the run's own baseline, at a fixed fraction of the
8
+ * run's declared size — 0.8, an exact ratio observed to repeat across mixed
9
+ * two-size runs at every point size and never round to a half-point `w:sz`
10
+ * step. That is not a canvas or DOM backend's own `font-variant: small-caps`,
11
+ * which synthesizes at its own, uncontrollable, narrower ratio (Blink and
12
+ * WebKit use 0.7).
13
+ *
14
+ * Pure: no canvas, no DOM. Shared by every measurement and paint backend so a
15
+ * run's advances and its glyphs agree on which characters shrink.
16
+ */
17
+ /** Fraction of the run's declared size a synthesized small capital draws at. */
18
+ const SMALL_CAPS_SCALE = .8;
19
+ /** Whitespace never gets its own small-caps class; see `smallCapsMask`. */
20
+ const WHITESPACE_PATTERN = /\s/u;
21
+ /**
22
+ * Whether `char` is a cased letter in its lowercase form — the class
23
+ * `w:smallCaps` draws as a shrunken, uppercased capital. Digits, punctuation,
24
+ * symbols and already-uppercase letters keep the run's full size; combining
25
+ * `char`'s upper and lower forms this way (rather than comparing to a
26
+ * separate "is uppercase" test) is what correctly leaves caseless scripts
27
+ * (CJK, most punctuation) at full size too, since neither form differs from
28
+ * the character itself there.
29
+ */
30
+ function isSmallCapsLetter(char) {
31
+ return char.toLocaleUpperCase() !== char;
32
+ }
33
+ /**
34
+ * Per-code-point small-caps classification for `text`, one entry per code
35
+ * point (iterated the same way `[...text]`/`for...of` does, so it lines up
36
+ * with any other code-point walk of the same string).
37
+ *
38
+ * Whitespace carries no case of its own; a run's space draws at the size of
39
+ * the character immediately before it — the trailing space after a
40
+ * synthesized word is itself synthesized — so a whitespace code point here
41
+ * takes the previous code point's class, and only falls back to full size
42
+ * when it opens the string (nothing to inherit from).
43
+ */
44
+ function smallCapsMask(text) {
45
+ const mask = [];
46
+ let previous = false;
47
+ for (const char of text) {
48
+ const small = WHITESPACE_PATTERN.test(char) ? previous : isSmallCapsLetter(char);
49
+ mask.push(small);
50
+ previous = small;
51
+ }
52
+ return mask;
53
+ }
54
+ /**
55
+ * `text` split into maximal same-size segments for painting, each already
56
+ * uppercased where `small` — the shrunken capital a `smcp`-less face
57
+ * substitutes for the source lowercase letter. A run with no lowercase letter
58
+ * at all (already-uppercase headings, digits, punctuation) comes back as one
59
+ * `small: false` segment, so a caller can skip the extra painting work its
60
+ * mixed-size runs need.
61
+ */
62
+ function smallCapsSegments(text) {
63
+ const mask = smallCapsMask(text);
64
+ const segments = [];
65
+ let current = "";
66
+ let currentSmall;
67
+ let index = 0;
68
+ for (const char of text) {
69
+ const small = mask[index] ?? false;
70
+ if (currentSmall !== void 0 && small !== currentSmall) {
71
+ segments.push({
72
+ text: current,
73
+ small: currentSmall
74
+ });
75
+ current = "";
76
+ }
77
+ current += small ? char.toLocaleUpperCase() : char;
78
+ currentSmall = small;
79
+ index += 1;
80
+ }
81
+ if (current) segments.push({
82
+ text: current,
83
+ small: currentSmall ?? false
84
+ });
85
+ return segments;
86
+ }
87
+ //#endregion
88
+ export { SMALL_CAPS_SCALE, isSmallCapsLetter, smallCapsMask, smallCapsSegments };
@@ -51,6 +51,8 @@ type PaginatorOptions = {
51
51
  firstPageMargins?: PageMargins;
52
52
  /** Per-section body margins used on even authored page numbers. */
53
53
  sectionEvenPageMargins?: (PageMargins | undefined)[];
54
+ /** Per-section body margins used on the first page of a `w:titlePg` section. */
55
+ sectionFirstPageMargins?: (PageMargins | undefined)[];
54
56
  /** Page-number policy for the initial section. */
55
57
  pageNumbering?: SectionPageNumbering;
56
58
  /** Column configuration (optional). */
@@ -94,7 +94,11 @@ function createPaginator(options) {
94
94
  }
95
95
  function getPageMargins(pageNumber, logicalPageNumber) {
96
96
  const evenMargins = logicalPageNumber % 2 === 0 ? options.sectionEvenPageMargins?.[currentSectionIndex] : void 0;
97
- const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...evenMargins ?? margins };
97
+ const titlePageMargins = currentSectionPageNumber === 1 ? options.sectionFirstPageMargins?.[currentSectionIndex] : void 0;
98
+ let pageMargins;
99
+ if (titlePageMargins) pageMargins = { ...titlePageMargins };
100
+ else if (pageNumber === 1 && options.firstPageMargins) pageMargins = { ...options.firstPageMargins };
101
+ else pageMargins = { ...evenMargins ?? margins };
98
102
  if (options.mirrorMargins === true && pageNumber % 2 === 0) return {
99
103
  ...pageMargins,
100
104
  left: pageMargins.right,
@@ -1,4 +1,4 @@
1
- import { FlowBlock, ParagraphBlock } from "./types.js";
1
+ import { FlowBlock, ParagraphBlock, TableBlock } from "./types.js";
2
2
  //#region src/layout-engine/paragraphSpacing.d.ts
3
3
  type EffectiveParagraphSpacing = Readonly<{
4
4
  before: number;
@@ -35,5 +35,7 @@ declare const resolveEffectiveParagraphSpacing: (block: ParagraphBlock) => Effec
35
35
  */
36
36
  declare const resolveEffectiveParagraphSpacingTree: (blocks: FlowBlock[]) => FlowBlock[];
37
37
  declare const paragraphsShareStyle: (previous: ParagraphBlock, current: ParagraphBlock) => boolean;
38
+ /** Derive the effective paragraph spacing of every cell story in a table. */
39
+ declare const resolveTableSpacing: (block: TableBlock) => TableBlock;
38
40
  //#endregion
39
- export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isAuthoredEmptyParagraph, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree };
41
+ export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isAuthoredEmptyParagraph, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree, resolveTableSpacing };
@@ -88,6 +88,7 @@ const resolveTextBoxContentSpacing = (blocks) => {
88
88
  });
89
89
  return changed ? resolved : blocks;
90
90
  };
91
+ /** Derive the effective paragraph spacing of every cell story in a table. */
91
92
  const resolveTableSpacing = (block) => {
92
93
  let changed = false;
93
94
  const rows = block.rows.map((row) => {
@@ -122,4 +123,4 @@ const resolveTextBoxSpacing = (block) => {
122
123
  };
123
124
  };
124
125
  //#endregion
125
- export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isAuthoredEmptyParagraph, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree };
126
+ export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isAuthoredEmptyParagraph, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree, resolveTableSpacing };
@@ -19,6 +19,13 @@ type TableRowBreakInfo = {
19
19
  /** Suppressible leading paragraph whitespace after each matching break offset. */
20
20
  continuationSkips: number[][];
21
21
  };
22
+ /**
23
+ * Height of the start of a table that a preceding `w:keepNext` paragraph
24
+ * (§17.3.1.15) must share a page with: the leading header rows (§17.4.49) and
25
+ * the first body row. A row that may break across pages (no `w:cantSplit`,
26
+ * §17.4.6) needs only its first line there; an unbreakable row needs all of it.
27
+ */
28
+ declare function tableKeepNextOpeningHeight(block: TableBlock, measure: TableMeasure): number;
22
29
  /** Build break geometry for a table from its block + measure. */
23
30
  declare function buildTableRowBreakInfo(block: TableBlock, measure: TableMeasure): TableRowBreakInfo;
24
31
  /** Leading empty-paragraph whitespace to consume when a row resumes after `offset`. */
@@ -30,4 +37,4 @@ declare function getRowContinuationSkip(info: TableRowBreakInfo, rowIndex: numbe
30
37
  */
31
38
  declare function snapRowBreak(info: TableRowBreakInfo, rowIndex: number, fromOffset: number, maxSlice: number): number;
32
39
  //#endregion
33
- export { TableRowBreakInfo, buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak };
40
+ export { TableRowBreakInfo, buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak, tableKeepNextOpeningHeight };
@@ -141,6 +141,27 @@ const continuationSkipAfter = (geometries, offset) => {
141
141
  if (skips.length === 0 || skips.some((skip) => skip <= BREAK_OFFSET_EPSILON)) return 0;
142
142
  return Math.min(...skips);
143
143
  };
144
+ /**
145
+ * Height of the start of a table that a preceding `w:keepNext` paragraph
146
+ * (§17.3.1.15) must share a page with: the leading header rows (§17.4.49) and
147
+ * the first body row. A row that may break across pages (no `w:cantSplit`,
148
+ * §17.4.6) needs only its first line there; an unbreakable row needs all of it.
149
+ */
150
+ function tableKeepNextOpeningHeight(block, measure) {
151
+ let height = 0;
152
+ let rowIndex = 0;
153
+ while (rowIndex < measure.rows.length && block.rows[rowIndex]?.isHeader === true) {
154
+ height += measure.rows[rowIndex]?.height ?? 0;
155
+ rowIndex += 1;
156
+ }
157
+ const row = measure.rows[rowIndex];
158
+ const sourceRow = block.rows[rowIndex];
159
+ if (!row || !sourceRow) return height;
160
+ const breakOffsets = buildTableRowBreakInfo(block, measure).breakOffsets[rowIndex] ?? [];
161
+ const firstBreak = breakOffsets.at(0);
162
+ const rowMayBreak = sourceRow.cantSplit !== true && breakOffsets.length > 1;
163
+ return height + (rowMayBreak && firstBreak !== void 0 ? firstBreak : row.height);
164
+ }
144
165
  /** Build break geometry for a table from its block + measure. */
145
166
  function buildTableRowBreakInfo(block, measure) {
146
167
  const rowCount = measure.rows.length;
@@ -208,4 +229,4 @@ function snapRowBreak(info, rowIndex, fromOffset, maxSlice) {
208
229
  return best;
209
230
  }
210
231
  //#endregion
211
- export { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak };
232
+ export { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak, tableKeepNextOpeningHeight };
@@ -2,7 +2,7 @@ import { OutlineStyleAttr } from "../types/documentEnumValues.js";
2
2
  import { CssBorderStyle } from "../utils/borderCss.js";
3
3
  import { ImagePosition, ImageWrap, InlineWrapper, NumberFormat, OutlineLevel, ParagraphNumberingOverride, PreviewDescriptor, SdtProperties, SdtType, SectionStart, ShapeTextBody, TableCellFormatting, TableWidthType, UnderlineStyle, VerticalAlign } from "@stll/docx-core/model";
4
4
  declare namespace types_d_exports {
5
- export { 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 };
5
+ export { 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 };
6
6
  }
7
7
  /**
8
8
  * Unique identifier for a block in the document.
@@ -849,6 +849,21 @@ declare const DEFAULT_TEXTBOX_MARGINS: {
849
849
  };
850
850
  /** Default text box width in pixels when no width is specified */
851
851
  declare const DEFAULT_TEXTBOX_WIDTH = 200;
852
+ /**
853
+ * A linear DrawingML gradient fill with its colors resolved (ECMA-376
854
+ * §20.1.8.33, §20.1.8.41). It runs along `angle`, in degrees clockwise from
855
+ * the positive x axis; when `scaled`, that angle is stated in the unit square
856
+ * and stretched with the box.
857
+ */
858
+ type TextBoxGradientFill = {
859
+ angle: number;
860
+ scaled: boolean;
861
+ /** Stops in ascending order; `offset` runs 0 to 1. */
862
+ stops: {
863
+ offset: number;
864
+ color: string;
865
+ }[];
866
+ };
852
867
  /**
853
868
  * Text box block — positioned container with block content.
854
869
  */
@@ -869,6 +884,8 @@ type TextBoxBlock = {
869
884
  verticalAlign?: ShapeTextBody["anchor"];
870
885
  /** Fill/background color */
871
886
  fillColor?: string;
887
+ /** Gradient fill, painted when no `fillColor` states a solid one. */
888
+ fillGradient?: TextBoxGradientFill;
872
889
  /** Border width in pixels */
873
890
  outlineWidth?: number;
874
891
  /** Border color */
@@ -1341,6 +1358,8 @@ type LayoutOptions = {
1341
1358
  firstPageMargins?: PageMargins;
1342
1359
  /** Per-section body margins used on even authored page numbers. */
1343
1360
  sectionEvenPageMargins?: (PageMargins | undefined)[];
1361
+ /** Per-section body margins used on the first page of a `w:titlePg` section. */
1362
+ sectionFirstPageMargins?: (PageMargins | undefined)[];
1344
1363
  /** Page-number policy for the initial section. */
1345
1364
  pageNumbering?: SectionPageNumbering;
1346
1365
  /** Body-level final section page size. */
@@ -1576,4 +1595,4 @@ declare const getTableRowLeadingWidth: (row: TableRow, columnWidths: readonly nu
1576
1595
  */
1577
1596
  declare function stripFlowBlockPmAnchors(block: FlowBlock): FlowBlock;
1578
1597
  //#endregion
1579
- export { 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, types_d_exports };
1598
+ export { 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, types_d_exports };