@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.
Files changed (160) hide show
  1. package/dist/controller/fontReadiness.js +3 -3
  2. package/dist/controller/hyphenationReadiness.d.ts +19 -0
  3. package/dist/controller/hyphenationReadiness.js +47 -0
  4. package/dist/controller/layoutPipeline.d.ts +3 -0
  5. package/dist/controller/layoutPipeline.js +59 -17
  6. package/dist/display-list/build/buildDisplayList.js +39 -12
  7. package/dist/display-list/build/pageFurniture.d.ts +7 -1
  8. package/dist/display-list/build/pageFurniture.js +21 -8
  9. package/dist/display-list/build/paragraphPrimitives.js +5 -4
  10. package/dist/display-list/build/tablePrimitives.js +10 -6
  11. package/dist/docx/attributeRemainder.js +10 -2
  12. package/dist/docx/blockContentParser.d.ts +2 -0
  13. package/dist/docx/blockContentParser.js +77 -62
  14. package/dist/docx/commentParser.d.ts +2 -1
  15. package/dist/docx/commentParser.js +57 -45
  16. package/dist/docx/containerChildren.d.ts +21 -8
  17. package/dist/docx/containerChildren.js +39 -9
  18. package/dist/docx/documentParser.d.ts +5 -1
  19. package/dist/docx/documentParser.js +8 -5
  20. package/dist/docx/documentSectionFacts.d.ts +22 -0
  21. package/dist/docx/documentSectionFacts.js +43 -0
  22. package/dist/docx/fieldParser.d.ts +8 -1
  23. package/dist/docx/fieldParser.js +47 -3
  24. package/dist/docx/fontTableParser.js +34 -27
  25. package/dist/docx/footnoteParser.d.ts +3 -2
  26. package/dist/docx/footnoteParser.js +15 -12
  27. package/dist/docx/groupDrawingParser.d.ts +2 -1
  28. package/dist/docx/groupDrawingParser.js +3 -6
  29. package/dist/docx/headerFooterParser.d.ts +7 -2
  30. package/dist/docx/headerFooterParser.js +15 -5
  31. package/dist/docx/hyperlinkParser.d.ts +48 -36
  32. package/dist/docx/hyperlinkParser.js +97 -82
  33. package/dist/docx/numberingParser.js +108 -84
  34. package/dist/docx/paragraphParser.d.ts +5 -1
  35. package/dist/docx/paragraphParser.js +381 -362
  36. package/dist/docx/paragraphProperties.js +123 -109
  37. package/dist/docx/paragraphPropertySource.d.ts +6 -1
  38. package/dist/docx/paragraphPropertySource.js +9 -1
  39. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  40. package/dist/docx/paragraphTextBoxEnrichment.js +17 -6
  41. package/dist/docx/parser.d.ts +7 -1
  42. package/dist/docx/parser.js +33 -16
  43. package/dist/docx/previewBudget.d.ts +55 -33
  44. package/dist/docx/previewBudget.js +59 -65
  45. package/dist/docx/rezip.js +41 -58
  46. package/dist/docx/runParser.d.ts +4 -1
  47. package/dist/docx/runParser.js +172 -184
  48. package/dist/docx/sdtProperties.js +87 -84
  49. package/dist/docx/sectionParser.js +169 -160
  50. package/dist/docx/serializer/paragraphSerializer.js +42 -38
  51. package/dist/docx/serializer/settingsSerializer.js +19 -1
  52. package/dist/docx/settingsParser.js +8 -0
  53. package/dist/docx/streamingXmlParser.d.ts +6 -2
  54. package/dist/docx/streamingXmlParser.js +12 -7
  55. package/dist/docx/styleParser.js +34 -8
  56. package/dist/docx/tableParser.d.ts +21 -12
  57. package/dist/docx/tableParser.js +518 -441
  58. package/dist/docx/textBoxParser.d.ts +8 -3
  59. package/dist/docx/textBoxParser.js +3 -3
  60. package/dist/docx/verbatimCapture.d.ts +10 -8
  61. package/dist/docx/verbatimCapture.js +45 -2
  62. package/dist/docx/vmlImageParser.d.ts +15 -13
  63. package/dist/docx/vmlImageParser.js +75 -60
  64. package/dist/docx/vmlPreview.d.ts +3 -3
  65. package/dist/docx/vmlPreview.js +3 -4
  66. package/dist/docx/xmlNamespaceContext.d.ts +9 -0
  67. package/dist/docx/xmlNamespaceContext.js +54 -0
  68. package/dist/docx/xmlParser.d.ts +6 -3
  69. package/dist/docx/xmlParser.js +11 -41
  70. package/dist/docx/xmlResourceLimits.d.ts +2 -1
  71. package/dist/docx/xmlResourceLimits.js +4 -1
  72. package/dist/fonts/headlessMeasure.js +0 -0
  73. package/dist/headless-layout.d.ts +11 -2
  74. package/dist/headless-layout.js +153 -107
  75. package/dist/internal/paragraphFormattingSerialization.js +3 -3
  76. package/dist/layout-bridge/convert/endnoteLayout.d.ts +65 -0
  77. package/dist/layout-bridge/convert/endnoteLayout.js +227 -0
  78. package/dist/layout-bridge/convert/footnoteLayout.d.ts +19 -3
  79. package/dist/layout-bridge/convert/footnoteLayout.js +40 -31
  80. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -7
  81. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -20
  82. package/dist/layout-bridge/convert/paragraphMarkFormatting.d.ts +10 -0
  83. package/dist/layout-bridge/convert/paragraphMarkFormatting.js +59 -0
  84. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +29 -1
  85. package/dist/layout-bridge/convert/toFlowBlocks.js +258 -35
  86. package/dist/layout-bridge/engine/hitTest.js +5 -3
  87. package/dist/layout-bridge/engine/selectionRects.js +5 -3
  88. package/dist/layout-engine/anchorLayoutInCellCompatibility.d.ts +10 -0
  89. package/dist/layout-engine/anchorLayoutInCellCompatibility.js +22 -0
  90. package/dist/layout-engine/index.d.ts +2 -2
  91. package/dist/layout-engine/index.js +70 -11
  92. package/dist/layout-engine/justificationCompatibility.d.ts +1 -1
  93. package/dist/layout-engine/justificationCompatibility.js +9 -5
  94. package/dist/layout-engine/keep-together.d.ts +11 -5
  95. package/dist/layout-engine/keep-together.js +39 -16
  96. package/dist/layout-engine/layoutInstrumentation.d.ts +8 -2
  97. package/dist/layout-engine/layoutInstrumentation.js +8 -1
  98. package/dist/layout-engine/measure/advanceComposition.js +4 -7
  99. package/dist/layout-engine/measure/hyphenationDictionaries.d.ts +74 -0
  100. package/dist/layout-engine/measure/hyphenationDictionaries.js +138 -0
  101. package/dist/layout-engine/measure/hyphenationPreload.d.ts +11 -0
  102. package/dist/layout-engine/measure/hyphenationPreload.js +24 -0
  103. package/dist/layout-engine/measure/lineBreakProvider.d.ts +12 -2
  104. package/dist/layout-engine/measure/lineBreakProvider.js +30 -30
  105. package/dist/layout-engine/measure/lineBreaks.d.ts +6 -1
  106. package/dist/layout-engine/measure/lineBreaks.js +10 -3
  107. package/dist/layout-engine/measure/listMarkerWidth.d.ts +9 -6
  108. package/dist/layout-engine/measure/listMarkerWidth.js +20 -22
  109. package/dist/layout-engine/measure/measureBlocks.js +42 -8
  110. package/dist/layout-engine/measure/measureContainer.js +5 -5
  111. package/dist/layout-engine/measure/measureHelpers.d.ts +15 -3
  112. package/dist/layout-engine/measure/measureHelpers.js +21 -5
  113. package/dist/layout-engine/measure/measureParagraph.js +109 -30
  114. package/dist/layout-engine/measure/measureTypes.d.ts +5 -0
  115. package/dist/layout-engine/measure/tabCalculator.d.ts +2 -0
  116. package/dist/layout-engine/measure/tabCalculator.js +2 -1
  117. package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
  118. package/dist/layout-engine/measure/tableCellFloating.js +51 -19
  119. package/dist/layout-engine/measure/tableCellGrid.d.ts +28 -3
  120. package/dist/layout-engine/measure/tableCellGrid.js +45 -5
  121. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +2 -1
  122. package/dist/layout-engine/noteAreaFlow.d.ts +19 -0
  123. package/dist/layout-engine/noteAreaFlow.js +83 -0
  124. package/dist/layout-engine/paginator.d.ts +10 -1
  125. package/dist/layout-engine/paginator.js +13 -1
  126. package/dist/layout-engine/tableRowBreak.js +3 -1
  127. package/dist/layout-engine/types.d.ts +89 -3
  128. package/dist/layout-engine/types.js +96 -1
  129. package/dist/layout-painter/index.d.ts +3 -0
  130. package/dist/layout-painter/renderPage.js +32 -83
  131. package/dist/layout-painter/renderParagraph.d.ts +6 -1
  132. package/dist/layout-painter/renderParagraph.js +34 -22
  133. package/dist/layout-painter/renderTable.js +26 -13
  134. package/dist/prosemirror/attrs/index.d.ts +9 -9
  135. package/dist/prosemirror/attrs/index.js +43 -11
  136. package/dist/prosemirror/conversion/fromProseDoc.js +8 -1
  137. package/dist/prosemirror/conversion/markInterner.d.ts +21 -0
  138. package/dist/prosemirror/conversion/markInterner.js +90 -0
  139. package/dist/prosemirror/conversion/toProseDoc.js +77 -41
  140. package/dist/prosemirror/extensions/core/DocExtension.js +2 -1
  141. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  142. package/dist/prosemirror/extensions/marks/markUtils.d.ts +6 -2
  143. package/dist/prosemirror/extensions/marks/markUtils.js +39 -51
  144. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +2 -1
  145. package/dist/prosemirror/listMarker.d.ts +1 -0
  146. package/dist/prosemirror/listMarker.js +2 -0
  147. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  148. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  149. package/dist/prosemirror/schema/nodes.d.ts +13 -1
  150. package/dist/prosemirror/textBoxHostParagraph.d.ts +13 -0
  151. package/dist/prosemirror/textBoxHostParagraph.js +21 -0
  152. package/dist/prosemirror/validation.js +91 -80
  153. package/dist/utils/createDocument.js +10 -1
  154. package/dist/utils/scriptSegments.d.ts +33 -4
  155. package/dist/utils/scriptSegments.js +39 -6
  156. package/dist/utils/textFormattingMerge.d.ts +17 -3
  157. package/dist/utils/textFormattingMerge.js +18 -12
  158. package/dist/utils/trailingText.d.ts +22 -0
  159. package/dist/utils/trailingText.js +32 -0
  160. 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
