@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.
- package/dist/ai-edits/apply.js +565 -121
- package/dist/ai-edits/minimal-replacement.d.ts +64 -0
- package/dist/ai-edits/minimal-replacement.js +282 -0
- package/dist/controller/layoutPipeline.js +11 -1
- package/dist/display-list/build/paragraphPrimitives.js +63 -7
- package/dist/display-list/build/textBoxPrimitives.js +1 -1
- package/dist/display-list/build/unsupported.d.ts +2 -2
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/dom/renderDisplayListToDom.js +0 -1
- package/dist/docx/blockContentParser.js +2 -2
- package/dist/docx/bulletMarkers.d.ts +18 -2
- package/dist/docx/bulletMarkers.js +27 -2
- package/dist/docx/drawingGroupChildren.d.ts +60 -0
- package/dist/docx/drawingGroupChildren.js +176 -0
- package/dist/docx/drawingUtils.js +3 -0
- package/dist/docx/groupDrawingParser.d.ts +7 -1
- package/dist/docx/groupDrawingParser.js +123 -32
- package/dist/docx/packageParts.d.ts +45 -1
- package/dist/docx/packageParts.js +93 -2
- package/dist/docx/paraIdAttribute.d.ts +3 -2
- package/dist/docx/paragraphParser.js +91 -7
- package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
- package/dist/docx/runParser.js +10 -0
- package/dist/docx/selectiveSaveFlags.d.ts +10 -3
- package/dist/docx/selectiveSaveFlags.js +1 -1
- package/dist/docx/selectiveXmlPatch.d.ts +56 -34
- package/dist/docx/selectiveXmlPatch.js +222 -115
- package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
- package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
- package/dist/docx/serializer/paragraphSerializer.js +5 -4
- package/dist/docx/serializer/runSerializer.d.ts +5 -1
- package/dist/docx/serializer/runSerializer.js +4 -3
- package/dist/docx/shapeAlternateContent.d.ts +25 -0
- package/dist/docx/shapeAlternateContent.js +52 -0
- package/dist/docx/textBoxParser.d.ts +10 -2
- package/dist/docx/textBoxParser.js +9 -3
- package/dist/internal/headlessRevisionResolution.js +50 -13
- package/dist/layout-bridge/convert/toFlowBlocks.js +26 -4
- package/dist/layout-engine/index.d.ts +2 -2
- package/dist/layout-engine/index.js +33 -4
- package/dist/layout-engine/keep-together.d.ts +5 -2
- package/dist/layout-engine/keep-together.js +5 -1
- package/dist/layout-engine/measure/advanceComposition.js +49 -3
- package/dist/layout-engine/measure/measureBlocks.js +5 -3
- package/dist/layout-engine/measure/measureContainer.js +46 -3
- package/dist/layout-engine/measure/measureHelpers.js +13 -3
- package/dist/layout-engine/measure/measureParagraph.js +44 -81
- package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
- package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
- package/dist/layout-engine/paginator.d.ts +2 -0
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
- package/dist/layout-engine/paragraphSpacing.js +2 -1
- package/dist/layout-engine/tableRowBreak.d.ts +8 -1
- package/dist/layout-engine/tableRowBreak.js +22 -1
- package/dist/layout-engine/types.d.ts +21 -2
- package/dist/layout-painter/renderParagraph.js +41 -4
- package/dist/layout-painter/renderTextBox.d.ts +12 -2
- package/dist/layout-painter/renderTextBox.js +26 -2
- package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
- package/dist/prosemirror/alternateContentAttrs.js +35 -0
- package/dist/prosemirror/attrs/index.js +90 -0
- package/dist/prosemirror/commands/comments.js +51 -41
- package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
- package/dist/prosemirror/contentControlRevisions.js +84 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
- package/dist/prosemirror/conversion/toProseDoc.js +42 -19
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
- package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
- package/dist/prosemirror/listMarker.js +2 -2
- package/dist/prosemirror/paragraphDirection.d.ts +29 -2
- package/dist/prosemirror/paragraphDirection.js +21 -2
- package/dist/prosemirror/plugins/suggestionMode.js +37 -8
- package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
- package/dist/prosemirror/rejoinRunCarriers.js +66 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
- package/dist/prosemirror/schema/nodes.d.ts +37 -5
- package/dist/types/content.d.ts +2 -2
- package/package.json +2 -2
- package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
- 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
|
|
304
|
-
|
|
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
|
|
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
|
|
33
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
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
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
|
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 };
|