@stll/folio-core 0.47.7 → 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/fontReadiness.js +3 -3
- package/dist/controller/hyphenationReadiness.d.ts +19 -0
- package/dist/controller/hyphenationReadiness.js +47 -0
- package/dist/controller/layoutPipeline.d.ts +3 -0
- package/dist/controller/layoutPipeline.js +70 -18
- package/dist/display-list/build/buildDisplayList.js +39 -12
- package/dist/display-list/build/pageFurniture.d.ts +7 -1
- package/dist/display-list/build/pageFurniture.js +21 -8
- package/dist/display-list/build/paragraphPrimitives.js +68 -11
- package/dist/display-list/build/tablePrimitives.js +10 -6
- 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/attributeRemainder.js +10 -2
- package/dist/docx/blockContentParser.d.ts +2 -0
- package/dist/docx/blockContentParser.js +79 -64
- package/dist/docx/bulletMarkers.d.ts +18 -2
- package/dist/docx/bulletMarkers.js +27 -2
- package/dist/docx/commentParser.d.ts +2 -1
- package/dist/docx/commentParser.js +57 -45
- package/dist/docx/containerChildren.d.ts +21 -8
- package/dist/docx/containerChildren.js +39 -9
- package/dist/docx/documentParser.d.ts +5 -1
- package/dist/docx/documentParser.js +8 -5
- package/dist/docx/documentSectionFacts.d.ts +22 -0
- package/dist/docx/documentSectionFacts.js +43 -0
- 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/fieldParser.d.ts +8 -1
- package/dist/docx/fieldParser.js +47 -3
- package/dist/docx/fontTableParser.js +34 -27
- package/dist/docx/footnoteParser.d.ts +3 -2
- package/dist/docx/footnoteParser.js +15 -12
- package/dist/docx/groupDrawingParser.d.ts +9 -2
- package/dist/docx/groupDrawingParser.js +125 -37
- package/dist/docx/headerFooterParser.d.ts +7 -2
- package/dist/docx/headerFooterParser.js +15 -5
- package/dist/docx/hyperlinkParser.d.ts +48 -36
- package/dist/docx/hyperlinkParser.js +97 -82
- package/dist/docx/numberingParser.js +108 -84
- 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.d.ts +5 -1
- package/dist/docx/paragraphParser.js +468 -365
- package/dist/docx/paragraphProperties.js +123 -109
- package/dist/docx/paragraphPropertySource.d.ts +6 -1
- package/dist/docx/paragraphPropertySource.js +9 -1
- package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
- package/dist/docx/paragraphTextBoxEnrichment.js +143 -10
- package/dist/docx/parser.d.ts +7 -1
- package/dist/docx/parser.js +33 -16
- package/dist/docx/previewBudget.d.ts +55 -33
- package/dist/docx/previewBudget.js +59 -65
- package/dist/docx/rezip.js +41 -58
- package/dist/docx/runParser.d.ts +4 -1
- package/dist/docx/runParser.js +182 -184
- package/dist/docx/sdtProperties.js +87 -84
- package/dist/docx/sectionParser.js +169 -160
- 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 +47 -42
- package/dist/docx/serializer/runSerializer.d.ts +5 -1
- package/dist/docx/serializer/runSerializer.js +4 -3
- package/dist/docx/serializer/settingsSerializer.js +19 -1
- package/dist/docx/settingsParser.js +8 -0
- package/dist/docx/shapeAlternateContent.d.ts +25 -0
- package/dist/docx/shapeAlternateContent.js +52 -0
- package/dist/docx/streamingXmlParser.d.ts +6 -2
- package/dist/docx/streamingXmlParser.js +12 -7
- package/dist/docx/styleParser.js +34 -8
- package/dist/docx/tableParser.d.ts +21 -12
- package/dist/docx/tableParser.js +518 -441
- package/dist/docx/textBoxParser.d.ts +18 -5
- package/dist/docx/textBoxParser.js +12 -6
- package/dist/docx/verbatimCapture.d.ts +10 -8
- package/dist/docx/verbatimCapture.js +45 -2
- package/dist/docx/vmlImageParser.d.ts +15 -13
- package/dist/docx/vmlImageParser.js +75 -60
- package/dist/docx/vmlPreview.d.ts +3 -3
- package/dist/docx/vmlPreview.js +3 -4
- package/dist/docx/xmlNamespaceContext.d.ts +9 -0
- package/dist/docx/xmlNamespaceContext.js +54 -0
- package/dist/docx/xmlParser.d.ts +6 -3
- package/dist/docx/xmlParser.js +11 -41
- package/dist/docx/xmlResourceLimits.d.ts +2 -1
- package/dist/docx/xmlResourceLimits.js +4 -1
- package/dist/fonts/headlessMeasure.js +0 -0
- package/dist/headless-layout.d.ts +11 -2
- package/dist/headless-layout.js +153 -107
- package/dist/internal/headlessRevisionResolution.js +50 -13
- package/dist/internal/paragraphFormattingSerialization.js +3 -3
- package/dist/layout-bridge/convert/endnoteLayout.d.ts +65 -0
- package/dist/layout-bridge/convert/endnoteLayout.js +227 -0
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +19 -3
- package/dist/layout-bridge/convert/footnoteLayout.js +40 -31
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -7
- package/dist/layout-bridge/convert/headerFooterLayout.js +5 -20
- package/dist/layout-bridge/convert/paragraphMarkFormatting.d.ts +10 -0
- package/dist/layout-bridge/convert/paragraphMarkFormatting.js +59 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +29 -1
- package/dist/layout-bridge/convert/toFlowBlocks.js +284 -39
- package/dist/layout-bridge/engine/hitTest.js +5 -3
- package/dist/layout-bridge/engine/selectionRects.js +5 -3
- package/dist/layout-engine/anchorLayoutInCellCompatibility.d.ts +10 -0
- package/dist/layout-engine/anchorLayoutInCellCompatibility.js +22 -0
- package/dist/layout-engine/index.d.ts +2 -2
- package/dist/layout-engine/index.js +102 -14
- package/dist/layout-engine/justificationCompatibility.d.ts +1 -1
- package/dist/layout-engine/justificationCompatibility.js +9 -5
- package/dist/layout-engine/keep-together.d.ts +15 -6
- package/dist/layout-engine/keep-together.js +43 -16
- package/dist/layout-engine/layoutInstrumentation.d.ts +8 -2
- package/dist/layout-engine/layoutInstrumentation.js +8 -1
- package/dist/layout-engine/measure/advanceComposition.js +50 -7
- package/dist/layout-engine/measure/hyphenationDictionaries.d.ts +74 -0
- package/dist/layout-engine/measure/hyphenationDictionaries.js +138 -0
- package/dist/layout-engine/measure/hyphenationPreload.d.ts +11 -0
- package/dist/layout-engine/measure/hyphenationPreload.js +24 -0
- package/dist/layout-engine/measure/lineBreakProvider.d.ts +12 -2
- package/dist/layout-engine/measure/lineBreakProvider.js +30 -30
- package/dist/layout-engine/measure/lineBreaks.d.ts +6 -1
- package/dist/layout-engine/measure/lineBreaks.js +10 -3
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +9 -6
- package/dist/layout-engine/measure/listMarkerWidth.js +20 -22
- package/dist/layout-engine/measure/measureBlocks.js +46 -10
- package/dist/layout-engine/measure/measureContainer.js +51 -8
- package/dist/layout-engine/measure/measureHelpers.d.ts +15 -3
- package/dist/layout-engine/measure/measureHelpers.js +34 -8
- package/dist/layout-engine/measure/measureParagraph.js +143 -101
- package/dist/layout-engine/measure/measureTypes.d.ts +5 -0
- package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
- package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
- package/dist/layout-engine/measure/tabCalculator.d.ts +2 -0
- package/dist/layout-engine/measure/tabCalculator.js +2 -1
- package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
- package/dist/layout-engine/measure/tableCellFloating.js +51 -19
- package/dist/layout-engine/measure/tableCellGrid.d.ts +28 -3
- package/dist/layout-engine/measure/tableCellGrid.js +45 -5
- package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +2 -1
- package/dist/layout-engine/noteAreaFlow.d.ts +19 -0
- package/dist/layout-engine/noteAreaFlow.js +83 -0
- package/dist/layout-engine/paginator.d.ts +12 -1
- package/dist/layout-engine/paginator.js +18 -2
- 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 +25 -2
- package/dist/layout-engine/types.d.ts +108 -3
- package/dist/layout-engine/types.js +96 -1
- package/dist/layout-painter/index.d.ts +3 -0
- package/dist/layout-painter/renderPage.js +32 -83
- package/dist/layout-painter/renderParagraph.d.ts +6 -1
- package/dist/layout-painter/renderParagraph.js +75 -26
- package/dist/layout-painter/renderTable.js +26 -13
- 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.d.ts +9 -9
- package/dist/prosemirror/attrs/index.js +133 -11
- 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 +54 -23
- package/dist/prosemirror/conversion/markInterner.d.ts +21 -0
- package/dist/prosemirror/conversion/markInterner.js +90 -0
- package/dist/prosemirror/conversion/toProseDoc.js +117 -58
- package/dist/prosemirror/extensions/core/DocExtension.js +2 -1
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
- package/dist/prosemirror/extensions/marks/markUtils.d.ts +6 -2
- package/dist/prosemirror/extensions/marks/markUtils.js +39 -51
- 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 +5 -1
- package/dist/prosemirror/listMarker.d.ts +1 -0
- package/dist/prosemirror/listMarker.js +4 -2
- package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
- package/dist/prosemirror/listRenderingAttrs.js +3 -0
- 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 +50 -6
- package/dist/prosemirror/textBoxHostParagraph.d.ts +13 -0
- package/dist/prosemirror/textBoxHostParagraph.js +21 -0
- package/dist/prosemirror/validation.js +91 -80
- package/dist/types/content.d.ts +2 -2
- package/dist/utils/createDocument.js +10 -1
- package/dist/utils/scriptSegments.d.ts +33 -4
- package/dist/utils/scriptSegments.js +39 -6
- package/dist/utils/textFormattingMerge.d.ts +17 -3
- package/dist/utils/textFormattingMerge.js +18 -12
- package/dist/utils/trailingText.d.ts +22 -0
- package/dist/utils/trailingText.js +32 -0
- package/package.json +2 -2
- package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
- package/dist/layout-engine/justifiedLineFit.js +0 -6
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
|
|
2
|
-
import { SCRIPT_CLASS, hasComplexScript, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
|
|
2
|
+
import { SCRIPT_CLASS, hasComplexScript, hasEastAsiaSlotText, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
|
|
3
3
|
import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./complexScriptFormatting.js";
|
|
4
|
+
import "./measureHelpers.js";
|
|
5
|
+
import { SMALL_CAPS_SCALE, smallCapsMask, smallCapsSegments } from "./smallCapsCasing.js";
|
|
4
6
|
//#region src/layout-engine/measure/advanceComposition.ts
|
|
5
7
|
/**
|
|
6
8
|
* The arithmetic that turns raw glyph advances into measured widths.
|
|
@@ -48,9 +50,38 @@ const glyphAdvanceStyle = (style) => {
|
|
|
48
50
|
if (style.kerning !== void 0) result.kerning = style.kerning;
|
|
49
51
|
return result;
|
|
50
52
|
};
|
|
51
|
-
const needsPerScriptFonts = (style, text) => style.eastAsiaFontFamily !== void 0 &&
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const needsPerScriptFonts = (style, text) => style.eastAsiaFontFamily !== void 0 && hasEastAsiaSlotText(text, style.eastAsiaHint) || hasComplexScriptFormatting(style) && hasComplexScript(text);
|
|
54
|
+
/**
|
|
55
|
+
* Width of a `w:smallCaps` string: its lowercase letters (and the whitespace
|
|
56
|
+
* that follows them — see smallCapsCasing.ts) draw at `SMALL_CAPS_SCALE` of
|
|
57
|
+
* the style's size, everything else at full size. A small segment is measured
|
|
58
|
+
* at that literal, smaller size — not the full-size advance scaled by
|
|
59
|
+
* arithmetic — mirroring {@link canvasMeasureTextWidth}'s
|
|
60
|
+
* `measureSmallCapsWidth` so the two backends agree; each segment still
|
|
61
|
+
* resolves its own per-script font, the same as the plain (non-small-caps)
|
|
62
|
+
* path below.
|
|
63
|
+
*/
|
|
64
|
+
const smallCapsGlyphWidth = (text, style, advanceOf) => {
|
|
65
|
+
const segmentStyle = { ...style };
|
|
66
|
+
delete segmentStyle.fontVariant;
|
|
67
|
+
delete segmentStyle.letterSpacing;
|
|
68
|
+
delete segmentStyle.horizontalScale;
|
|
69
|
+
const smallSegmentStyle = {
|
|
70
|
+
...segmentStyle,
|
|
71
|
+
fontSize: (segmentStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
|
|
72
|
+
};
|
|
73
|
+
let total = 0;
|
|
74
|
+
for (const segment of smallCapsSegments(text)) {
|
|
75
|
+
const activeStyle = segment.small ? smallSegmentStyle : segmentStyle;
|
|
76
|
+
total += needsPerScriptFonts(activeStyle, segment.text) ? segmentByScript(segment.text, activeStyle.eastAsiaHint).reduce((sum, scriptSegment) => sum + advanceOf(scriptSegment.text, glyphAdvanceStyle(scriptStyle(activeStyle, scriptSegment.script))), 0) : advanceOf(segment.text, activeStyle);
|
|
77
|
+
}
|
|
78
|
+
return total;
|
|
79
|
+
};
|
|
80
|
+
/** The un-spaced, un-scaled glyph width `composeTextWidth` spaces and scales. */
|
|
81
|
+
const glyphWidthOf = ({ source, style, transformed, letterSpacing, advanceOf }) => {
|
|
82
|
+
if (!source.forceComplexScript && style.fontVariant === "small-caps") return smallCapsGlyphWidth(transformed, style, advanceOf);
|
|
83
|
+
if (!source.forceComplexScript && !letterSpacing && needsPerScriptFonts(style, transformed)) return segmentByScript(transformed, style.eastAsiaHint).reduce((sum, segment) => sum + advanceOf(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script))), 0);
|
|
84
|
+
return advanceOf(transformed, style);
|
|
54
85
|
};
|
|
55
86
|
/**
|
|
56
87
|
* Width of a string: the advance width used for line breaking, never the
|
|
@@ -62,7 +93,13 @@ const composeTextWidth = ({ text, style: source, advanceOf }) => {
|
|
|
62
93
|
const transformed = applyMeasurementTextTransform(text, style);
|
|
63
94
|
const letterSpacing = style.letterSpacing ?? 0;
|
|
64
95
|
const horizontalScale = getHorizontalScaleFactor(style.horizontalScale);
|
|
65
|
-
const glyphWidth =
|
|
96
|
+
const glyphWidth = glyphWidthOf({
|
|
97
|
+
source,
|
|
98
|
+
style,
|
|
99
|
+
transformed,
|
|
100
|
+
letterSpacing,
|
|
101
|
+
advanceOf
|
|
102
|
+
});
|
|
66
103
|
const codePoints = countCodePoints(transformed);
|
|
67
104
|
return (letterSpacing && codePoints > 1 ? glyphWidth + letterSpacing * (codePoints - 1) : glyphWidth) * horizontalScale;
|
|
68
105
|
};
|
|
@@ -95,10 +132,16 @@ const composeRunMeasurement = ({ text, style: source, metrics, advanceOf, advanc
|
|
|
95
132
|
perScript
|
|
96
133
|
});
|
|
97
134
|
let codePointIndex = 0;
|
|
135
|
+
const smallCapsAt = style.fontVariant === "small-caps" ? smallCapsMask(text) : void 0;
|
|
98
136
|
for (const char of text) {
|
|
99
137
|
const cp = char.codePointAt(0);
|
|
100
|
-
const
|
|
101
|
-
|
|
138
|
+
const isSmallCap = smallCapsAt?.[codePointIndex] ?? false;
|
|
139
|
+
const scriptedStyle = perScript ? scriptStyle(style, scriptClassOf(cp, style.eastAsiaHint)) : style;
|
|
140
|
+
const charStyle = isSmallCap ? {
|
|
141
|
+
...scriptedStyle,
|
|
142
|
+
fontSize: (scriptedStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
|
|
143
|
+
} : scriptedStyle;
|
|
144
|
+
let charWidth = shaped.get(codePointIndex) ?? advanceOf(isSmallCap ? char.toLocaleUpperCase() : applyMeasurementTextTransform(char, style), charStyle);
|
|
102
145
|
if (letterSpacing && offset + char.length < text.length) charWidth += letterSpacing;
|
|
103
146
|
charWidth *= scale;
|
|
104
147
|
charWidths.push(charWidth);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/layout-engine/measure/hyphenationDictionaries.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* On-demand hyphenation dictionaries for automatic hyphenation.
|
|
4
|
+
*
|
|
5
|
+
* Each `hyphen` pattern set is tens to hundreds of kilobytes of source whose
|
|
6
|
+
* evaluation builds a pattern trie, and most documents never enable
|
|
7
|
+
* `w:autoHyphenation`. A dictionary is therefore imported only once measurement
|
|
8
|
+
* asks to hyphenate a word in its language. Measurement is synchronous, so the
|
|
9
|
+
* first request starts the import and hyphenates nothing. A layout run wrapped
|
|
10
|
+
* in {@link collectRequestedHyphenationDictionaries} learns which dictionaries
|
|
11
|
+
* it lacked, so its own editor can re-run layout on load or report the failure
|
|
12
|
+
* (`controller/hyphenationReadiness`). Hosts that must lay out correctly on the
|
|
13
|
+
* first pass (headless layout, tests) await `preloadHyphenationDictionaries`
|
|
14
|
+
* (`./hyphenationPreload`) before measuring.
|
|
15
|
+
*
|
|
16
|
+
* This module sits on every editor's import graph, so it speaks in its own
|
|
17
|
+
* settled-state union rather than `Result`, keeping the `Result` API out of
|
|
18
|
+
* adapters' type checks; the preload boundary converts.
|
|
19
|
+
*/
|
|
20
|
+
type HyphenateWord = (text: string) => string;
|
|
21
|
+
declare const HYPHENATION_DICTIONARY_IDS: readonly ["cs", "en-gb", "en-us", "sk"];
|
|
22
|
+
type HyphenationDictionaryId = (typeof HYPHENATION_DICTIONARY_IDS)[number];
|
|
23
|
+
type HyphenationDictionaryLoaders = Readonly<Record<HyphenationDictionaryId, () => Promise<HyphenateWord>>>;
|
|
24
|
+
declare const HyphenationDictionaryError_base: import("better-result").TaggedErrorClass<"HyphenationDictionaryError">;
|
|
25
|
+
declare class HyphenationDictionaryError extends HyphenationDictionaryError_base<{
|
|
26
|
+
message: string;
|
|
27
|
+
dictionary: HyphenationDictionaryId;
|
|
28
|
+
cause: unknown;
|
|
29
|
+
}> {}
|
|
30
|
+
type SettledHyphenationDictionary = Readonly<{
|
|
31
|
+
status: "loaded";
|
|
32
|
+
hyphenate: HyphenateWord;
|
|
33
|
+
}> | Readonly<{
|
|
34
|
+
status: "failed";
|
|
35
|
+
error: HyphenationDictionaryError;
|
|
36
|
+
}>;
|
|
37
|
+
type HyphenationDictionaryState = Readonly<{
|
|
38
|
+
status: "unloaded";
|
|
39
|
+
}> | Readonly<{
|
|
40
|
+
status: "loading";
|
|
41
|
+
promise: Promise<SettledHyphenationDictionary>;
|
|
42
|
+
}> | SettledHyphenationDictionary;
|
|
43
|
+
/** Compare before and after a measurement to tell whether it lacked a dictionary. */
|
|
44
|
+
declare const getHyphenationDictionaryMisses: () => number;
|
|
45
|
+
/** The dictionary that hyphenates text in `locale`, if one is bundled. */
|
|
46
|
+
declare const hyphenationDictionaryFor: (locale: string | undefined) => HyphenationDictionaryId | undefined;
|
|
47
|
+
/** Changes whenever a dictionary finishes loading, so measurement caches cannot go stale. */
|
|
48
|
+
declare const getHyphenationDictionaryGeneration: () => number;
|
|
49
|
+
/** Load `dictionary` once; later calls share the load or its settled outcome. */
|
|
50
|
+
declare const requestHyphenationDictionary: (dictionary: HyphenationDictionaryId) => Promise<SettledHyphenationDictionary>;
|
|
51
|
+
/**
|
|
52
|
+
* The loaded hyphenator for `dictionary`, or `undefined` while it is not
|
|
53
|
+
* available. An unloaded dictionary starts loading, and a missing one is
|
|
54
|
+
* recorded against the enclosing {@link collectRequestedHyphenationDictionaries}
|
|
55
|
+
* run; the caller hyphenates nothing now.
|
|
56
|
+
*/
|
|
57
|
+
declare const hyphenatorOrRequest: (dictionary: HyphenationDictionaryId) => HyphenateWord | undefined;
|
|
58
|
+
type CollectedHyphenationDictionaries<T> = {
|
|
59
|
+
result: T;
|
|
60
|
+
/** Dictionaries the run needed and did not have: loading or failed. */
|
|
61
|
+
missing: ReadonlySet<HyphenationDictionaryId>;
|
|
62
|
+
};
|
|
63
|
+
/** Run a synchronous layout pass and report the dictionaries it lacked. */
|
|
64
|
+
declare const collectRequestedHyphenationDictionaries: <T>(run: () => T) => CollectedHyphenationDictionaries<T>;
|
|
65
|
+
/**
|
|
66
|
+
* Test seam: forget every dictionary so a test observes the unloaded path, and
|
|
67
|
+
* optionally load through `loaders` (for example, ones that fail) until the
|
|
68
|
+
* next reset.
|
|
69
|
+
*/
|
|
70
|
+
declare const resetHyphenationDictionaries: (loaders?: HyphenationDictionaryLoaders) => void;
|
|
71
|
+
/** Test seam: which dictionaries have been requested or loaded. */
|
|
72
|
+
declare const hyphenationDictionaryStatus: (dictionary: HyphenationDictionaryId) => HyphenationDictionaryState["status"];
|
|
73
|
+
//#endregion
|
|
74
|
+
export { CollectedHyphenationDictionaries, HyphenateWord, HyphenationDictionaryError, HyphenationDictionaryId, HyphenationDictionaryLoaders, SettledHyphenationDictionary, collectRequestedHyphenationDictionaries, getHyphenationDictionaryGeneration, getHyphenationDictionaryMisses, hyphenationDictionaryFor, hyphenationDictionaryStatus, hyphenatorOrRequest, requestHyphenationDictionary, resetHyphenationDictionaries };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { recordHyphenationDictionaryError } from "../layoutInstrumentation.js";
|
|
2
|
+
import { TaggedError } from "better-result";
|
|
3
|
+
//#region src/layout-engine/measure/hyphenationDictionaries.ts
|
|
4
|
+
/**
|
|
5
|
+
* On-demand hyphenation dictionaries for automatic hyphenation.
|
|
6
|
+
*
|
|
7
|
+
* Each `hyphen` pattern set is tens to hundreds of kilobytes of source whose
|
|
8
|
+
* evaluation builds a pattern trie, and most documents never enable
|
|
9
|
+
* `w:autoHyphenation`. A dictionary is therefore imported only once measurement
|
|
10
|
+
* asks to hyphenate a word in its language. Measurement is synchronous, so the
|
|
11
|
+
* first request starts the import and hyphenates nothing. A layout run wrapped
|
|
12
|
+
* in {@link collectRequestedHyphenationDictionaries} learns which dictionaries
|
|
13
|
+
* it lacked, so its own editor can re-run layout on load or report the failure
|
|
14
|
+
* (`controller/hyphenationReadiness`). Hosts that must lay out correctly on the
|
|
15
|
+
* first pass (headless layout, tests) await `preloadHyphenationDictionaries`
|
|
16
|
+
* (`./hyphenationPreload`) before measuring.
|
|
17
|
+
*
|
|
18
|
+
* This module sits on every editor's import graph, so it speaks in its own
|
|
19
|
+
* settled-state union rather than `Result`, keeping the `Result` API out of
|
|
20
|
+
* adapters' type checks; the preload boundary converts.
|
|
21
|
+
*/
|
|
22
|
+
const HYPHENATION_DICTIONARY_IDS = [
|
|
23
|
+
"cs",
|
|
24
|
+
"en-gb",
|
|
25
|
+
"en-us",
|
|
26
|
+
"sk"
|
|
27
|
+
];
|
|
28
|
+
const HYPHENATION_DICTIONARY_LOADERS = {
|
|
29
|
+
cs: async () => (await import("hyphen/cs/index.js")).default.hyphenateSync,
|
|
30
|
+
"en-gb": async () => (await import("hyphen/en-gb/index.js")).default.hyphenateSync,
|
|
31
|
+
"en-us": async () => (await import("hyphen/en-us/index.js")).default.hyphenateSync,
|
|
32
|
+
sk: async () => (await import("hyphen/sk/index.js")).default.hyphenateSync
|
|
33
|
+
};
|
|
34
|
+
let dictionaryLoaders = HYPHENATION_DICTIONARY_LOADERS;
|
|
35
|
+
var HyphenationDictionaryError = class extends TaggedError("HyphenationDictionaryError") {};
|
|
36
|
+
const createInitialStates = () => ({
|
|
37
|
+
cs: { status: "unloaded" },
|
|
38
|
+
"en-gb": { status: "unloaded" },
|
|
39
|
+
"en-us": { status: "unloaded" },
|
|
40
|
+
sk: { status: "unloaded" }
|
|
41
|
+
});
|
|
42
|
+
let dictionaryStates = createInitialStates();
|
|
43
|
+
let dictionaryGeneration = 0;
|
|
44
|
+
let activeRequests;
|
|
45
|
+
let dictionaryMisses = 0;
|
|
46
|
+
/** Compare before and after a measurement to tell whether it lacked a dictionary. */
|
|
47
|
+
const getHyphenationDictionaryMisses = () => dictionaryMisses;
|
|
48
|
+
/** The dictionary that hyphenates text in `locale`, if one is bundled. */
|
|
49
|
+
const hyphenationDictionaryFor = (locale) => {
|
|
50
|
+
const normalized = locale?.trim().replaceAll("_", "-").toLowerCase();
|
|
51
|
+
if (!normalized) return;
|
|
52
|
+
return HYPHENATION_DICTIONARY_IDS.find((id) => normalized === id || normalized.startsWith(`${id}-`));
|
|
53
|
+
};
|
|
54
|
+
/** Changes whenever a dictionary finishes loading, so measurement caches cannot go stale. */
|
|
55
|
+
const getHyphenationDictionaryGeneration = () => dictionaryGeneration;
|
|
56
|
+
const importDictionary = (dictionary) => dictionaryLoaders[dictionary]().then((hyphenate) => ({
|
|
57
|
+
status: "loaded",
|
|
58
|
+
hyphenate
|
|
59
|
+
}), (cause) => ({
|
|
60
|
+
status: "failed",
|
|
61
|
+
error: new HyphenationDictionaryError({
|
|
62
|
+
message: `The ${dictionary} hyphenation dictionary could not be loaded.`,
|
|
63
|
+
dictionary,
|
|
64
|
+
cause
|
|
65
|
+
})
|
|
66
|
+
}));
|
|
67
|
+
const startLoad = (dictionary) => {
|
|
68
|
+
const promise = importDictionary(dictionary).then((settled) => {
|
|
69
|
+
if (dictionaryStates[dictionary].status !== "loading") return settled;
|
|
70
|
+
dictionaryStates[dictionary] = settled;
|
|
71
|
+
switch (settled.status) {
|
|
72
|
+
case "failed":
|
|
73
|
+
recordHyphenationDictionaryError(dictionary, settled.error);
|
|
74
|
+
return settled;
|
|
75
|
+
case "loaded":
|
|
76
|
+
dictionaryGeneration += 1;
|
|
77
|
+
return settled;
|
|
78
|
+
default: return settled;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
dictionaryStates[dictionary] = {
|
|
82
|
+
status: "loading",
|
|
83
|
+
promise
|
|
84
|
+
};
|
|
85
|
+
return promise;
|
|
86
|
+
};
|
|
87
|
+
/** Load `dictionary` once; later calls share the load or its settled outcome. */
|
|
88
|
+
const requestHyphenationDictionary = (dictionary) => {
|
|
89
|
+
const state = dictionaryStates[dictionary];
|
|
90
|
+
switch (state.status) {
|
|
91
|
+
case "unloaded": return startLoad(dictionary);
|
|
92
|
+
case "loading": return state.promise;
|
|
93
|
+
case "loaded":
|
|
94
|
+
case "failed": return Promise.resolve(state);
|
|
95
|
+
default: return state;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* The loaded hyphenator for `dictionary`, or `undefined` while it is not
|
|
100
|
+
* available. An unloaded dictionary starts loading, and a missing one is
|
|
101
|
+
* recorded against the enclosing {@link collectRequestedHyphenationDictionaries}
|
|
102
|
+
* run; the caller hyphenates nothing now.
|
|
103
|
+
*/
|
|
104
|
+
const hyphenatorOrRequest = (dictionary) => {
|
|
105
|
+
const state = dictionaryStates[dictionary];
|
|
106
|
+
if (state.status === "loaded") return state.hyphenate;
|
|
107
|
+
dictionaryMisses += 1;
|
|
108
|
+
activeRequests?.add(dictionary);
|
|
109
|
+
if (state.status === "unloaded") startLoad(dictionary);
|
|
110
|
+
};
|
|
111
|
+
/** Run a synchronous layout pass and report the dictionaries it lacked. */
|
|
112
|
+
const collectRequestedHyphenationDictionaries = (run) => {
|
|
113
|
+
const outer = activeRequests;
|
|
114
|
+
const missing = /* @__PURE__ */ new Set();
|
|
115
|
+
activeRequests = missing;
|
|
116
|
+
try {
|
|
117
|
+
return {
|
|
118
|
+
result: run(),
|
|
119
|
+
missing
|
|
120
|
+
};
|
|
121
|
+
} finally {
|
|
122
|
+
activeRequests = outer;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Test seam: forget every dictionary so a test observes the unloaded path, and
|
|
127
|
+
* optionally load through `loaders` (for example, ones that fail) until the
|
|
128
|
+
* next reset.
|
|
129
|
+
*/
|
|
130
|
+
const resetHyphenationDictionaries = (loaders = HYPHENATION_DICTIONARY_LOADERS) => {
|
|
131
|
+
dictionaryLoaders = loaders;
|
|
132
|
+
dictionaryStates = createInitialStates();
|
|
133
|
+
dictionaryGeneration += 1;
|
|
134
|
+
};
|
|
135
|
+
/** Test seam: which dictionaries have been requested or loaded. */
|
|
136
|
+
const hyphenationDictionaryStatus = (dictionary) => dictionaryStates[dictionary].status;
|
|
137
|
+
//#endregion
|
|
138
|
+
export { HyphenationDictionaryError, collectRequestedHyphenationDictionaries, getHyphenationDictionaryGeneration, getHyphenationDictionaryMisses, hyphenationDictionaryFor, hyphenationDictionaryStatus, hyphenatorOrRequest, requestHyphenationDictionary, resetHyphenationDictionaries };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HyphenationDictionaryError } from "./hyphenationDictionaries.js";
|
|
2
|
+
import { Result } from "better-result";
|
|
3
|
+
//#region src/layout-engine/measure/hyphenationPreload.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Load the dictionaries `locales` need before measuring, so a synchronous
|
|
6
|
+
* layout pass hyphenates on its first run. Locales without a dictionary are
|
|
7
|
+
* ignored.
|
|
8
|
+
*/
|
|
9
|
+
declare const preloadHyphenationDictionaries: (locales: Iterable<string>) => Promise<Result<void, HyphenationDictionaryError>>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { preloadHyphenationDictionaries };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { hyphenationDictionaryFor, requestHyphenationDictionary } from "./hyphenationDictionaries.js";
|
|
2
|
+
import { Result } from "better-result";
|
|
3
|
+
//#region src/layout-engine/measure/hyphenationPreload.ts
|
|
4
|
+
/**
|
|
5
|
+
* Awaitable dictionary loading for hosts that measure once and cannot re-run
|
|
6
|
+
* layout when a dictionary arrives (headless layout, tests).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Load the dictionaries `locales` need before measuring, so a synchronous
|
|
10
|
+
* layout pass hyphenates on its first run. Locales without a dictionary are
|
|
11
|
+
* ignored.
|
|
12
|
+
*/
|
|
13
|
+
const preloadHyphenationDictionaries = async (locales) => {
|
|
14
|
+
const dictionaries = /* @__PURE__ */ new Set();
|
|
15
|
+
for (const locale of locales) {
|
|
16
|
+
const dictionary = hyphenationDictionaryFor(locale);
|
|
17
|
+
if (dictionary !== void 0) dictionaries.add(dictionary);
|
|
18
|
+
}
|
|
19
|
+
const settled = await Promise.all([...dictionaries].map(requestHyphenationDictionary));
|
|
20
|
+
for (const outcome of settled) if (outcome.status === "failed") return Result.err(outcome.error);
|
|
21
|
+
return Result.ok(void 0);
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { preloadHyphenationDictionaries };
|
|
@@ -34,11 +34,21 @@ type LineBreakProvider = {
|
|
|
34
34
|
isHangingPunctuation?: (text: string, policy?: LineBreakPolicy) => boolean;
|
|
35
35
|
};
|
|
36
36
|
declare const MAX_HYPHENATION_WORD_LENGTH = 256;
|
|
37
|
+
declare const isNonBreakingGlue: (character: string | undefined) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether glue characters permit a line break between `previous` and `next`.
|
|
40
|
+
* Only the character after the break matters here: glue characters are never
|
|
41
|
+
* break opportunities themselves, so a break after one is never proposed.
|
|
42
|
+
*/
|
|
43
|
+
declare const gluePermitsBreakBefore: (previous: string | undefined, next: string | undefined) => boolean;
|
|
37
44
|
declare const defaultLineBreakProvider: LineBreakProvider;
|
|
38
45
|
declare const getLineBreakProvider: () => LineBreakProvider;
|
|
39
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Changes whenever the active provider or a hyphenation dictionary changes, so
|
|
48
|
+
* layout caches cannot go stale. Both counters only grow, so their sum does too.
|
|
49
|
+
*/
|
|
40
50
|
declare const getLineBreakProviderGeneration: () => number;
|
|
41
51
|
declare const setLineBreakProvider: (provider: LineBreakProvider) => void;
|
|
42
52
|
declare const resetLineBreakProvider: () => void;
|
|
43
53
|
//#endregion
|
|
44
|
-
export { LineBreakPolicy, LineBreakProvider, MAX_HYPHENATION_WORD_LENGTH, defaultLineBreakProvider, getLineBreakProvider, getLineBreakProviderGeneration, resetLineBreakProvider, setLineBreakProvider };
|
|
54
|
+
export { LineBreakPolicy, LineBreakProvider, MAX_HYPHENATION_WORD_LENGTH, defaultLineBreakProvider, getLineBreakProvider, getLineBreakProviderGeneration, gluePermitsBreakBefore, isNonBreakingGlue, resetLineBreakProvider, setLineBreakProvider };
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import britishEnglishHyphenation from "hyphen/en-gb/index.js";
|
|
3
|
-
import americanEnglishHyphenation from "hyphen/en-us/index.js";
|
|
4
|
-
import slovakHyphenation from "hyphen/sk/index.js";
|
|
1
|
+
import { getHyphenationDictionaryGeneration, hyphenationDictionaryFor, hyphenatorOrRequest } from "./hyphenationDictionaries.js";
|
|
5
2
|
//#region src/layout-engine/measure/lineBreakProvider.ts
|
|
6
3
|
/**
|
|
7
4
|
* Replaceable line-break policy for the layout engine.
|
|
@@ -11,10 +8,6 @@ import slovakHyphenation from "hyphen/sk/index.js";
|
|
|
11
8
|
* default provider uses the platform's Unicode segmenter, then applies the
|
|
12
9
|
* OOXML line-edge restrictions that are available in the flow model.
|
|
13
10
|
*/
|
|
14
|
-
const { hyphenateSync: hyphenateCzech } = czechHyphenation;
|
|
15
|
-
const { hyphenateSync: hyphenateBritishEnglish } = britishEnglishHyphenation;
|
|
16
|
-
const { hyphenateSync: hyphenateAmericanEnglish } = americanEnglishHyphenation;
|
|
17
|
-
const { hyphenateSync: hyphenateSlovak } = slovakHyphenation;
|
|
18
11
|
const SEGMENTER_CACHE_LIMIT = 16;
|
|
19
12
|
const LINE_BREAK_LOCALE_CACHE_LIMIT = 16;
|
|
20
13
|
const DEFAULT_SEGMENTER_KEY = "";
|
|
@@ -59,11 +52,20 @@ const BREAK_AFTER_CHARACTER = /* @__PURE__ */ new Set([
|
|
|
59
52
|
"",
|
|
60
53
|
""
|
|
61
54
|
]);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
const isGlueCodeUnit = (code) => code >= 160 && (code === 160 || code === 8199 || code === 8209 || code === 8239 || code === 8288 || code === 65279);
|
|
56
|
+
const isWordJoinerCodeUnit = (code) => code === 8288 || code === 65279;
|
|
57
|
+
const isBreakingWhitespace = (character) => /\s/u.test(character) && !isGlueCodeUnit(character.charCodeAt(0));
|
|
58
|
+
const isNonBreakingGlue = (character) => character?.length === 1 && isGlueCodeUnit(character.charCodeAt(0));
|
|
59
|
+
/**
|
|
60
|
+
* Whether glue characters permit a line break between `previous` and `next`.
|
|
61
|
+
* Only the character after the break matters here: glue characters are never
|
|
62
|
+
* break opportunities themselves, so a break after one is never proposed.
|
|
63
|
+
*/
|
|
64
|
+
const gluePermitsBreakBefore = (previous, next) => {
|
|
65
|
+
if (!isNonBreakingGlue(next)) return true;
|
|
66
|
+
if (isWordJoinerCodeUnit(next?.charCodeAt(0) ?? 0) || previous === void 0) return false;
|
|
67
|
+
return isBreakingWhitespace(previous) || BREAK_AFTER_CHARACTER.has(previous);
|
|
68
|
+
};
|
|
67
69
|
const DEFAULT_PROHIBITED_LINE_START = /* @__PURE__ */ new Set([
|
|
68
70
|
"!",
|
|
69
71
|
"%",
|
|
@@ -175,6 +177,7 @@ const allowsBreak = (text, index, policy, usesEastAsianRules, nextLineStart) =>
|
|
|
175
177
|
};
|
|
176
178
|
const pushBreak = (breaks, text, index, policy, usesEastAsianRules, nextLineStart) => {
|
|
177
179
|
if (index <= 0 || index > text.length || breaks.at(-1) === index) return;
|
|
180
|
+
if (isGlueCodeUnit(text.charCodeAt(index)) && !gluePermitsBreakBefore(previousCodePoint(text, index), text[index])) return;
|
|
178
181
|
if (allowsBreak(text, index, policy, usesEastAsianRules, nextLineStart)) breaks.push(index);
|
|
179
182
|
};
|
|
180
183
|
const findSimpleGraphemeBreaks = (text) => {
|
|
@@ -220,11 +223,11 @@ const findSimpleBreaks = (text, policy) => {
|
|
|
220
223
|
if (codePoint >= 55296 && codePoint <= 57343) return;
|
|
221
224
|
if (codePoint === 13 && text.charCodeAt(index + 1) === 10) continue;
|
|
222
225
|
const character = text[index];
|
|
223
|
-
if (isSimpleWhitespace(character, codePoint) && !
|
|
226
|
+
if (isSimpleWhitespace(character, codePoint) && !isGlueCodeUnit(codePoint)) {
|
|
224
227
|
if (index >= whitespaceRunEnd) {
|
|
225
228
|
whitespaceRunEnd = index + 1;
|
|
226
229
|
let next = firstCodePoint(text, whitespaceRunEnd);
|
|
227
|
-
while (next !== void 0 &&
|
|
230
|
+
while (next !== void 0 && isBreakingWhitespace(next)) {
|
|
228
231
|
whitespaceRunEnd += next.length;
|
|
229
232
|
next = firstCodePoint(text, whitespaceRunEnd);
|
|
230
233
|
}
|
|
@@ -246,11 +249,11 @@ const findUnicodeBreaks = (text, policy) => {
|
|
|
246
249
|
let nextVisibleAfterWhitespace;
|
|
247
250
|
for (const index of graphemeBreaks) {
|
|
248
251
|
const previous = previousCodePoint(text, index);
|
|
249
|
-
if (previous !== void 0 &&
|
|
252
|
+
if (previous !== void 0 && isBreakingWhitespace(previous)) {
|
|
250
253
|
if (index > whitespaceRunEnd) {
|
|
251
254
|
whitespaceRunEnd = index;
|
|
252
255
|
let next = firstCodePoint(text, whitespaceRunEnd);
|
|
253
|
-
while (next !== void 0 &&
|
|
256
|
+
while (next !== void 0 && isBreakingWhitespace(next)) {
|
|
254
257
|
whitespaceRunEnd += next.length;
|
|
255
258
|
next = firstCodePoint(text, whitespaceRunEnd);
|
|
256
259
|
}
|
|
@@ -271,14 +274,6 @@ const findUnicodeBreaks = (text, policy) => {
|
|
|
271
274
|
}
|
|
272
275
|
return [...new Set(breaks)].sort((left, right) => left - right);
|
|
273
276
|
};
|
|
274
|
-
const hyphenatorFor = (locale) => {
|
|
275
|
-
const normalized = locale?.trim().replaceAll("_", "-").toLowerCase();
|
|
276
|
-
if (!normalized) return;
|
|
277
|
-
if (normalized === "en-gb" || normalized.startsWith("en-gb-")) return hyphenateBritishEnglish;
|
|
278
|
-
if (normalized === "en-us" || normalized.startsWith("en-us-")) return hyphenateAmericanEnglish;
|
|
279
|
-
if (normalized === "cs" || normalized.startsWith("cs-")) return hyphenateCzech;
|
|
280
|
-
if (normalized === "sk" || normalized.startsWith("sk-")) return hyphenateSlovak;
|
|
281
|
-
};
|
|
282
277
|
const isAllCapsWord = (text, locale) => {
|
|
283
278
|
const letters = [...text].filter((character) => /\p{Letter}/u.test(character)).join("");
|
|
284
279
|
if (letters.length === 0) return false;
|
|
@@ -287,9 +282,11 @@ const isAllCapsWord = (text, locale) => {
|
|
|
287
282
|
};
|
|
288
283
|
const findPatternHyphenationBreaks = (text, policy) => {
|
|
289
284
|
if (text.length > 256 || text.includes(SOFT_HYPHEN)) return [];
|
|
290
|
-
const
|
|
291
|
-
if (!
|
|
285
|
+
const dictionary = hyphenationDictionaryFor(policy?.locale);
|
|
286
|
+
if (!dictionary) return [];
|
|
292
287
|
if (policy?.doNotHyphenateCaps && (policy.renderedAllCaps === true || isAllCapsWord(text, policy.locale))) return [];
|
|
288
|
+
const hyphenate = hyphenatorOrRequest(dictionary);
|
|
289
|
+
if (!hyphenate) return [];
|
|
293
290
|
const hyphenated = hyphenate(text);
|
|
294
291
|
const breaks = [];
|
|
295
292
|
let sourceOffset = 0;
|
|
@@ -317,8 +314,11 @@ const defaultLineBreakProvider = {
|
|
|
317
314
|
let activeLineBreakProvider = defaultLineBreakProvider;
|
|
318
315
|
let lineBreakProviderGeneration = 0;
|
|
319
316
|
const getLineBreakProvider = () => activeLineBreakProvider;
|
|
320
|
-
/**
|
|
321
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Changes whenever the active provider or a hyphenation dictionary changes, so
|
|
319
|
+
* layout caches cannot go stale. Both counters only grow, so their sum does too.
|
|
320
|
+
*/
|
|
321
|
+
const getLineBreakProviderGeneration = () => lineBreakProviderGeneration + getHyphenationDictionaryGeneration();
|
|
322
322
|
const setLineBreakProvider = (provider) => {
|
|
323
323
|
activeLineBreakProvider = provider;
|
|
324
324
|
lineBreakProviderGeneration += 1;
|
|
@@ -328,4 +328,4 @@ const resetLineBreakProvider = () => {
|
|
|
328
328
|
lineBreakProviderGeneration += 1;
|
|
329
329
|
};
|
|
330
330
|
//#endregion
|
|
331
|
-
export { MAX_HYPHENATION_WORD_LENGTH, defaultLineBreakProvider, getLineBreakProvider, getLineBreakProviderGeneration, resetLineBreakProvider, setLineBreakProvider };
|
|
331
|
+
export { MAX_HYPHENATION_WORD_LENGTH, defaultLineBreakProvider, getLineBreakProvider, getLineBreakProviderGeneration, gluePermitsBreakBefore, isNonBreakingGlue, resetLineBreakProvider, setLineBreakProvider };
|
|
@@ -10,5 +10,10 @@ declare const findGraphemeBreaks: (text: string, policy?: LineBreakPolicy) => nu
|
|
|
10
10
|
declare const findHyphenationBreaks: (text: string, policy?: LineBreakPolicy) => number[];
|
|
11
11
|
declare const isHangingPunctuation: (text: string, policy?: LineBreakPolicy) => boolean;
|
|
12
12
|
declare function isBreakChar(char: string | undefined): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a line may break at a run boundary, between the last character of
|
|
15
|
+
* one run and the first character of the text that follows it.
|
|
16
|
+
*/
|
|
17
|
+
declare function isBreakBetween(previous: string | undefined, next: string | undefined): boolean;
|
|
13
18
|
//#endregion
|
|
14
|
-
export { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakChar, isHangingPunctuation };
|
|
19
|
+
export { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakBetween, isBreakChar, isHangingPunctuation };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isCjkCodePoint } from "../../utils/scriptSegments.js";
|
|
2
|
-
import { defaultLineBreakProvider, getLineBreakProvider } from "./lineBreakProvider.js";
|
|
2
|
+
import { defaultLineBreakProvider, getLineBreakProvider, gluePermitsBreakBefore, isNonBreakingGlue } from "./lineBreakProvider.js";
|
|
3
3
|
//#region src/layout-engine/measure/lineBreaks.ts
|
|
4
4
|
/**
|
|
5
5
|
* Indices in `text` where a line may end. Latin text breaks after
|
|
@@ -13,11 +13,18 @@ const isHangingPunctuation = (text, policy) => {
|
|
|
13
13
|
return (getLineBreakProvider().isHangingPunctuation ?? defaultLineBreakProvider.isHangingPunctuation)?.(text, policy) ?? false;
|
|
14
14
|
};
|
|
15
15
|
function isBreakChar(char) {
|
|
16
|
-
if (char === void 0) return false;
|
|
16
|
+
if (char === void 0 || isNonBreakingGlue(char)) return false;
|
|
17
17
|
if (/\s/u.test(char) || char === "-" || char === "" || char === "") return true;
|
|
18
18
|
if (char >= "\udc00" && char <= "\udfff") return true;
|
|
19
19
|
const codePoint = char.codePointAt(0);
|
|
20
20
|
return codePoint !== void 0 && isCjkCodePoint(codePoint);
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether a line may break at a run boundary, between the last character of
|
|
24
|
+
* one run and the first character of the text that follows it.
|
|
25
|
+
*/
|
|
26
|
+
function isBreakBetween(previous, next) {
|
|
27
|
+
return isBreakChar(previous) && gluePermitsBreakBefore(previous, next);
|
|
28
|
+
}
|
|
22
29
|
//#endregion
|
|
23
|
-
export { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakChar, isHangingPunctuation };
|
|
30
|
+
export { findGraphemeBreaks, findHyphenationBreaks, findWordBreaks, isBreakBetween, isBreakChar, isHangingPunctuation };
|
|
@@ -7,10 +7,10 @@ import { ParagraphBlock } from "../types.js";
|
|
|
7
7
|
*/
|
|
8
8
|
declare const DEFAULT_TAB_STOP_TWIPS = 720;
|
|
9
9
|
/**
|
|
10
|
-
* Marker font resolution per ECMA-376 §17.9
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Marker font resolution per ECMA-376 §17.9: the numbering level's `w:rPr`
|
|
11
|
+
* over the paragraph mark's run properties (`attrs.listMarkerFormatting`,
|
|
12
|
+
* resolved by the bridge), then paragraph and document defaults. The text runs
|
|
13
|
+
* of the paragraph do not format its number.
|
|
14
14
|
*/
|
|
15
15
|
declare function resolveListMarkerFont(block: ParagraphBlock): {
|
|
16
16
|
fontFamily: string;
|
|
@@ -19,6 +19,7 @@ declare function resolveListMarkerFont(block: ParagraphBlock): {
|
|
|
19
19
|
bold?: boolean;
|
|
20
20
|
italic?: boolean;
|
|
21
21
|
rtl?: boolean;
|
|
22
|
+
color?: string;
|
|
22
23
|
};
|
|
23
24
|
/**
|
|
24
25
|
* Compute the marker's inline-block width in pixels, or 0 if the paragraph
|
|
@@ -31,8 +32,10 @@ declare function resolveListMarkerFont(block: ParagraphBlock): {
|
|
|
31
32
|
* one space glyph, `tab` (default) → grow to the next tab stop. A hanging
|
|
32
33
|
* indent remains the minimum footprint so body text cannot enter the
|
|
33
34
|
* marker slot.
|
|
34
|
-
* - `w:tabs` on the paragraph
|
|
35
|
-
* `bar` (§17.3.1.37) is
|
|
35
|
+
* - `w:tabs` on the paragraph (its style and numbering level's stops merged
|
|
36
|
+
* in): non-`clear`/non-`bar` stops past the marker. `bar` (§17.3.1.37) is
|
|
37
|
+
* a vertical line and doesn't advance the cursor. A stop inside the
|
|
38
|
+
* hanging slot, past the marker, takes precedence over the hanging indent.
|
|
36
39
|
* - default tab grid: stops at multiples of `DEFAULT_TAB_STOP_TWIPS`,
|
|
37
40
|
* anchored at 0 (start of body content area, NOT `w:ind`).
|
|
38
41
|
*
|
|
@@ -3,7 +3,6 @@ import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./comp
|
|
|
3
3
|
import { ptToPx } from "./measureHelpers.js";
|
|
4
4
|
import { measureTextWidth } from "./measureProvider.js";
|
|
5
5
|
//#region src/layout-engine/measure/listMarkerWidth.ts
|
|
6
|
-
const DEFAULT_FONT_FAMILY = "Calibri";
|
|
7
6
|
const DEFAULT_FONT_SIZE = 11;
|
|
8
7
|
/**
|
|
9
8
|
* OOXML default `w:defaultTabStop` (§17.6.13) when settings.xml omits it.
|
|
@@ -13,23 +12,16 @@ const DEFAULT_FONT_SIZE = 11;
|
|
|
13
12
|
const DEFAULT_TAB_STOP_TWIPS = 720;
|
|
14
13
|
const TWIPS_TO_PX = 96 / 1440;
|
|
15
14
|
/**
|
|
16
|
-
* Marker font resolution per ECMA-376 §17.9
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* Marker font resolution per ECMA-376 §17.9: the numbering level's `w:rPr`
|
|
16
|
+
* over the paragraph mark's run properties (`attrs.listMarkerFormatting`,
|
|
17
|
+
* resolved by the bridge), then paragraph and document defaults. The text runs
|
|
18
|
+
* of the paragraph do not format its number.
|
|
20
19
|
*/
|
|
21
20
|
function resolveListMarkerFont(block) {
|
|
22
21
|
const attrs = block.attrs;
|
|
23
|
-
const firstTextRun = block.runs.find((r) => r.kind === "text");
|
|
24
22
|
const markerFormatting = attrs?.listMarkerFormatting;
|
|
25
|
-
|
|
26
|
-
const italic = markerFormatting?.italic ?? firstTextRun?.italic;
|
|
27
|
-
let fontFamily = attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY;
|
|
23
|
+
let fontFamily = attrs?.defaultFontFamily ?? "Times New Roman";
|
|
28
24
|
let alternateFontFamily = attrs?.defaultAlternateFontFamily;
|
|
29
|
-
if (firstTextRun?.fontFamily !== void 0) {
|
|
30
|
-
fontFamily = firstTextRun.fontFamily;
|
|
31
|
-
alternateFontFamily = firstTextRun.alternateFontFamily;
|
|
32
|
-
}
|
|
33
25
|
if (markerFormatting?.fontFamily !== void 0) {
|
|
34
26
|
fontFamily = markerFormatting.fontFamily;
|
|
35
27
|
alternateFontFamily = markerFormatting.alternateFontFamily;
|
|
@@ -37,10 +29,11 @@ function resolveListMarkerFont(block) {
|
|
|
37
29
|
const base = {
|
|
38
30
|
fontFamily,
|
|
39
31
|
...alternateFontFamily !== void 0 ? { alternateFontFamily } : {},
|
|
40
|
-
fontSize: markerFormatting?.fontSize ??
|
|
41
|
-
...bold !== void 0 ? { bold } : {},
|
|
42
|
-
...italic !== void 0 ? { italic } : {},
|
|
43
|
-
...markerFormatting?.rtl !== void 0 ? { rtl: markerFormatting.rtl } : {}
|
|
32
|
+
fontSize: markerFormatting?.fontSize ?? attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE,
|
|
33
|
+
...markerFormatting?.bold !== void 0 ? { bold: markerFormatting.bold } : {},
|
|
34
|
+
...markerFormatting?.italic !== void 0 ? { italic: markerFormatting.italic } : {},
|
|
35
|
+
...markerFormatting?.rtl !== void 0 ? { rtl: markerFormatting.rtl } : {},
|
|
36
|
+
...markerFormatting?.color !== void 0 ? { color: markerFormatting.color } : {}
|
|
44
37
|
};
|
|
45
38
|
const marker = attrs?.listMarker ?? "";
|
|
46
39
|
if (markerFormatting && hasComplexScriptFormatting(markerFormatting) && (markerFormatting.forceComplexScript || hasComplexScript(marker))) return applyComplexScriptFormatting(base, markerFormatting);
|
|
@@ -66,8 +59,10 @@ function resolveListMarkerFont(block) {
|
|
|
66
59
|
* one space glyph, `tab` (default) → grow to the next tab stop. A hanging
|
|
67
60
|
* indent remains the minimum footprint so body text cannot enter the
|
|
68
61
|
* marker slot.
|
|
69
|
-
* - `w:tabs` on the paragraph
|
|
70
|
-
* `bar` (§17.3.1.37) is
|
|
62
|
+
* - `w:tabs` on the paragraph (its style and numbering level's stops merged
|
|
63
|
+
* in): non-`clear`/non-`bar` stops past the marker. `bar` (§17.3.1.37) is
|
|
64
|
+
* a vertical line and doesn't advance the cursor. A stop inside the
|
|
65
|
+
* hanging slot, past the marker, takes precedence over the hanging indent.
|
|
71
66
|
* - default tab grid: stops at multiples of `DEFAULT_TAB_STOP_TWIPS`,
|
|
72
67
|
* anchored at 0 (start of body content area, NOT `w:ind`).
|
|
73
68
|
*
|
|
@@ -90,10 +85,13 @@ function getListMarkerInlineWidth(block) {
|
|
|
90
85
|
const firstLine = indent?.firstLine ?? 0;
|
|
91
86
|
const markerStartPx = hanging > 0 ? indentLeft - hanging : indentLeft + firstLine;
|
|
92
87
|
const minBodyStart = markerStartPx + markerEndOffset;
|
|
93
|
-
const customTabs = (attrs.tabs ?? []).filter((t) => t.val !== "clear" && t.val !== "bar").map((t) => t.pos * TWIPS_TO_PX);
|
|
94
|
-
if (hanging > 0
|
|
88
|
+
const customTabs = (attrs.tabs ?? []).filter((t) => t.val !== "clear" && t.val !== "bar").map((t) => t.pos * TWIPS_TO_PX).sort((a, b) => a - b);
|
|
89
|
+
if (hanging > 0 && minBodyStart <= indentLeft) {
|
|
90
|
+
const firstCustomStop = customTabs.find((px) => px >= minBodyStart);
|
|
91
|
+
return (firstCustomStop !== void 0 && (firstCustomStop < indentLeft || attrs.listNumberingTabIgnoresIndent === true) ? firstCustomStop : indentLeft) - markerStartPx;
|
|
92
|
+
}
|
|
95
93
|
const searchStart = hanging > 0 ? Math.max(minBodyStart, indentLeft) : minBodyStart;
|
|
96
|
-
const firstCustomPast = customTabs.
|
|
94
|
+
const firstCustomPast = customTabs.find((px) => px >= searchStart);
|
|
97
95
|
const defaultTabStopPx = (attrs.defaultTabStopTwips ?? 720) * TWIPS_TO_PX;
|
|
98
96
|
const firstGridPast = defaultTabStopPx > 0 ? Math.ceil(searchStart / defaultTabStopPx) * defaultTabStopPx : void 0;
|
|
99
97
|
let bodyStart;
|