- /** Changes whenever the active provider changes, so layout caches cannot go stale. */
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 czechHyphenation from "hyphen/cs/index.js";
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 NONBREAKING_SPACES = /* @__PURE__ */ new Set([
63
- "\xA0",
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) && !NONBREAKING_SPACES.has(character)) {
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 && /\s/u.test(next) && !NONBREAKING_SPACES.has(next)) {
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 && /\s/u.test(previous) && !NONBREAKING_SPACES.has(previous)) {
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 && /\s/u.test(next) && !NONBREAKING_SPACES.has(next)) {
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 hyphenate = hyphenatorFor(policy?.locale);
291
- if (!hyphenate) return [];
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
- /** Changes whenever the active provider changes, so layout caches cannot go stale. */
321
- const getLineBreakProviderGeneration = () => lineBreakProviderGeneration;
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.6:
11
- * 1. explicit numbering-level rPr (`attrs.listMarkerFormatting`),
12
- * 2. first body text run's font,
13
- * 3. paragraph defaults, then document defaults.
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: non-`clear`/non-`bar` stops past the marker.
35
- * `bar` (§17.3.1.37) is a vertical line and doesn't advance the cursor.
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.6:
17
- * 1. explicit numbering-level rPr (`attrs.listMarkerFormatting`),
18
- * 2. first body text run's font,
19
- * 3. paragraph defaults, then document defaults.
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
- const bold = markerFormatting?.bold ?? firstTextRun?.bold;
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 ?? firstTextRun?.fontSize ?? attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE,
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: non-`clear`/non-`bar` stops past the marker.
70
- * `bar` (§17.3.1.37) is a vertical line and doesn't advance the cursor.
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) customTabs.push(indentLeft);
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.filter((px) => px >= searchStart).sort((a, b) => a - b)[0];
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 measureWidth = cell.noWrap === true && !columnsPinned ? NO_WRAP_MEASURE_WIDTH : cellContentWidth;
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
- if (explicitHeight && heightRule === "exact") row.height = explicitHeight;
139
- else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight + maxCellInsets);
140
- else row.height = maxCellHeightWithBorders;
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 deficit = measuredCell.height + getTableCellVerticalBorderHeight(cellGrid, sourceCell, rowIdx) - combinedHeight;
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, hasCjk, hasComplexScript, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
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 ?? "Calibri";
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 && hasCjk(measuredText)) return true;
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;