@stll/folio-core 0.7.0 → 0.9.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/README.md +39 -0
- package/dist/ai-edits/apply.js +18 -4
- package/dist/ai-edits/blockRange.d.ts +13 -2
- package/dist/ai-edits/blockRange.js +23 -2
- package/dist/ai-edits/headless.d.ts +31 -4
- package/dist/ai-edits/headless.js +148 -35
- package/dist/ai-edits/index.d.ts +6 -4
- package/dist/ai-edits/index.js +4 -1
- package/dist/ai-edits/scoped-reading.d.ts +9 -0
- package/dist/ai-edits/scoped-reading.js +60 -0
- package/dist/ai-edits/snapshot.js +53 -7
- package/dist/ai-edits/types.d.ts +46 -3
- package/dist/compat/eigenpal.d.ts +8 -3
- package/dist/compat/eigenpal.js +7 -2
- package/dist/controller/fontReadiness.d.ts +29 -0
- package/dist/controller/fontReadiness.js +139 -0
- package/dist/controller/latestRequestGate.d.ts +13 -0
- package/dist/controller/latestRequestGate.js +16 -0
- package/dist/controller/layoutPipeline.js +88 -10
- package/dist/document-operations.d.ts +10 -3
- package/dist/document-operations.js +29 -14
- package/dist/docx/blockContentParser.js +53 -10
- package/dist/docx/commentParser.js +1 -1
- package/dist/docx/compatibility.d.ts +30 -2
- package/dist/docx/compatibility.js +118 -39
- package/dist/docx/conformance.d.ts +6 -0
- package/dist/docx/conformance.js +18 -0
- package/dist/docx/encryption/agileDecryption.js +1 -1
- package/dist/docx/encryption/encryptionInfo.js +1 -1
- package/dist/docx/encryption/openEncryptedDocx.js +1 -1
- package/dist/docx/ensureParaIds.d.ts +29 -0
- package/dist/docx/ensureParaIds.js +423 -0
- package/dist/docx/fontTableParser.d.ts +6 -0
- package/dist/docx/fontTableParser.js +72 -0
- package/dist/docx/groupDrawingParser.js +7 -2
- package/dist/docx/headerFooterParser.js +1 -1
- package/dist/docx/normalizeBaseDirection.js +1 -1
- package/dist/docx/numberingParser.js +2 -0
- package/dist/docx/paragraphParser.js +21 -1
- package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
- package/dist/docx/parser.d.ts +1 -1
- package/dist/docx/parser.js +14 -9
- package/dist/docx/rezip.js +70 -5
- package/dist/docx/runConsolidator.js +4 -0
- package/dist/docx/runParser.js +19 -1
- package/dist/docx/selectiveSave.js +3 -3
- package/dist/docx/selectiveXmlPatch.d.ts +10 -1
- package/dist/docx/selectiveXmlPatch.js +1 -1
- package/dist/docx/serializer/fontTableSerializer.d.ts +6 -0
- package/dist/docx/serializer/fontTableSerializer.js +26 -0
- package/dist/docx/serializer/headerFooterSerializer.js +1 -1
- package/dist/docx/serializer/paragraphSerializer.js +8 -0
- package/dist/docx/serializer/runSerializer.js +14 -2
- package/dist/docx/serializer/settingsSerializer.d.ts +6 -0
- package/dist/docx/serializer/settingsSerializer.js +16 -0
- package/dist/docx/serializer/stylesSerializer.d.ts +6 -0
- package/dist/docx/serializer/stylesSerializer.js +56 -0
- package/dist/docx/serializer/tableSerializer.js +29 -26
- package/dist/docx/serializer/themeSerializer.d.ts +6 -0
- package/dist/docx/serializer/themeSerializer.js +36 -0
- package/dist/docx/settingsParser.js +46 -0
- package/dist/docx/styleParser.js +26 -1
- package/dist/docx/tableParser.js +19 -1
- package/dist/docx/textBoxParser.js +6 -1
- package/dist/docx/vmlImageParser.js +146 -1
- package/dist/i18n/messages/catalogs.gen.d.ts +5916 -5916
- package/dist/i18n/messages/catalogs.gen.js +5910 -5910
- package/dist/index.d.ts +8 -3
- package/dist/index.js +7 -2
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +3 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +3 -1
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +3 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +22 -3
- package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
- package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +14 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +93 -17
- package/dist/layout-bridge/engine/hitTest.js +2 -1
- package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
- package/dist/layout-bridge/engine/measuring/index.js +2 -1
- package/dist/layout-bridge/engine/selectionRects.js +2 -1
- package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
- package/dist/layout-engine/headerFooterRefs.js +40 -0
- package/dist/layout-engine/index.d.ts +3 -2
- package/dist/layout-engine/index.js +68 -106
- package/dist/layout-engine/measure/cache.js +11 -3
- package/dist/layout-engine/measure/font-metrics.worker.js +1 -0
- package/dist/layout-engine/measure/index.d.ts +2 -1
- package/dist/layout-engine/measure/index.js +2 -1
- package/dist/layout-engine/measure/lineBreakProvider.d.ts +33 -0
- package/dist/layout-engine/measure/lineBreakProvider.js +252 -0
- package/dist/layout-engine/measure/lineBreaks.d.ts +7 -2
- package/dist/layout-engine/measure/lineBreaks.js +11 -18
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +4 -1
- package/dist/layout-engine/measure/listMarkerWidth.js +33 -8
- package/dist/layout-engine/measure/measureBlocks.d.ts +4 -2
- package/dist/layout-engine/measure/measureBlocks.js +138 -86
- package/dist/layout-engine/measure/measureContainer.js +32 -9
- package/dist/layout-engine/measure/measureHelpers.js +5 -2
- package/dist/layout-engine/measure/measureParagraph.js +346 -74
- package/dist/layout-engine/measure/measureTypes.d.ts +2 -1
- package/dist/layout-engine/measure/measureWorker.d.ts +2 -2
- package/dist/layout-engine/measure/measureWorker.js +8 -19
- package/dist/layout-engine/measure/measureWorkerProtocol.d.ts +3 -14
- package/dist/layout-engine/measure/tableCellFloating.d.ts +10 -3
- package/dist/layout-engine/measure/tableCellFloating.js +37 -29
- package/dist/layout-engine/measure/tableCellFlow.d.ts +27 -0
- package/dist/layout-engine/measure/tableCellFlow.js +73 -0
- package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
- package/dist/layout-engine/measure/tableCellGrid.js +62 -0
- package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
- package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
- package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +18 -0
- package/dist/layout-engine/measure/textMeasurementPolicy.js +24 -0
- package/dist/layout-engine/paginator.d.ts +2 -1
- package/dist/layout-engine/paginator.js +7 -6
- package/dist/layout-engine/paragraphFrame.d.ts +22 -0
- package/dist/layout-engine/paragraphFrame.js +17 -0
- package/dist/layout-engine/paragraphSequence.d.ts +7 -0
- package/dist/layout-engine/paragraphSequence.js +25 -0
- package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
- package/dist/layout-engine/paragraphSpacing.js +30 -0
- package/dist/layout-engine/renderedBreakReconciliation.d.ts +63 -0
- package/dist/layout-engine/renderedBreakReconciliation.js +86 -0
- package/dist/layout-engine/tableRowBreak.d.ts +5 -2
- package/dist/layout-engine/tableRowBreak.js +62 -29
- package/dist/layout-engine/types.d.ts +54 -12
- package/dist/layout-engine/types.js +15 -2
- package/dist/layout-painter/anchoredImagePosition.d.ts +23 -0
- package/dist/layout-painter/anchoredImagePosition.js +95 -0
- package/dist/layout-painter/borderStroke.d.ts +23 -0
- package/dist/layout-painter/borderStroke.js +39 -0
- package/dist/layout-painter/renderImage.d.ts +5 -4
- package/dist/layout-painter/renderImage.js +18 -4
- package/dist/layout-painter/renderPage.d.ts +2 -27
- package/dist/layout-painter/renderPage.js +15 -101
- package/dist/layout-painter/renderParagraph.js +45 -26
- package/dist/layout-painter/renderTable.d.ts +2 -0
- package/dist/layout-painter/renderTable.js +257 -47
- package/dist/layout-painter/renderTextBox.js +17 -7
- package/dist/managers/DocumentLoaderManager.d.ts +1 -1
- package/dist/managers/DocumentLoaderManager.js +2 -2
- package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
- package/dist/paged-layout/sectionBlockWidths.js +9 -0
- package/dist/paged-layout/sectionGeometry.js +1 -1
- package/dist/prosemirror/attrs/index.d.ts +4 -2
- package/dist/prosemirror/attrs/index.js +32 -2
- package/dist/prosemirror/commands/propertyChangeScope.js +1 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +35 -18
- package/dist/prosemirror/conversion/toProseDoc.js +73 -37
- package/dist/prosemirror/extensions/StarterKit.js +2 -0
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +8 -2
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +1 -1
- package/dist/prosemirror/extensions/features/ListExtension.js +2 -0
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
- package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +1 -0
- package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
- package/dist/prosemirror/extensions/marks/LanguageExtension.d.ts +7 -0
- package/dist/prosemirror/extensions/marks/LanguageExtension.js +30 -0
- package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
- package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +1 -1
- package/dist/prosemirror/extensions/marks/markUtils.js +12 -0
- package/dist/prosemirror/extensions/nodes/ImageExtension.js +2 -1
- package/dist/prosemirror/extensions/nodes/TableExtension.js +4 -2
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/marks.d.ts +6 -1
- package/dist/prosemirror/schema/nodes.d.ts +16 -12
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +2 -0
- package/dist/prosemirror/utils/tabCalculator.d.ts +3 -1
- package/dist/prosemirror/utils/tabCalculator.js +12 -10
- package/dist/prosemirror/validation.js +4 -1
- package/dist/redline.js +13 -3
- package/dist/server.d.ts +9 -4
- package/dist/server.js +7 -2
- package/dist/style-sets/extract.d.ts +35 -0
- package/dist/style-sets/extract.js +123 -0
- package/dist/style-sets/stellaStyle.d.ts +13 -0
- package/dist/style-sets/stellaStyle.js +516 -0
- package/dist/style-sets/types.d.ts +31 -0
- package/dist/style-sets/types.js +5 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/utils/createDocument.d.ts +12 -2
- package/dist/utils/createDocument.js +55 -36
- package/dist/utils/fontResolver.js +6 -6
- package/dist/utils/formatToStyle.js +1 -1
- package/dist/utils/hexId.d.ts +8 -1
- package/dist/utils/hexId.js +11 -3
- package/dist/utils/paragraphFormattingMerge.d.ts +14 -3
- package/dist/utils/paragraphFormattingMerge.js +14 -4
- package/package.json +5 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
|
|
2
1
|
import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
|
|
2
|
+
import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
|
|
3
|
+
import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
|
|
3
4
|
import { buildRunFontStyle, ptToPx } from "./measureHelpers.js";
|
|
5
|
+
import "./lineBreakProvider.js";
|
|
4
6
|
import { calculateTabWidth, pixelsToTwips } from "../../prosemirror/utils/tabCalculator.js";
|
|
5
7
|
import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
|
|
6
8
|
import { hasCjk } from "../../utils/scriptSegments.js";
|
|
@@ -9,7 +11,7 @@ import { isFloatingImageRun } from "../types.js";
|
|
|
9
11
|
import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
|
|
10
12
|
import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
|
|
11
13
|
import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
|
|
12
|
-
import { findWordBreaks, isBreakChar } from "./lineBreaks.js";
|
|
14
|
+
import { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakChar, isHangingPunctuation } from "./lineBreaks.js";
|
|
13
15
|
//#region src/layout-engine/measure/measureParagraph.ts
|
|
14
16
|
/**
|
|
15
17
|
* Paragraph measurement module
|
|
@@ -22,8 +24,11 @@ const DEFAULT_FONT_FAMILY = "Calibri";
|
|
|
22
24
|
const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
|
|
23
25
|
const WIDTH_TOLERANCE = .5;
|
|
24
26
|
const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
|
|
27
|
+
const JUSTIFY_SPACE_CONTRACTION_RATIO = .1;
|
|
28
|
+
const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
|
|
29
|
+
const JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO = .23;
|
|
30
|
+
const JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO = .015;
|
|
25
31
|
const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
|
|
26
|
-
const JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO = .025;
|
|
27
32
|
const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
|
|
28
33
|
const DEFAULT_LIST_HANGING_INDENT_PX = 24;
|
|
29
34
|
const ALL_CAPS_RATIO_THRESHOLD = .8;
|
|
@@ -31,18 +36,20 @@ const ALL_CAPS_RATIO_THRESHOLD = .8;
|
|
|
31
36
|
* Find the longest prefix of `text` that fits within `maxWidth` pixels.
|
|
32
37
|
* Returns the number of characters that fit (at least 1 if `forceMin` is true).
|
|
33
38
|
*/
|
|
34
|
-
function findMaxFittingLength(text, style, maxWidth, forceMin = false) {
|
|
35
|
-
|
|
36
|
-
let
|
|
39
|
+
function findMaxFittingLength(text, style, maxWidth, forceMin = false, policy) {
|
|
40
|
+
const boundaries = findGraphemeBreaks(text, policy);
|
|
41
|
+
let lo = 0;
|
|
42
|
+
let hi = boundaries.length - 1;
|
|
37
43
|
let best = 0;
|
|
38
44
|
while (lo <= hi) {
|
|
39
45
|
const mid = Math.floor((lo + hi) / 2);
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
const boundary = boundaries[mid];
|
|
47
|
+
if (measureTextWidth(text.slice(0, boundary), style) <= maxWidth) {
|
|
48
|
+
best = boundary;
|
|
42
49
|
lo = mid + 1;
|
|
43
50
|
} else hi = mid - 1;
|
|
44
51
|
}
|
|
45
|
-
return forceMin && best === 0 ?
|
|
52
|
+
return forceMin && best === 0 ? boundaries.at(0) ?? 0 : best;
|
|
46
53
|
}
|
|
47
54
|
/**
|
|
48
55
|
* Extract FontStyle from a run that carries RunFormatting (text, tab, or
|
|
@@ -53,6 +60,31 @@ function findMaxFittingLength(text, style, maxWidth, forceMin = false) {
|
|
|
53
60
|
function runToFontStyle(run) {
|
|
54
61
|
return buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
|
|
55
62
|
}
|
|
63
|
+
function languageMatches(ruleLanguage, locale) {
|
|
64
|
+
if (!ruleLanguage) return true;
|
|
65
|
+
if (!locale) return false;
|
|
66
|
+
const rule = ruleLanguage.toLowerCase();
|
|
67
|
+
const active = locale.toLowerCase();
|
|
68
|
+
return active === rule || active.startsWith(`${rule}-`) || rule.startsWith(`${active}-`);
|
|
69
|
+
}
|
|
70
|
+
function lineBreakPolicy(block, run) {
|
|
71
|
+
const language = run.language;
|
|
72
|
+
let locale = language?.val;
|
|
73
|
+
if (hasCjk(run.text)) locale = language?.eastAsia ?? language?.val;
|
|
74
|
+
else if (run.rtl) locale = language?.bidi ?? language?.val;
|
|
75
|
+
const rules = block.attrs?.lineBreakRules;
|
|
76
|
+
const before = rules?.noLineBreaksBefore;
|
|
77
|
+
const after = rules?.noLineBreaksAfter;
|
|
78
|
+
return {
|
|
79
|
+
...locale ? { locale } : {},
|
|
80
|
+
...block.attrs?.kinsoku !== void 0 ? { kinsoku: block.attrs.kinsoku } : {},
|
|
81
|
+
...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: before.characters } : {},
|
|
82
|
+
...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: after.characters } : {},
|
|
83
|
+
...rules?.useLegacyEthiopicAmharicRules ? { useLegacyEthiopicAmharicRules: true } : {},
|
|
84
|
+
...block.attrs?.automaticHyphenation?.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: block.attrs.automaticHyphenation.doNotHyphenateCaps } : {},
|
|
85
|
+
...run.allCaps === true ? { renderedAllCaps: true } : {}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
56
88
|
/**
|
|
57
89
|
* Line-HEIGHT style for a text run. Word derives a CJK line's height from an
|
|
58
90
|
* East-Asian face, not the run's ascii font: real documents routinely put CJK
|
|
@@ -245,6 +277,7 @@ function isEmptyTextRun(run) {
|
|
|
245
277
|
* preceding a floating image.
|
|
246
278
|
*/
|
|
247
279
|
function isBlockLayoutImageRun(run) {
|
|
280
|
+
if (isFloatingImageRun(run)) return false;
|
|
248
281
|
return run.wrapType === "topAndBottom" || run.displayMode === "block";
|
|
249
282
|
}
|
|
250
283
|
function measureInlineWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
@@ -329,34 +362,63 @@ function uppercaseLetterRatio(text) {
|
|
|
329
362
|
}
|
|
330
363
|
return letters === 0 ? 0 : uppercase / letters;
|
|
331
364
|
}
|
|
332
|
-
function
|
|
333
|
-
|
|
334
|
-
if (totalSpaces === 0) return tolerance;
|
|
335
|
-
return Math.max(JUSTIFY_SHRINK_TOLERANCE_RATIO, tolerance * (regularSpaceCount / totalSpaces));
|
|
336
|
-
}
|
|
337
|
-
function justifyShrinkToleranceRatio(block, isFirstLine, regularSpaceCount, nonBreakingSpaceCount) {
|
|
365
|
+
function resolveJustifyFitStrategy(block, isFirstLine, profile) {
|
|
366
|
+
if (isShallowFullHangingListContinuation(block, isFirstLine)) return { type: "rounding" };
|
|
338
367
|
if (block.attrs?.listMarker !== void 0) {
|
|
339
368
|
const hanging = block.attrs.indent?.hanging ?? 0;
|
|
340
|
-
if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ?
|
|
369
|
+
if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ? {
|
|
370
|
+
type: "space",
|
|
371
|
+
ratio: JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO
|
|
372
|
+
} : {
|
|
373
|
+
type: "width",
|
|
374
|
+
ratio: JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO
|
|
375
|
+
};
|
|
341
376
|
const left = block.attrs.indent?.left ?? 0;
|
|
342
|
-
if (hanging > 0 && left > hanging) return
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
377
|
+
if (hanging > 0 && left > hanging) return {
|
|
378
|
+
type: "width",
|
|
379
|
+
ratio: JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
type: "space",
|
|
383
|
+
ratio: JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO
|
|
384
|
+
};
|
|
348
385
|
}
|
|
349
386
|
const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
387
|
+
if (isFirstLine && profile.hasTabRuns && (block.attrs?.indent?.hanging ?? 0) > 0) return {
|
|
388
|
+
type: "width",
|
|
389
|
+
ratio: JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
390
|
+
};
|
|
391
|
+
if (!isFirstLine && profile.hasTabRuns && !hasTabStops) return {
|
|
392
|
+
type: "width",
|
|
393
|
+
ratio: JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO
|
|
394
|
+
};
|
|
395
|
+
if (profile.hasTabRuns && (isFirstLine || hasTabStops)) return {
|
|
396
|
+
type: "width",
|
|
397
|
+
ratio: (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
398
|
+
};
|
|
399
|
+
if (profile.uppercaseRatio > ALL_CAPS_RATIO_THRESHOLD) return {
|
|
400
|
+
type: "width",
|
|
401
|
+
ratio: JUSTIFY_SHRINK_TOLERANCE_RATIO
|
|
402
|
+
};
|
|
403
|
+
return {
|
|
404
|
+
type: "space",
|
|
405
|
+
ratio: JUSTIFY_SPACE_CONTRACTION_RATIO
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function compressibleSpaceWidth(text, style) {
|
|
409
|
+
const count = countCompressibleSpaces(text);
|
|
410
|
+
return count === 0 ? 0 : count * measureTextWidth(" ", style);
|
|
411
|
+
}
|
|
412
|
+
function justifyFitTolerance(line, strategy, candidateSpaceWidth) {
|
|
413
|
+
if (strategy.type === "rounding") return WIDTH_TOLERANCE;
|
|
414
|
+
if (strategy.type === "width") return Math.max(WIDTH_TOLERANCE, line.availableWidth * strategy.ratio);
|
|
415
|
+
return Math.max(WIDTH_TOLERANCE, (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio);
|
|
416
|
+
}
|
|
417
|
+
function isShallowFullHangingListContinuation(block, isFirstLine) {
|
|
418
|
+
if (isFirstLine || block.attrs?.listMarker === void 0) return false;
|
|
419
|
+
const hanging = block.attrs.indent?.hanging ?? 0;
|
|
420
|
+
const left = block.attrs.indent?.left ?? 0;
|
|
421
|
+
return hanging > 0 && hanging < DEFAULT_LIST_HANGING_INDENT_PX && Math.abs(left - hanging) <= WIDTH_TOLERANCE;
|
|
360
422
|
}
|
|
361
423
|
function trimTrailingSpacesAndTabs(text) {
|
|
362
424
|
let end = text.length;
|
|
@@ -367,12 +429,20 @@ function trimTrailingSpacesAndTabs(text) {
|
|
|
367
429
|
}
|
|
368
430
|
return text.slice(0, end);
|
|
369
431
|
}
|
|
432
|
+
function trailingHangingPunctuationWidth(text, style, policy, enabled) {
|
|
433
|
+
if (!enabled || text.length === 0) return 0;
|
|
434
|
+
const lastStart = findGraphemeBreaks(text, policy).at(-2) ?? 0;
|
|
435
|
+
const lastGrapheme = text.slice(lastStart);
|
|
436
|
+
if (!isHangingPunctuation(lastGrapheme, policy)) return 0;
|
|
437
|
+
return measureTextWidth(lastGrapheme, style);
|
|
438
|
+
}
|
|
370
439
|
/**
|
|
371
440
|
* Width of the unbreakable text glued to the end of each run.
|
|
372
441
|
* A run boundary is not itself a wrap opportunity: adjacent note markers and
|
|
373
442
|
* format-only word splits must wrap as one cluster.
|
|
374
443
|
*/
|
|
375
|
-
function computeTrailingGlueWidths(
|
|
444
|
+
function computeTrailingGlueWidths(block) {
|
|
445
|
+
const runs = block.runs;
|
|
376
446
|
const widths = Array.from({ length: runs.length }, () => 0);
|
|
377
447
|
for (let index = runs.length - 1; index >= 0; index--) {
|
|
378
448
|
const nextRun = runs[index + 1];
|
|
@@ -382,19 +452,112 @@ function computeTrailingGlueWidths(runs) {
|
|
|
382
452
|
widths[index] = widths[index + 1] ?? 0;
|
|
383
453
|
continue;
|
|
384
454
|
}
|
|
385
|
-
if (isSpaceOrTab(text
|
|
455
|
+
if (isSpaceOrTab(text.at(0))) continue;
|
|
386
456
|
const style = runToFontStyle(nextRun);
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
457
|
+
const firstBreak = findWordBreaks(text, lineBreakPolicy(block, nextRun)).at(0);
|
|
458
|
+
widths[index] = measureTextWidth(firstBreak === void 0 ? text : trimTrailingSpacesAndTabs(text.slice(0, firstBreak)), style) + (firstBreak === void 0 ? widths[index + 1] ?? 0 : 0);
|
|
459
|
+
}
|
|
460
|
+
return widths;
|
|
461
|
+
}
|
|
462
|
+
function computeProtectedCrossRunGlueWidths(block) {
|
|
463
|
+
const widths = Array.from({ length: block.runs.length }, () => 0);
|
|
464
|
+
for (let index = 0; index < block.runs.length; index++) {
|
|
465
|
+
const run = block.runs[index];
|
|
466
|
+
if (!run || !isTextRun(run)) continue;
|
|
467
|
+
const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
|
|
468
|
+
const token = trailing?.[1];
|
|
469
|
+
const policy = lineBreakPolicy(block, run);
|
|
470
|
+
if (!token || token.length !== 1 || !policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
|
|
471
|
+
let separator = trailing[2] ?? "";
|
|
472
|
+
let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
|
|
473
|
+
let followingWord = "";
|
|
474
|
+
let unseparatedFollowingText = false;
|
|
475
|
+
for (let nextIndex = index + 1; nextIndex < block.runs.length; nextIndex++) {
|
|
476
|
+
const nextRun = block.runs[nextIndex];
|
|
477
|
+
if (!nextRun || !isTextRun(nextRun)) break;
|
|
478
|
+
const text = nextRun.text ?? "";
|
|
479
|
+
let consumed = "";
|
|
480
|
+
for (const char of text) {
|
|
481
|
+
const isWhitespace = /\s/u.test(char);
|
|
482
|
+
if (followingWord.length > 0 && isWhitespace) break;
|
|
483
|
+
if (separator.length === 0 && !isWhitespace) {
|
|
484
|
+
unseparatedFollowingText = true;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
consumed += char;
|
|
488
|
+
if (isWhitespace) separator += char;
|
|
489
|
+
else followingWord += char;
|
|
490
|
+
}
|
|
491
|
+
if (consumed.length > 0) glueWidth += measureTextWidth(consumed, runToFontStyle(nextRun));
|
|
492
|
+
if (unseparatedFollowingText) break;
|
|
493
|
+
if (followingWord.length > 0 && consumed.length < text.length) break;
|
|
391
494
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
495
|
+
if (unseparatedFollowingText || separator.length === 0 || followingWord.length === 0) continue;
|
|
496
|
+
const boundary = token.length + separator.length;
|
|
497
|
+
if (!findWordBreaks(token + separator + followingWord, policy).includes(boundary)) widths[index] = glueWidth;
|
|
395
498
|
}
|
|
396
499
|
return widths;
|
|
397
500
|
}
|
|
501
|
+
/** Collect one lexical word continued through adjacent formatting runs. */
|
|
502
|
+
function collectCrossRunWord({ block, startRunIndex, startChar }) {
|
|
503
|
+
const segments = [];
|
|
504
|
+
let text = "";
|
|
505
|
+
let width = 0;
|
|
506
|
+
for (let runIndex = startRunIndex; runIndex < block.runs.length; runIndex++) {
|
|
507
|
+
const run = block.runs[runIndex];
|
|
508
|
+
if (!run || !isTextRun(run)) break;
|
|
509
|
+
const start = runIndex === startRunIndex ? startChar : 0;
|
|
510
|
+
const remainder = run.text.slice(start);
|
|
511
|
+
if (remainder.length === 0) continue;
|
|
512
|
+
if (runIndex !== startRunIndex && isSpaceOrTab(remainder[0])) break;
|
|
513
|
+
const remainingBudget = 256 - text.length;
|
|
514
|
+
const boundedRemainder = remainder.slice(0, remainingBudget + 1);
|
|
515
|
+
const firstBreak = findWordBreaks(boundedRemainder, lineBreakPolicy(block, run)).at(0);
|
|
516
|
+
const segmentText = trimTrailingSpacesAndTabs(firstBreak === void 0 ? boundedRemainder : boundedRemainder.slice(0, firstBreak));
|
|
517
|
+
if (segmentText.length === 0) break;
|
|
518
|
+
if (segmentText.length > remainingBudget) return;
|
|
519
|
+
const style = runToFontStyle(run);
|
|
520
|
+
segments.push({
|
|
521
|
+
runIndex,
|
|
522
|
+
startChar: start,
|
|
523
|
+
text: segmentText,
|
|
524
|
+
run,
|
|
525
|
+
style
|
|
526
|
+
});
|
|
527
|
+
text += segmentText;
|
|
528
|
+
width += measureTextWidth(segmentText, style);
|
|
529
|
+
if (firstBreak !== void 0) break;
|
|
530
|
+
}
|
|
531
|
+
if (segments.length < 2) return;
|
|
532
|
+
const firstRun = segments[0]?.run;
|
|
533
|
+
if (!firstRun) return;
|
|
534
|
+
return {
|
|
535
|
+
text,
|
|
536
|
+
width,
|
|
537
|
+
breaks: findHyphenationBreaks(text, lineBreakPolicy(block, firstRun)),
|
|
538
|
+
segments
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function measureCrossRunPrefix(word, breakOffset) {
|
|
542
|
+
let width = 0;
|
|
543
|
+
let remaining = breakOffset;
|
|
544
|
+
const measuredSegments = [];
|
|
545
|
+
for (const segment of word.segments) {
|
|
546
|
+
const length = Math.min(remaining, segment.text.length);
|
|
547
|
+
if (length > 0) {
|
|
548
|
+
width += measureTextWidth(segment.text.slice(0, length), segment.style);
|
|
549
|
+
measuredSegments.push(segment);
|
|
550
|
+
}
|
|
551
|
+
if (remaining <= segment.text.length) return {
|
|
552
|
+
width,
|
|
553
|
+
endRunIndex: segment.runIndex,
|
|
554
|
+
endChar: segment.startChar + remaining,
|
|
555
|
+
hyphenStyle: segment.style,
|
|
556
|
+
segments: measuredSegments
|
|
557
|
+
};
|
|
558
|
+
remaining -= segment.text.length;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
398
561
|
/**
|
|
399
562
|
* Minimum horizontal room a line must offer before we treat it as usable for
|
|
400
563
|
* body text. Below this threshold the line is bumped past obstructing floats
|
|
@@ -444,6 +607,12 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
444
607
|
const attrs = block.attrs;
|
|
445
608
|
const spacing = attrs?.spacing;
|
|
446
609
|
const isJustifiedParagraph = attrs?.alignment === "justify";
|
|
610
|
+
const justificationProfile = {
|
|
611
|
+
hasTabRuns: isJustifiedParagraph && runs.some(isTabRun),
|
|
612
|
+
uppercaseRatio: isJustifiedParagraph ? uppercaseLetterRatio(runs.map((run) => isTextRun(run) ? run.text ?? "" : "").join("")) : 0
|
|
613
|
+
};
|
|
614
|
+
const firstLineJustifyFitStrategy = resolveJustifyFitStrategy(block, true, justificationProfile);
|
|
615
|
+
const continuationJustifyFitStrategy = resolveJustifyFitStrategy(block, false, justificationProfile);
|
|
447
616
|
const floatingZones = options?.floatingZones;
|
|
448
617
|
const paragraphYOffset = options?.paragraphYOffset ?? 0;
|
|
449
618
|
const indent = attrs?.indent;
|
|
@@ -474,6 +643,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
474
643
|
const firstLineFloatingMargins = getFloatingMargins(cumulativeHeight, estimatedFirstLineHeight, floatingZones, paragraphYOffset);
|
|
475
644
|
const firstLineWidth = Math.max(1, getFloatingAvailableWidth(firstLineFloatingMargins, baseFirstLineWidth));
|
|
476
645
|
const lines = [];
|
|
646
|
+
let consecutiveHyphenatedLines = 0;
|
|
477
647
|
if (runs.length === 0) {
|
|
478
648
|
if (attrs?.suppressEmptyParagraphHeight) {
|
|
479
649
|
lines.push({
|
|
@@ -532,7 +702,8 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
532
702
|
totalHeight
|
|
533
703
|
};
|
|
534
704
|
}
|
|
535
|
-
const trailingGlueWidths = computeTrailingGlueWidths(
|
|
705
|
+
const trailingGlueWidths = computeTrailingGlueWidths(block);
|
|
706
|
+
const protectedCrossRunGlueWidths = computeProtectedCrossRunGlueWidths(block);
|
|
536
707
|
let currentLine = {
|
|
537
708
|
fromRun: 0,
|
|
538
709
|
fromChar: 0,
|
|
@@ -540,11 +711,11 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
540
711
|
toChar: 0,
|
|
541
712
|
width: 0,
|
|
542
713
|
trailingWhitespaceWidth: 0,
|
|
543
|
-
|
|
544
|
-
nonBreakingSpaceCount: 0,
|
|
714
|
+
regularSpaceWidth: 0,
|
|
545
715
|
maxFontSize: DEFAULT_FONT_SIZE,
|
|
546
716
|
maxFontMetrics: null,
|
|
547
717
|
maxImageHeightPx: 0,
|
|
718
|
+
maxExactImageHeightPx: 0,
|
|
548
719
|
maxMathHeightPx: 0,
|
|
549
720
|
availableWidth: firstLineWidth,
|
|
550
721
|
leftOffset: firstLineFloatingMargins.leftMargin,
|
|
@@ -552,14 +723,29 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
552
723
|
...firstLineFloatingMargins.segments?.length ? { segmentZones: firstLineFloatingMargins.segments } : {}
|
|
553
724
|
};
|
|
554
725
|
const calculateLineTypography = (line) => {
|
|
555
|
-
const
|
|
726
|
+
const paragraphFontSize = attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE;
|
|
727
|
+
const paragraphFontFamily = attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY;
|
|
728
|
+
const fontSize = line.maxFontMetrics ? line.maxFontSize : paragraphFontSize;
|
|
729
|
+
const metrics = line.maxFontMetrics ?? getFontMetrics({
|
|
730
|
+
fontSize: paragraphFontSize,
|
|
731
|
+
fontFamily: paragraphFontFamily
|
|
732
|
+
});
|
|
733
|
+
const finalTypography = { ...calculateTypographyMetrics(fontSize, spacing, metrics) };
|
|
556
734
|
const inlineObjectHeight = Math.max(line.maxImageHeightPx, line.maxMathHeightPx);
|
|
557
735
|
if (inlineObjectHeight > finalTypography.lineHeight) {
|
|
558
736
|
const objectHeight = inlineObjectHeight;
|
|
559
737
|
const buffer = finalTypography.descent;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
finalTypography.
|
|
738
|
+
const soleRun = line.fromRun === line.toRun ? runs[line.fromRun] : void 0;
|
|
739
|
+
if (line.maxExactImageHeightPx >= objectHeight) {
|
|
740
|
+
finalTypography.lineHeight = objectHeight;
|
|
741
|
+
finalTypography.ascent = objectHeight;
|
|
742
|
+
finalTypography.descent = 0;
|
|
743
|
+
return finalTypography;
|
|
744
|
+
}
|
|
745
|
+
if (soleRun && isImageRun(soleRun)) {
|
|
746
|
+
finalTypography.lineHeight = objectHeight;
|
|
747
|
+
finalTypography.ascent = objectHeight;
|
|
748
|
+
finalTypography.descent = 0;
|
|
563
749
|
} else {
|
|
564
750
|
finalTypography.lineHeight = objectHeight + buffer;
|
|
565
751
|
finalTypography.ascent = objectHeight;
|
|
@@ -589,7 +775,8 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
589
775
|
toRun: currentLine.toRun,
|
|
590
776
|
toChar: currentLine.toChar,
|
|
591
777
|
width: Math.max(0, currentLine.width - currentLine.trailingWhitespaceWidth),
|
|
592
|
-
...finalTypography
|
|
778
|
+
...finalTypography,
|
|
779
|
+
...currentLine.discretionaryHyphen ? { discretionaryHyphen: currentLine.discretionaryHyphen } : {}
|
|
593
780
|
};
|
|
594
781
|
if (currentLine.leftOffset > 0) line.leftOffset = currentLine.leftOffset;
|
|
595
782
|
if (currentLine.rightOffset > 0) line.rightOffset = currentLine.rightOffset;
|
|
@@ -598,6 +785,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
598
785
|
pendingFloatSkip = 0;
|
|
599
786
|
}
|
|
600
787
|
lines.push(line);
|
|
788
|
+
consecutiveHyphenatedLines = currentLine.discretionaryHyphen ? consecutiveHyphenatedLines + 1 : 0;
|
|
601
789
|
cumulativeHeight += finalTypography.lineHeight;
|
|
602
790
|
};
|
|
603
791
|
/**
|
|
@@ -616,11 +804,11 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
616
804
|
toChar: charIndex,
|
|
617
805
|
width: 0,
|
|
618
806
|
trailingWhitespaceWidth: 0,
|
|
619
|
-
|
|
620
|
-
nonBreakingSpaceCount: 0,
|
|
807
|
+
regularSpaceWidth: 0,
|
|
621
808
|
maxFontSize: DEFAULT_FONT_SIZE,
|
|
622
809
|
maxFontMetrics: null,
|
|
623
810
|
maxImageHeightPx: 0,
|
|
811
|
+
maxExactImageHeightPx: 0,
|
|
624
812
|
maxMathHeightPx: 0,
|
|
625
813
|
availableWidth: adjustedWidth,
|
|
626
814
|
leftOffset: floatingMargins.leftMargin,
|
|
@@ -642,7 +830,9 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
642
830
|
const metrics = getFontMetrics(style);
|
|
643
831
|
if (metrics.singleLineRatio > currentLine.maxFontMetrics.singleLineRatio) currentLine.maxFontMetrics = metrics;
|
|
644
832
|
};
|
|
833
|
+
let crossRunResume;
|
|
645
834
|
for (let runIndex = 0; runIndex < runs.length; runIndex++) {
|
|
835
|
+
if (crossRunResume && runIndex < crossRunResume.runIndex) continue;
|
|
646
836
|
const run = runs[runIndex];
|
|
647
837
|
if (isLineBreakRun(run)) {
|
|
648
838
|
currentLine.toRun = runIndex;
|
|
@@ -682,7 +872,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
682
872
|
}
|
|
683
873
|
if (isImageRun(run)) {
|
|
684
874
|
const wrapType = run.wrapType;
|
|
685
|
-
if (run
|
|
875
|
+
if (isFloatingImageRun(run)) {
|
|
686
876
|
currentLine.toRun = runIndex;
|
|
687
877
|
currentLine.toChar = 1;
|
|
688
878
|
continue;
|
|
@@ -704,6 +894,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
704
894
|
if (currentLine.width > 0 && currentLine.width + imageWidth > currentLine.availableWidth + WIDTH_TOLERANCE) startNewLine(runIndex, 0);
|
|
705
895
|
const imageFootprintPx = imageHeight + (run.distTop ?? 0) + (run.distBottom ?? 0);
|
|
706
896
|
if (imageFootprintPx > currentLine.maxImageHeightPx) currentLine.maxImageHeightPx = imageFootprintPx;
|
|
897
|
+
if (run.exactLineHeight === true && imageFootprintPx > currentLine.maxExactImageHeightPx) currentLine.maxExactImageHeightPx = imageFootprintPx;
|
|
707
898
|
currentLine.width += imageWidth;
|
|
708
899
|
currentLine.trailingWhitespaceWidth = 0;
|
|
709
900
|
currentLine.toRun = runIndex;
|
|
@@ -715,11 +906,14 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
715
906
|
const style = buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
|
|
716
907
|
updateMaxFont(style);
|
|
717
908
|
const fieldWidth = measureTextWidth(fallback, style);
|
|
718
|
-
|
|
909
|
+
const fieldSpaceWidth = compressibleSpaceWidth(fallback, style);
|
|
910
|
+
const fieldTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, lines.length === 0 ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, fieldSpaceWidth) : WIDTH_TOLERANCE;
|
|
911
|
+
if (currentLine.width > 0 && currentLine.width + fieldWidth > currentLine.availableWidth + fieldTolerance) {
|
|
719
912
|
startNewLine(runIndex, 0);
|
|
720
913
|
updateMaxFont(style);
|
|
721
914
|
}
|
|
722
915
|
currentLine.width += fieldWidth;
|
|
916
|
+
currentLine.regularSpaceWidth += fieldSpaceWidth;
|
|
723
917
|
currentLine.trailingWhitespaceWidth = 0;
|
|
724
918
|
currentLine.toRun = runIndex;
|
|
725
919
|
currentLine.toChar = 1;
|
|
@@ -750,6 +944,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
750
944
|
const textRun = run;
|
|
751
945
|
const text = textRun.text;
|
|
752
946
|
const style = runToFontStyle(textRun);
|
|
947
|
+
const breakPolicy = lineBreakPolicy(block, textRun);
|
|
753
948
|
const lineHeightStyle = cjkLineHeightStyle(textRun, style);
|
|
754
949
|
updateMaxFont(lineHeightStyle);
|
|
755
950
|
if (!text || text.length === 0) {
|
|
@@ -757,8 +952,10 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
757
952
|
currentLine.toChar = 0;
|
|
758
953
|
continue;
|
|
759
954
|
}
|
|
760
|
-
const wordBreaks = findWordBreaks(text);
|
|
761
|
-
let charIndex = 0;
|
|
955
|
+
const wordBreaks = findWordBreaks(text, breakPolicy);
|
|
956
|
+
let charIndex = crossRunResume?.runIndex === runIndex ? crossRunResume.charIndex : 0;
|
|
957
|
+
if (crossRunResume?.runIndex === runIndex) crossRunResume = void 0;
|
|
958
|
+
let activeHyphenationWord;
|
|
762
959
|
while (charIndex < text.length) {
|
|
763
960
|
let nextBreak = text.length;
|
|
764
961
|
for (const breakPoint of wordBreaks) if (breakPoint > charIndex) {
|
|
@@ -769,29 +966,103 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
769
966
|
const measuredWord = trimTrailingSpacesAndTabs(word);
|
|
770
967
|
const wordWidth = measureTextWidth(measuredWord, style);
|
|
771
968
|
const fullWordWidth = measureTextWidth(word, style);
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
const
|
|
775
|
-
|
|
969
|
+
const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, block.attrs?.overflowPunctuation);
|
|
970
|
+
const isFirstLine = lines.length === 0;
|
|
971
|
+
const regularSpaceWidth = compressibleSpaceWidth(measuredWord, style);
|
|
972
|
+
const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, isFirstLine ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, regularSpaceWidth) : WIDTH_TOLERANCE;
|
|
973
|
+
const automaticHyphenation = block.attrs?.automaticHyphenation;
|
|
974
|
+
const consecutiveLineLimit = automaticHyphenation?.consecutiveLineLimit ?? 0;
|
|
975
|
+
const mayHyphenateLine = automaticHyphenation?.enabled === true && block.attrs?.suppressAutoHyphens !== true && (consecutiveLineLimit === 0 || consecutiveHyphenatedLines < consecutiveLineLimit);
|
|
976
|
+
const isRunTail = nextBreak === text.length;
|
|
977
|
+
const crossRunWord = mayHyphenateLine && isRunTail && word.length > 0 && !isBreakChar(word.at(-1)) ? collectCrossRunWord({
|
|
978
|
+
block,
|
|
979
|
+
startRunIndex: runIndex,
|
|
980
|
+
startChar: charIndex
|
|
981
|
+
}) : void 0;
|
|
982
|
+
if (mayHyphenateLine && crossRunWord === void 0 && measuredWord.length > 0 && (activeHyphenationWord === void 0 || charIndex < activeHyphenationWord.start || charIndex >= activeHyphenationWord.end)) {
|
|
983
|
+
const fullWordEnd = charIndex + measuredWord.length;
|
|
984
|
+
const fullWordText = text.slice(charIndex, fullWordEnd);
|
|
985
|
+
activeHyphenationWord = {
|
|
986
|
+
start: charIndex,
|
|
987
|
+
end: fullWordEnd,
|
|
988
|
+
text: fullWordText,
|
|
989
|
+
breaks: findHyphenationBreaks(fullWordText, breakPolicy)
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
const overflowsCurrentLine = wordWidth > 0 && currentLine.width + wordWidth > currentLine.availableWidth + widthTolerance;
|
|
993
|
+
if (mayHyphenateLine && overflowsCurrentLine && activeHyphenationWord) {
|
|
994
|
+
const consumed = charIndex - activeHyphenationWord.start;
|
|
995
|
+
const spaceLeft = currentLine.availableWidth - currentLine.width + widthTolerance;
|
|
996
|
+
const hyphenWidth = measureTextWidth("-", style);
|
|
997
|
+
let fittingBreak;
|
|
998
|
+
for (const breakOffset of activeHyphenationWord.breaks) {
|
|
999
|
+
if (breakOffset <= consumed || breakOffset >= activeHyphenationWord.text.length) continue;
|
|
1000
|
+
if (measureTextWidth(activeHyphenationWord.text.slice(consumed, breakOffset), style) + hyphenWidth <= spaceLeft) fittingBreak = breakOffset;
|
|
1001
|
+
}
|
|
1002
|
+
if (fittingBreak !== void 0) {
|
|
1003
|
+
const absoluteBreak = activeHyphenationWord.start + fittingBreak;
|
|
1004
|
+
const prefix = text.slice(charIndex, absoluteBreak);
|
|
1005
|
+
currentLine.width += measureTextWidth(prefix, style) + hyphenWidth;
|
|
1006
|
+
currentLine.trailingWhitespaceWidth = 0;
|
|
1007
|
+
currentLine.toRun = runIndex;
|
|
1008
|
+
currentLine.toChar = absoluteBreak;
|
|
1009
|
+
currentLine.discretionaryHyphen = { runIndex };
|
|
1010
|
+
startNewLine(runIndex, absoluteBreak);
|
|
1011
|
+
updateMaxFont(lineHeightStyle);
|
|
1012
|
+
charIndex = absoluteBreak;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (crossRunWord && currentLine.width + crossRunWord.width > currentLine.availableWidth + widthTolerance) {
|
|
1017
|
+
const spaceLeft = currentLine.availableWidth - currentLine.width + widthTolerance;
|
|
1018
|
+
let fittingPrefix;
|
|
1019
|
+
for (let breakIndex = crossRunWord.breaks.length - 1; breakIndex >= 0; breakIndex -= 1) {
|
|
1020
|
+
const breakOffset = crossRunWord.breaks[breakIndex];
|
|
1021
|
+
if (breakOffset === void 0) continue;
|
|
1022
|
+
if (breakOffset <= 0 || breakOffset >= crossRunWord.text.length) continue;
|
|
1023
|
+
const prefix = measureCrossRunPrefix(crossRunWord, breakOffset);
|
|
1024
|
+
if (prefix && prefix.width + measureTextWidth("-", prefix.hyphenStyle) <= spaceLeft) {
|
|
1025
|
+
fittingPrefix = prefix;
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (fittingPrefix) {
|
|
1030
|
+
for (const segment of fittingPrefix.segments) updateMaxFont(cjkLineHeightStyle(segment.run, segment.style));
|
|
1031
|
+
currentLine.width += fittingPrefix.width + measureTextWidth("-", fittingPrefix.hyphenStyle);
|
|
1032
|
+
currentLine.trailingWhitespaceWidth = 0;
|
|
1033
|
+
currentLine.toRun = fittingPrefix.endRunIndex;
|
|
1034
|
+
currentLine.toChar = fittingPrefix.endChar;
|
|
1035
|
+
currentLine.discretionaryHyphen = { runIndex: fittingPrefix.endRunIndex };
|
|
1036
|
+
startNewLine(fittingPrefix.endRunIndex, fittingPrefix.endChar);
|
|
1037
|
+
if (fittingPrefix.endRunIndex === runIndex) {
|
|
1038
|
+
updateMaxFont(lineHeightStyle);
|
|
1039
|
+
charIndex = fittingPrefix.endChar;
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
crossRunResume = {
|
|
1043
|
+
runIndex: fittingPrefix.endRunIndex,
|
|
1044
|
+
charIndex: fittingPrefix.endChar
|
|
1045
|
+
};
|
|
1046
|
+
charIndex = text.length;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (wordWidth - hangingPunctuationWidth > currentLine.availableWidth + widthTolerance) {
|
|
776
1051
|
let chunkStart = 0;
|
|
777
1052
|
while (chunkStart < measuredWord.length) {
|
|
778
1053
|
const spaceLeft = currentLine.availableWidth - currentLine.width + WIDTH_TOLERANCE;
|
|
779
|
-
let bestEnd = findMaxFittingLength(measuredWord.slice(chunkStart), style, spaceLeft);
|
|
1054
|
+
let bestEnd = findMaxFittingLength(measuredWord.slice(chunkStart), style, spaceLeft, currentLine.width === 0, breakPolicy);
|
|
780
1055
|
if (bestEnd === 0) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
continue;
|
|
785
|
-
}
|
|
786
|
-
bestEnd = 1;
|
|
1056
|
+
startNewLine(runIndex, charIndex + chunkStart);
|
|
1057
|
+
updateMaxFont(lineHeightStyle);
|
|
1058
|
+
continue;
|
|
787
1059
|
}
|
|
788
1060
|
const chunkEnd = chunkStart + bestEnd;
|
|
789
1061
|
const chunk = measuredWord.slice(chunkStart, chunkEnd);
|
|
790
1062
|
const chunkWidth = measureTextWidth(chunk, style);
|
|
791
1063
|
currentLine.width += chunkWidth;
|
|
792
1064
|
currentLine.trailingWhitespaceWidth = chunkWidth - measureTextWidth(trimTrailingSpacesAndTabs(chunk), style);
|
|
793
|
-
currentLine.
|
|
794
|
-
currentLine.nonBreakingSpaceCount += chunk.split("\xA0").length - 1;
|
|
1065
|
+
currentLine.regularSpaceWidth += compressibleSpaceWidth(chunk, style);
|
|
795
1066
|
currentLine.toRun = runIndex;
|
|
796
1067
|
currentLine.toChar = charIndex + chunkEnd;
|
|
797
1068
|
chunkStart = chunkEnd;
|
|
@@ -803,23 +1074,24 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
803
1074
|
const trailingWhitespaceWidth = fullWordWidth - wordWidth;
|
|
804
1075
|
currentLine.width += trailingWhitespaceWidth;
|
|
805
1076
|
currentLine.trailingWhitespaceWidth = trailingWhitespaceWidth;
|
|
806
|
-
currentLine.
|
|
807
|
-
currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
|
|
1077
|
+
currentLine.regularSpaceWidth += compressibleSpaceWidth(word, style);
|
|
808
1078
|
currentLine.toChar = nextBreak;
|
|
809
1079
|
charIndex = nextBreak;
|
|
810
1080
|
continue;
|
|
811
1081
|
}
|
|
812
|
-
const
|
|
1082
|
+
const wordTail = word.at(-1);
|
|
1083
|
+
const protectedGlueWidth = protectedCrossRunGlueWidths[runIndex] ?? 0;
|
|
1084
|
+
const rawGlueWidth = isRunTail && wordTail !== void 0 && (!isBreakChar(wordTail) || protectedGlueWidth > 0) ? Math.max(trailingGlueWidths[runIndex] ?? 0, protectedGlueWidth) : 0;
|
|
813
1085
|
const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + widthTolerance ? rawGlueWidth : 0;
|
|
814
|
-
|
|
1086
|
+
const hangingAllowance = glueWidth === 0 ? hangingPunctuationWidth : 0;
|
|
1087
|
+
if (wordWidth > 0 && currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + widthTolerance + hangingAllowance) {
|
|
815
1088
|
startNewLine(runIndex, charIndex);
|
|
816
1089
|
updateMaxFont(lineHeightStyle);
|
|
817
1090
|
}
|
|
818
1091
|
currentLine.width += fullWordWidth;
|
|
819
1092
|
const wordTrailingWhitespaceWidth = fullWordWidth - wordWidth;
|
|
820
1093
|
currentLine.trailingWhitespaceWidth = wordWidth === 0 ? currentLine.trailingWhitespaceWidth + wordTrailingWhitespaceWidth : wordTrailingWhitespaceWidth;
|
|
821
|
-
currentLine.
|
|
822
|
-
currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
|
|
1094
|
+
currentLine.regularSpaceWidth += compressibleSpaceWidth(word, style);
|
|
823
1095
|
currentLine.toRun = runIndex;
|
|
824
1096
|
currentLine.toChar = nextBreak;
|
|
825
1097
|
charIndex = nextBreak;
|
|
@@ -27,7 +27,8 @@ type FontStyle = {
|
|
|
27
27
|
letterSpacing?: number;
|
|
28
28
|
textTransform?: "uppercase";
|
|
29
29
|
fontVariant?: "small-caps";
|
|
30
|
-
horizontalScale?: number;
|
|
30
|
+
horizontalScale?: number; /** Enable pair kerning for runs whose authored threshold is met. */
|
|
31
|
+
kerning?: boolean;
|
|
31
32
|
};
|
|
32
33
|
/**
|
|
33
34
|
* Typography metrics for a font
|