@stll/folio-core 0.47.7 → 0.48.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/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 +59 -17
- 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 +5 -4
- package/dist/display-list/build/tablePrimitives.js +10 -6
- package/dist/docx/attributeRemainder.js +10 -2
- package/dist/docx/blockContentParser.d.ts +2 -0
- package/dist/docx/blockContentParser.js +77 -62
- 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/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 +2 -1
- package/dist/docx/groupDrawingParser.js +3 -6
- 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/paragraphParser.d.ts +5 -1
- package/dist/docx/paragraphParser.js +381 -362
- 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 +17 -6
- 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 +172 -184
- package/dist/docx/sdtProperties.js +87 -84
- package/dist/docx/sectionParser.js +169 -160
- package/dist/docx/serializer/paragraphSerializer.js +42 -38
- package/dist/docx/serializer/settingsSerializer.js +19 -1
- package/dist/docx/settingsParser.js +8 -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 +8 -3
- package/dist/docx/textBoxParser.js +3 -3
- 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/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 +258 -35
- 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 +70 -11
- 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 +11 -5
- package/dist/layout-engine/keep-together.js +39 -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 +4 -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 +42 -8
- package/dist/layout-engine/measure/measureContainer.js +5 -5
- package/dist/layout-engine/measure/measureHelpers.d.ts +15 -3
- package/dist/layout-engine/measure/measureHelpers.js +21 -5
- package/dist/layout-engine/measure/measureParagraph.js +109 -30
- package/dist/layout-engine/measure/measureTypes.d.ts +5 -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 +10 -1
- package/dist/layout-engine/paginator.js +13 -1
- package/dist/layout-engine/tableRowBreak.js +3 -1
- package/dist/layout-engine/types.d.ts +89 -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 +34 -22
- package/dist/layout-painter/renderTable.js +26 -13
- package/dist/prosemirror/attrs/index.d.ts +9 -9
- package/dist/prosemirror/attrs/index.js +43 -11
- package/dist/prosemirror/conversion/fromProseDoc.js +8 -1
- package/dist/prosemirror/conversion/markInterner.d.ts +21 -0
- package/dist/prosemirror/conversion/markInterner.js +90 -0
- package/dist/prosemirror/conversion/toProseDoc.js +77 -41
- package/dist/prosemirror/extensions/core/DocExtension.js +2 -1
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
- package/dist/prosemirror/extensions/marks/markUtils.d.ts +6 -2
- package/dist/prosemirror/extensions/marks/markUtils.js +39 -51
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +2 -1
- package/dist/prosemirror/listMarker.d.ts +1 -0
- package/dist/prosemirror/listMarker.js +2 -0
- package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
- package/dist/prosemirror/listRenderingAttrs.js +3 -0
- package/dist/prosemirror/schema/nodes.d.ts +13 -1
- package/dist/prosemirror/textBoxHostParagraph.d.ts +13 -0
- package/dist/prosemirror/textBoxHostParagraph.js +21 -0
- package/dist/prosemirror/validation.js +91 -80
- 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
|
@@ -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;
|
|
@@ -7,9 +7,11 @@ import { getTextBoxGroupId } from "../textBoxGroup.js";
|
|
|
7
7
|
import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, floatingTextBoxWrapsText, resolveTableCellPadding, resolveTableWidthPx, tableColumnsArePinned } from "../types.js";
|
|
8
8
|
import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
|
|
9
9
|
import { resolveFloatingTablePageX } from "./floatingTablePosition.js";
|
|
10
|
+
import { getHyphenationDictionaryMisses } from "./hyphenationDictionaries.js";
|
|
10
11
|
import { findClearLineY, measureParagraph } from "./measureParagraph.js";
|
|
12
|
+
import { buildTableCellFloatingZones, getTableCellFloatingImages } from "./tableCellFloating.js";
|
|
11
13
|
import { createTableCellFlowState, finishTableCellFlow, placeTableCellBlock } from "./tableCellFlow.js";
|
|
12
|
-
import { buildTableCellGrid, getFirstAvailableColumn, getTableCellVerticalBorderHeight } from "./tableCellGrid.js";
|
|
14
|
+
import { buildTableCellGrid, getFirstAvailableColumn, getTableCellSpacingInsetsX, getTableCellSpacingInsetsY, getTableCellVerticalBorderHeight } from "./tableCellGrid.js";
|
|
13
15
|
import { layoutTextBoxContent } from "./textBoxParagraphLayout.js";
|
|
14
16
|
//#region src/layout-engine/measure/measureBlocks.ts
|
|
15
17
|
/**
|
|
@@ -59,6 +61,27 @@ function emuToPixels(emu) {
|
|
|
59
61
|
if (emu === void 0) return 0;
|
|
60
62
|
return Math.round(emu * 96 / 914400);
|
|
61
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Re-measure a cell's paragraphs against the wrap exclusions of the pictures
|
|
66
|
+
* anchored in it, so lines a picture displaces (beside it, or below it when it
|
|
67
|
+
* leaves no room) count toward the cell height. Mirrors the painter's
|
|
68
|
+
* per-paragraph re-measure so both agree on where each line sits.
|
|
69
|
+
*/
|
|
70
|
+
function wrapTableCellParagraphsAroundFloats(cell, cellMeasure, contentWidth, fieldValues) {
|
|
71
|
+
const zones = buildTableCellFloatingZones(getTableCellFloatingImages(cell, cellMeasure, contentWidth), contentWidth);
|
|
72
|
+
if (zones.length === 0) return;
|
|
73
|
+
const flowState = createTableCellFlowState();
|
|
74
|
+
for (let blockIdx = 0; blockIdx < cell.blocks.length; blockIdx++) {
|
|
75
|
+
const block = cell.blocks[blockIdx];
|
|
76
|
+
let blockMeasure = cellMeasure.blocks[blockIdx];
|
|
77
|
+
if (!block || !blockMeasure) continue;
|
|
78
|
+
if (block.kind === "paragraph") {
|
|
79
|
+
blockMeasure = measureBlock(block, contentWidth, zones, placeTableCellBlock({ ...flowState }, block, blockMeasure).contentTop, fieldValues);
|
|
80
|
+
cellMeasure.blocks[blockIdx] = blockMeasure;
|
|
81
|
+
}
|
|
82
|
+
placeTableCellBlock(flowState, block, blockMeasure);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
62
85
|
function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
63
86
|
let columnWidths = tableBlock.columnWidths ?? [];
|
|
64
87
|
const explicitWidthPx = resolveTableWidthPx(tableBlock.width, tableBlock.widthType, contentWidth);
|
|
@@ -74,6 +97,9 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
74
97
|
}
|
|
75
98
|
}
|
|
76
99
|
const cellGrid = buildTableCellGrid(tableBlock.rows, columnWidths.length);
|
|
100
|
+
const cellSpacing = tableBlock.cellSpacing;
|
|
101
|
+
const bordersSeparated = (cellSpacing ?? 0) > 0;
|
|
102
|
+
const rowCount = tableBlock.rows.length;
|
|
77
103
|
const columnsPinned = tableColumnsArePinned(tableBlock);
|
|
78
104
|
const rows = tableBlock.rows.map((row, rowIdx) => {
|
|
79
105
|
let columnIndex = getFirstAvailableColumn(cellGrid, rowIdx, row.gridBefore ?? 0);
|
|
@@ -83,15 +109,19 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
83
109
|
let cellWidth = 0;
|
|
84
110
|
for (let c = 0; c < colSpan && columnIndex + c < columnWidths.length; c++) cellWidth += columnWidths[columnIndex + c] ?? 0;
|
|
85
111
|
if (cellWidth === 0) cellWidth = cell.width ?? 100;
|
|
112
|
+
const spacingInsets = getTableCellSpacingInsetsX(cellSpacing, columnIndex, colSpan, columnWidths.length);
|
|
113
|
+
cellWidth = Math.max(0, cellWidth - spacingInsets.start - spacingInsets.end);
|
|
86
114
|
columnIndex = getFirstAvailableColumn(cellGrid, rowIdx, columnIndex + colSpan);
|
|
87
115
|
const { left: padLeft, right: padRight } = resolveTableCellPadding(cell);
|
|
88
116
|
const cellContentWidth = Math.max(1, cellWidth - padLeft - padRight);
|
|
89
|
-
const
|
|
117
|
+
const keepSingleLine = cell.noWrap === true && !columnsPinned;
|
|
118
|
+
const measureWidth = keepSingleLine ? NO_WRAP_MEASURE_WIDTH : cellContentWidth;
|
|
90
119
|
const cellMeasure = {
|
|
91
120
|
blocks: cell.blocks.map((b) => measureBlock(b, measureWidth, void 0, void 0, fieldValues)),
|
|
92
121
|
width: cellWidth,
|
|
93
122
|
height: 0
|
|
94
123
|
};
|
|
124
|
+
if (!keepSingleLine) wrapTableCellParagraphsAroundFloats(cell, cellMeasure, cellContentWidth, fieldValues);
|
|
95
125
|
if (cell.colSpan !== void 0) cellMeasure.colSpan = cell.colSpan;
|
|
96
126
|
if (cell.rowSpan !== void 0) cellMeasure.rowSpan = cell.rowSpan;
|
|
97
127
|
return cellMeasure;
|
|
@@ -129,15 +159,17 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
129
159
|
const { top: padTop, bottom: padBottom } = resolveTableCellPadding(sourceCell);
|
|
130
160
|
cell.height += padTop + padBottom;
|
|
131
161
|
if ((sourceCell?.rowSpan ?? 1) > 1) continue;
|
|
132
|
-
const borderHeight = getTableCellVerticalBorderHeight(cellGrid, sourceCell, rowIdx);
|
|
162
|
+
const borderHeight = getTableCellVerticalBorderHeight(cellGrid, sourceCell, rowIdx, bordersSeparated);
|
|
133
163
|
maxCellHeightWithBorders = Math.max(maxCellHeightWithBorders, cell.height + borderHeight);
|
|
134
164
|
maxCellInsets = Math.max(maxCellInsets, padTop + padBottom + borderHeight);
|
|
135
165
|
}
|
|
136
166
|
const explicitHeight = sourceRow?.height;
|
|
137
167
|
const heightRule = sourceRow?.heightRule;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
168
|
+
const spacing = getTableCellSpacingInsetsY(cellSpacing, rowIdx, 1, rowCount);
|
|
169
|
+
const spacingHeight = spacing.top + spacing.bottom;
|
|
170
|
+
if (explicitHeight && heightRule === "exact") row.height = (bordersSeparated ? Math.max(0, explicitHeight - (cellSpacing ?? 0)) : explicitHeight) + spacingHeight;
|
|
171
|
+
else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight + maxCellInsets) + spacingHeight;
|
|
172
|
+
else row.height = maxCellHeightWithBorders + spacingHeight;
|
|
141
173
|
}
|
|
142
174
|
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
143
175
|
const sourceRow = tableBlock.rows[rowIdx];
|
|
@@ -151,7 +183,8 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
151
183
|
const spanEnd = Math.min(rows.length, rowIdx + rowSpan);
|
|
152
184
|
let combinedHeight = 0;
|
|
153
185
|
for (let spannedRowIdx = rowIdx; spannedRowIdx < spanEnd; spannedRowIdx++) combinedHeight += rows[spannedRowIdx]?.height ?? 0;
|
|
154
|
-
const
|
|
186
|
+
const spanSpacing = getTableCellSpacingInsetsY(cellSpacing, rowIdx, rowSpan, rowCount);
|
|
187
|
+
const deficit = measuredCell.height + getTableCellVerticalBorderHeight(cellGrid, sourceCell, rowIdx, bordersSeparated) + spanSpacing.top + spanSpacing.bottom - combinedHeight;
|
|
155
188
|
if (deficit <= 0) continue;
|
|
156
189
|
for (let spannedRowIdx = spanEnd - 1; spannedRowIdx >= rowIdx; spannedRowIdx--) {
|
|
157
190
|
if (tableBlock.rows[spannedRowIdx]?.heightRule === "exact") continue;
|
|
@@ -529,8 +562,9 @@ function measureBlock(block, contentWidth, floatingZones, cumulativeY, fieldValu
|
|
|
529
562
|
if (floatingZones) measureOpts.floatingZones = floatingZones;
|
|
530
563
|
if (fieldValues) measureOpts.fieldValues = fieldValues;
|
|
531
564
|
if (options?.allowEndTabOverflow === true) measureOpts.allowEndTabOverflow = true;
|
|
565
|
+
const missesBefore = getHyphenationDictionaryMisses();
|
|
532
566
|
const result = measureParagraph(pBlock, contentWidth, measureOpts);
|
|
533
|
-
if (cacheable) setCachedParagraphMeasure(pBlock, contentWidth, result);
|
|
567
|
+
if (cacheable && getHyphenationDictionaryMisses() === missesBefore) setCachedParagraphMeasure(pBlock, contentWidth, result);
|
|
534
568
|
return result;
|
|
535
569
|
}
|
|
536
570
|
case "table": return measureTableBlock(block, contentWidth, fieldValues);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
|
|
2
|
-
import { SCRIPT_CLASS,
|
|
2
|
+
import { SCRIPT_CLASS, hasComplexScript, hasEastAsiaSlotText, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
|
|
3
3
|
import { getCachedFontMetrics, getCachedTextWidth, getTextWidthCacheGeneration, setCachedFontMetrics, setCachedTextWidth } from "./cache.js";
|
|
4
4
|
import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./complexScriptFormatting.js";
|
|
5
5
|
import { buildFontString, getResolvedData, ptToPx } from "./measureHelpers.js";
|
|
@@ -70,7 +70,7 @@ function getWorkerFontFingerprintWidth(ctx, font, fontCacheKey, fontKerning) {
|
|
|
70
70
|
*/
|
|
71
71
|
function canvasGetFontMetrics(style) {
|
|
72
72
|
const fontSize = style.fontSize ?? 11;
|
|
73
|
-
const fontFamily = style.fontFamily ?? "
|
|
73
|
+
const fontFamily = style.fontFamily ?? "Times New Roman";
|
|
74
74
|
const bold = style.bold ?? false;
|
|
75
75
|
const italic = style.italic ?? false;
|
|
76
76
|
const fontVariant = style.fontVariant;
|
|
@@ -197,7 +197,7 @@ function glyphAdvanceStyle(style) {
|
|
|
197
197
|
* AND text that would select it. Keeps the all-Latin path on a single font.
|
|
198
198
|
*/
|
|
199
199
|
function needsPerScriptFonts(style, measuredText) {
|
|
200
|
-
if (style.eastAsiaFontFamily &&
|
|
200
|
+
if (style.eastAsiaFontFamily && hasEastAsiaSlotText(measuredText, style.eastAsiaHint)) return true;
|
|
201
201
|
return hasComplexScriptFormatting(style) && hasComplexScript(measuredText);
|
|
202
202
|
}
|
|
203
203
|
/** Resolve the independent font/style slot selected by a script segment. */
|
|
@@ -219,7 +219,7 @@ function measureMixedScriptWidth(measuredText, style) {
|
|
|
219
219
|
const letterSpacing = style.letterSpacing ?? 0;
|
|
220
220
|
const horizontalScale = getHorizontalScaleFactor(style.horizontalScale);
|
|
221
221
|
let glyphWidth = 0;
|
|
222
|
-
for (const segment of segmentByScript(measuredText)) glyphWidth += canvasMeasureTextWidth(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script)));
|
|
222
|
+
for (const segment of segmentByScript(measuredText, style.eastAsiaHint)) glyphWidth += canvasMeasureTextWidth(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script)));
|
|
223
223
|
let width = glyphWidth;
|
|
224
224
|
if (letterSpacing) {
|
|
225
225
|
const codePoints = countCodePoints(measuredText);
|
|
@@ -302,7 +302,7 @@ function canvasMeasureRun(text, sourceStyle) {
|
|
|
302
302
|
const cp = char.codePointAt(0);
|
|
303
303
|
const measured = applyTextTransform(char, style);
|
|
304
304
|
if (eastAsiaFont !== void 0 || complexScriptFont !== void 0) {
|
|
305
|
-
const script = scriptClassOf(cp);
|
|
305
|
+
const script = scriptClassOf(cp, style.eastAsiaHint);
|
|
306
306
|
ctx.font = (script === SCRIPT_CLASS.eastAsia ? eastAsiaFont : void 0) ?? (script === SCRIPT_CLASS.complex ? complexScriptFont : void 0) ?? baseFont;
|
|
307
307
|
}
|
|
308
308
|
let charWidth = ctx.measureText(measured).width;
|