@stll/folio-core 0.47.7 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/dist/ai-edits/apply.js +565 -121
  2. package/dist/ai-edits/minimal-replacement.d.ts +64 -0
  3. package/dist/ai-edits/minimal-replacement.js +282 -0
  4. package/dist/controller/fontReadiness.js +3 -3
  5. package/dist/controller/hyphenationReadiness.d.ts +19 -0
  6. package/dist/controller/hyphenationReadiness.js +47 -0
  7. package/dist/controller/layoutPipeline.d.ts +3 -0
  8. package/dist/controller/layoutPipeline.js +70 -18
  9. package/dist/display-list/build/buildDisplayList.js +39 -12
  10. package/dist/display-list/build/pageFurniture.d.ts +7 -1
  11. package/dist/display-list/build/pageFurniture.js +21 -8
  12. package/dist/display-list/build/paragraphPrimitives.js +68 -11
  13. package/dist/display-list/build/tablePrimitives.js +10 -6
  14. package/dist/display-list/build/textBoxPrimitives.js +1 -1
  15. package/dist/display-list/build/unsupported.d.ts +2 -2
  16. package/dist/display-list/build/unsupported.js +0 -0
  17. package/dist/display-list/dom/renderDisplayListToDom.js +0 -1
  18. package/dist/docx/attributeRemainder.js +10 -2
  19. package/dist/docx/blockContentParser.d.ts +2 -0
  20. package/dist/docx/blockContentParser.js +79 -64
  21. package/dist/docx/bulletMarkers.d.ts +18 -2
  22. package/dist/docx/bulletMarkers.js +27 -2
  23. package/dist/docx/commentParser.d.ts +2 -1
  24. package/dist/docx/commentParser.js +57 -45
  25. package/dist/docx/containerChildren.d.ts +21 -8
  26. package/dist/docx/containerChildren.js +39 -9
  27. package/dist/docx/documentParser.d.ts +5 -1
  28. package/dist/docx/documentParser.js +8 -5
  29. package/dist/docx/documentSectionFacts.d.ts +22 -0
  30. package/dist/docx/documentSectionFacts.js +43 -0
  31. package/dist/docx/drawingGroupChildren.d.ts +60 -0
  32. package/dist/docx/drawingGroupChildren.js +176 -0
  33. package/dist/docx/drawingUtils.js +3 -0
  34. package/dist/docx/fieldParser.d.ts +8 -1
  35. package/dist/docx/fieldParser.js +47 -3
  36. package/dist/docx/fontTableParser.js +34 -27
  37. package/dist/docx/footnoteParser.d.ts +3 -2
  38. package/dist/docx/footnoteParser.js +15 -12
  39. package/dist/docx/groupDrawingParser.d.ts +9 -2
  40. package/dist/docx/groupDrawingParser.js +125 -37
  41. package/dist/docx/headerFooterParser.d.ts +7 -2
  42. package/dist/docx/headerFooterParser.js +15 -5
  43. package/dist/docx/hyperlinkParser.d.ts +48 -36
  44. package/dist/docx/hyperlinkParser.js +97 -82
  45. package/dist/docx/numberingParser.js +108 -84
  46. package/dist/docx/packageParts.d.ts +45 -1
  47. package/dist/docx/packageParts.js +93 -2
  48. package/dist/docx/paraIdAttribute.d.ts +3 -2
  49. package/dist/docx/paragraphParser.d.ts +5 -1
  50. package/dist/docx/paragraphParser.js +468 -365
  51. package/dist/docx/paragraphProperties.js +123 -109
  52. package/dist/docx/paragraphPropertySource.d.ts +6 -1
  53. package/dist/docx/paragraphPropertySource.js +9 -1
  54. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  55. package/dist/docx/paragraphTextBoxEnrichment.js +143 -10
  56. package/dist/docx/parser.d.ts +7 -1
  57. package/dist/docx/parser.js +33 -16
  58. package/dist/docx/previewBudget.d.ts +55 -33
  59. package/dist/docx/previewBudget.js +59 -65
  60. package/dist/docx/rezip.js +41 -58
  61. package/dist/docx/runParser.d.ts +4 -1
  62. package/dist/docx/runParser.js +182 -184
  63. package/dist/docx/sdtProperties.js +87 -84
  64. package/dist/docx/sectionParser.js +169 -160
  65. package/dist/docx/selectiveSaveFlags.d.ts +10 -3
  66. package/dist/docx/selectiveSaveFlags.js +1 -1
  67. package/dist/docx/selectiveXmlPatch.d.ts +56 -34
  68. package/dist/docx/selectiveXmlPatch.js +222 -115
  69. package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
  70. package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
  71. package/dist/docx/serializer/paragraphSerializer.js +47 -42
  72. package/dist/docx/serializer/runSerializer.d.ts +5 -1
  73. package/dist/docx/serializer/runSerializer.js +4 -3
  74. package/dist/docx/serializer/settingsSerializer.js +19 -1
  75. package/dist/docx/settingsParser.js +8 -0
  76. package/dist/docx/shapeAlternateContent.d.ts +25 -0
  77. package/dist/docx/shapeAlternateContent.js +52 -0
  78. package/dist/docx/streamingXmlParser.d.ts +6 -2
  79. package/dist/docx/streamingXmlParser.js +12 -7
  80. package/dist/docx/styleParser.js +34 -8
  81. package/dist/docx/tableParser.d.ts +21 -12
  82. package/dist/docx/tableParser.js +518 -441
  83. package/dist/docx/textBoxParser.d.ts +18 -5
  84. package/dist/docx/textBoxParser.js +12 -6
  85. package/dist/docx/verbatimCapture.d.ts +10 -8
  86. package/dist/docx/verbatimCapture.js +45 -2
  87. package/dist/docx/vmlImageParser.d.ts +15 -13
  88. package/dist/docx/vmlImageParser.js +75 -60
  89. package/dist/docx/vmlPreview.d.ts +3 -3
  90. package/dist/docx/vmlPreview.js +3 -4
  91. package/dist/docx/xmlNamespaceContext.d.ts +9 -0
  92. package/dist/docx/xmlNamespaceContext.js +54 -0
  93. package/dist/docx/xmlParser.d.ts +6 -3
  94. package/dist/docx/xmlParser.js +11 -41
  95. package/dist/docx/xmlResourceLimits.d.ts +2 -1
  96. package/dist/docx/xmlResourceLimits.js +4 -1
  97. package/dist/fonts/headlessMeasure.js +0 -0
  98. package/dist/headless-layout.d.ts +11 -2
  99. package/dist/headless-layout.js +153 -107
  100. package/dist/internal/headlessRevisionResolution.js +50 -13
  101. package/dist/internal/paragraphFormattingSerialization.js +3 -3
  102. package/dist/layout-bridge/convert/endnoteLayout.d.ts +65 -0
  103. package/dist/layout-bridge/convert/endnoteLayout.js +227 -0
  104. package/dist/layout-bridge/convert/footnoteLayout.d.ts +19 -3
  105. package/dist/layout-bridge/convert/footnoteLayout.js +40 -31
  106. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -7
  107. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -20
  108. package/dist/layout-bridge/convert/paragraphMarkFormatting.d.ts +10 -0
  109. package/dist/layout-bridge/convert/paragraphMarkFormatting.js +59 -0
  110. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +29 -1
  111. package/dist/layout-bridge/convert/toFlowBlocks.js +284 -39
  112. package/dist/layout-bridge/engine/hitTest.js +5 -3
  113. package/dist/layout-bridge/engine/selectionRects.js +5 -3
  114. package/dist/layout-engine/anchorLayoutInCellCompatibility.d.ts +10 -0
  115. package/dist/layout-engine/anchorLayoutInCellCompatibility.js +22 -0
  116. package/dist/layout-engine/index.d.ts +2 -2
  117. package/dist/layout-engine/index.js +102 -14
  118. package/dist/layout-engine/justificationCompatibility.d.ts +1 -1
  119. package/dist/layout-engine/justificationCompatibility.js +9 -5
  120. package/dist/layout-engine/keep-together.d.ts +15 -6
  121. package/dist/layout-engine/keep-together.js +43 -16
  122. package/dist/layout-engine/layoutInstrumentation.d.ts +8 -2
  123. package/dist/layout-engine/layoutInstrumentation.js +8 -1
  124. package/dist/layout-engine/measure/advanceComposition.js +50 -7
  125. package/dist/layout-engine/measure/hyphenationDictionaries.d.ts +74 -0
  126. package/dist/layout-engine/measure/hyphenationDictionaries.js +138 -0
  127. package/dist/layout-engine/measure/hyphenationPreload.d.ts +11 -0
  128. package/dist/layout-engine/measure/hyphenationPreload.js +24 -0
  129. package/dist/layout-engine/measure/lineBreakProvider.d.ts +12 -2
  130. package/dist/layout-engine/measure/lineBreakProvider.js +30 -30
  131. package/dist/layout-engine/measure/lineBreaks.d.ts +6 -1
  132. package/dist/layout-engine/measure/lineBreaks.js +10 -3
  133. package/dist/layout-engine/measure/listMarkerWidth.d.ts +9 -6
  134. package/dist/layout-engine/measure/listMarkerWidth.js +20 -22
  135. package/dist/layout-engine/measure/measureBlocks.js +46 -10
  136. package/dist/layout-engine/measure/measureContainer.js +51 -8
  137. package/dist/layout-engine/measure/measureHelpers.d.ts +15 -3
  138. package/dist/layout-engine/measure/measureHelpers.js +34 -8
  139. package/dist/layout-engine/measure/measureParagraph.js +143 -101
  140. package/dist/layout-engine/measure/measureTypes.d.ts +5 -0
  141. package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
  142. package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
  143. package/dist/layout-engine/measure/tabCalculator.d.ts +2 -0
  144. package/dist/layout-engine/measure/tabCalculator.js +2 -1
  145. package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
  146. package/dist/layout-engine/measure/tableCellFloating.js +51 -19
  147. package/dist/layout-engine/measure/tableCellGrid.d.ts +28 -3
  148. package/dist/layout-engine/measure/tableCellGrid.js +45 -5
  149. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +2 -1
  150. package/dist/layout-engine/noteAreaFlow.d.ts +19 -0
  151. package/dist/layout-engine/noteAreaFlow.js +83 -0
  152. package/dist/layout-engine/paginator.d.ts +12 -1
  153. package/dist/layout-engine/paginator.js +18 -2
  154. package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
  155. package/dist/layout-engine/paragraphSpacing.js +2 -1
  156. package/dist/layout-engine/tableRowBreak.d.ts +8 -1
  157. package/dist/layout-engine/tableRowBreak.js +25 -2
  158. package/dist/layout-engine/types.d.ts +108 -3
  159. package/dist/layout-engine/types.js +96 -1
  160. package/dist/layout-painter/index.d.ts +3 -0
  161. package/dist/layout-painter/renderPage.js +32 -83
  162. package/dist/layout-painter/renderParagraph.d.ts +6 -1
  163. package/dist/layout-painter/renderParagraph.js +75 -26
  164. package/dist/layout-painter/renderTable.js +26 -13
  165. package/dist/layout-painter/renderTextBox.d.ts +12 -2
  166. package/dist/layout-painter/renderTextBox.js +26 -2
  167. package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
  168. package/dist/prosemirror/alternateContentAttrs.js +35 -0
  169. package/dist/prosemirror/attrs/index.d.ts +9 -9
  170. package/dist/prosemirror/attrs/index.js +133 -11
  171. package/dist/prosemirror/commands/comments.js +51 -41
  172. package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
  173. package/dist/prosemirror/contentControlRevisions.js +84 -0
  174. package/dist/prosemirror/conversion/fromProseDoc.js +54 -23
  175. package/dist/prosemirror/conversion/markInterner.d.ts +21 -0
  176. package/dist/prosemirror/conversion/markInterner.js +90 -0
  177. package/dist/prosemirror/conversion/toProseDoc.js +117 -58
  178. package/dist/prosemirror/extensions/core/DocExtension.js +2 -1
  179. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  180. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
  181. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
  182. package/dist/prosemirror/extensions/marks/markUtils.d.ts +6 -2
  183. package/dist/prosemirror/extensions/marks/markUtils.js +39 -51
  184. package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
  185. package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
  186. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
  187. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +5 -1
  188. package/dist/prosemirror/listMarker.d.ts +1 -0
  189. package/dist/prosemirror/listMarker.js +4 -2
  190. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  191. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  192. package/dist/prosemirror/paragraphDirection.d.ts +29 -2
  193. package/dist/prosemirror/paragraphDirection.js +21 -2
  194. package/dist/prosemirror/plugins/suggestionMode.js +37 -8
  195. package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
  196. package/dist/prosemirror/rejoinRunCarriers.js +66 -0
  197. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
  198. package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
  199. package/dist/prosemirror/schema/nodes.d.ts +50 -6
  200. package/dist/prosemirror/textBoxHostParagraph.d.ts +13 -0
  201. package/dist/prosemirror/textBoxHostParagraph.js +21 -0
  202. package/dist/prosemirror/validation.js +91 -80
  203. package/dist/types/content.d.ts +2 -2
  204. package/dist/utils/createDocument.js +10 -1
  205. package/dist/utils/scriptSegments.d.ts +33 -4
  206. package/dist/utils/scriptSegments.js +39 -6
  207. package/dist/utils/textFormattingMerge.d.ts +17 -3
  208. package/dist/utils/textFormattingMerge.js +18 -12
  209. package/dist/utils/trailingText.d.ts +22 -0
  210. package/dist/utils/trailingText.js +32 -0
  211. package/package.json +2 -2
  212. package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
  213. package/dist/layout-engine/justifiedLineFit.js +0 -6
@@ -1,6 +1,8 @@
1
1
  import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
2
- import { SCRIPT_CLASS, hasComplexScript, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
2
+ import { SCRIPT_CLASS, hasComplexScript, hasEastAsiaSlotText, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
3
3
  import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./complexScriptFormatting.js";
4
+ import "./measureHelpers.js";
5
+ import { SMALL_CAPS_SCALE, smallCapsMask, smallCapsSegments } from "./smallCapsCasing.js";
4
6
  //#region src/layout-engine/measure/advanceComposition.ts
5
7
  /**
6
8
  * The arithmetic that turns raw glyph advances into measured widths.
@@ -48,9 +50,38 @@ const glyphAdvanceStyle = (style) => {
48
50
  if (style.kerning !== void 0) result.kerning = style.kerning;
49
51
  return result;
50
52
  };
51
- const needsPerScriptFonts = (style, text) => style.eastAsiaFontFamily !== void 0 && [...text].some((c) => isEastAsia(c)) || hasComplexScriptFormatting(style) && hasComplexScript(text);
52
- const isEastAsia = (char) => {
53
- return scriptClassOf(char.codePointAt(0)) === SCRIPT_CLASS.eastAsia;
53
+ const needsPerScriptFonts = (style, text) => style.eastAsiaFontFamily !== void 0 && hasEastAsiaSlotText(text, style.eastAsiaHint) || hasComplexScriptFormatting(style) && hasComplexScript(text);
54
+ /**
55
+ * Width of a `w:smallCaps` string: its lowercase letters (and the whitespace
56
+ * that follows them — see smallCapsCasing.ts) draw at `SMALL_CAPS_SCALE` of
57
+ * the style's size, everything else at full size. A small segment is measured
58
+ * at that literal, smaller size — not the full-size advance scaled by
59
+ * arithmetic — mirroring {@link canvasMeasureTextWidth}'s
60
+ * `measureSmallCapsWidth` so the two backends agree; each segment still
61
+ * resolves its own per-script font, the same as the plain (non-small-caps)
62
+ * path below.
63
+ */
64
+ const smallCapsGlyphWidth = (text, style, advanceOf) => {
65
+ const segmentStyle = { ...style };
66
+ delete segmentStyle.fontVariant;
67
+ delete segmentStyle.letterSpacing;
68
+ delete segmentStyle.horizontalScale;
69
+ const smallSegmentStyle = {
70
+ ...segmentStyle,
71
+ fontSize: (segmentStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
72
+ };
73
+ let total = 0;
74
+ for (const segment of smallCapsSegments(text)) {
75
+ const activeStyle = segment.small ? smallSegmentStyle : segmentStyle;
76
+ total += needsPerScriptFonts(activeStyle, segment.text) ? segmentByScript(segment.text, activeStyle.eastAsiaHint).reduce((sum, scriptSegment) => sum + advanceOf(scriptSegment.text, glyphAdvanceStyle(scriptStyle(activeStyle, scriptSegment.script))), 0) : advanceOf(segment.text, activeStyle);
77
+ }
78
+ return total;
79
+ };
80
+ /** The un-spaced, un-scaled glyph width `composeTextWidth` spaces and scales. */
81
+ const glyphWidthOf = ({ source, style, transformed, letterSpacing, advanceOf }) => {
82
+ if (!source.forceComplexScript && style.fontVariant === "small-caps") return smallCapsGlyphWidth(transformed, style, advanceOf);
83
+ if (!source.forceComplexScript && !letterSpacing && needsPerScriptFonts(style, transformed)) return segmentByScript(transformed, style.eastAsiaHint).reduce((sum, segment) => sum + advanceOf(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script))), 0);
84
+ return advanceOf(transformed, style);
54
85
  };
55
86
  /**
56
87
  * Width of a string: the advance width used for line breaking, never the
@@ -62,7 +93,13 @@ const composeTextWidth = ({ text, style: source, advanceOf }) => {
62
93
  const transformed = applyMeasurementTextTransform(text, style);
63
94
  const letterSpacing = style.letterSpacing ?? 0;
64
95
  const horizontalScale = getHorizontalScaleFactor(style.horizontalScale);
65
- const glyphWidth = !source.forceComplexScript && !letterSpacing && needsPerScriptFonts(style, transformed) ? segmentByScript(transformed).reduce((sum, segment) => sum + advanceOf(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script))), 0) : advanceOf(transformed, style);
96
+ const glyphWidth = glyphWidthOf({
97
+ source,
98
+ style,
99
+ transformed,
100
+ letterSpacing,
101
+ advanceOf
102
+ });
66
103
  const codePoints = countCodePoints(transformed);
67
104
  return (letterSpacing && codePoints > 1 ? glyphWidth + letterSpacing * (codePoints - 1) : glyphWidth) * horizontalScale;
68
105
  };
@@ -95,10 +132,16 @@ const composeRunMeasurement = ({ text, style: source, metrics, advanceOf, advanc
95
132
  perScript
96
133
  });
97
134
  let codePointIndex = 0;
135
+ const smallCapsAt = style.fontVariant === "small-caps" ? smallCapsMask(text) : void 0;
98
136
  for (const char of text) {
99
137
  const cp = char.codePointAt(0);
100
- const charStyle = perScript ? scriptStyle(style, scriptClassOf(cp)) : style;
101
- let charWidth = shaped.get(codePointIndex) ?? advanceOf(applyMeasurementTextTransform(char, style), charStyle);
138
+ const isSmallCap = smallCapsAt?.[codePointIndex] ?? false;
139
+ const scriptedStyle = perScript ? scriptStyle(style, scriptClassOf(cp, style.eastAsiaHint)) : style;
140
+ const charStyle = isSmallCap ? {
141
+ ...scriptedStyle,
142
+ fontSize: (scriptedStyle.fontSize ?? 11) * SMALL_CAPS_SCALE
143
+ } : scriptedStyle;
144
+ let charWidth = shaped.get(codePointIndex) ?? advanceOf(isSmallCap ? char.toLocaleUpperCase() : applyMeasurementTextTransform(char, style), charStyle);
102
145
  if (letterSpacing && offset + char.length < text.length) charWidth += letterSpacing;
103
146
  charWidth *= scale;
104
147
  charWidths.push(charWidth);
@@ -0,0 +1,74 @@
1
+ //#region src/layout-engine/measure/hyphenationDictionaries.d.ts
2
+ /**
3
+ * On-demand hyphenation dictionaries for automatic hyphenation.
4
+ *
5
+ * Each `hyphen` pattern set is tens to hundreds of kilobytes of source whose
6
+ * evaluation builds a pattern trie, and most documents never enable
7
+ * `w:autoHyphenation`. A dictionary is therefore imported only once measurement
8
+ * asks to hyphenate a word in its language. Measurement is synchronous, so the
9
+ * first request starts the import and hyphenates nothing. A layout run wrapped
10
+ * in {@link collectRequestedHyphenationDictionaries} learns which dictionaries
11
+ * it lacked, so its own editor can re-run layout on load or report the failure
12
+ * (`controller/hyphenationReadiness`). Hosts that must lay out correctly on the
13
+ * first pass (headless layout, tests) await `preloadHyphenationDictionaries`
14
+ * (`./hyphenationPreload`) before measuring.
15
+ *
16
+ * This module sits on every editor's import graph, so it speaks in its own
17
+ * settled-state union rather than `Result`, keeping the `Result` API out of
18
+ * adapters' type checks; the preload boundary converts.
19
+ */
20
+ type HyphenateWord = (text: string) => string;
21
+ declare const HYPHENATION_DICTIONARY_IDS: readonly ["cs", "en-gb", "en-us", "sk"];
22
+ type HyphenationDictionaryId = (typeof HYPHENATION_DICTIONARY_IDS)[number];
23
+ type HyphenationDictionaryLoaders = Readonly<Record<HyphenationDictionaryId, () => Promise<HyphenateWord>>>;
24
+ declare const HyphenationDictionaryError_base: import("better-result").TaggedErrorClass<"HyphenationDictionaryError">;
25
+ declare class HyphenationDictionaryError extends HyphenationDictionaryError_base<{
26
+ message: string;
27
+ dictionary: HyphenationDictionaryId;
28
+ cause: unknown;
29
+ }> {}
30
+ type SettledHyphenationDictionary = Readonly<{
31
+ status: "loaded";
32
+ hyphenate: HyphenateWord;
33
+ }> | Readonly<{
34
+ status: "failed";
35
+ error: HyphenationDictionaryError;
36
+ }>;
37
+ type HyphenationDictionaryState = Readonly<{
38
+ status: "unloaded";
39
+ }> | Readonly<{
40
+ status: "loading";
41
+ promise: Promise<SettledHyphenationDictionary>;
42
+ }> | SettledHyphenationDictionary;
43
+ /** Compare before and after a measurement to tell whether it lacked a dictionary. */
44
+ declare const getHyphenationDictionaryMisses: () => number;
45
+ /** The dictionary that hyphenates text in `locale`, if one is bundled. */
46
+ declare const hyphenationDictionaryFor: (locale: string | undefined) => HyphenationDictionaryId | undefined;
47
+ /** Changes whenever a dictionary finishes loading, so measurement caches cannot go stale. */
48
+ declare const getHyphenationDictionaryGeneration: () => number;
49
+ /** Load `dictionary` once; later calls share the load or its settled outcome. */
50
+ declare const requestHyphenationDictionary: (dictionary: HyphenationDictionaryId) => Promise<SettledHyphenationDictionary>;
51
+ /**
52
+ * The loaded hyphenator for `dictionary`, or `undefined` while it is not
53
+ * available. An unloaded dictionary starts loading, and a missing one is
54
+ * recorded against the enclosing {@link collectRequestedHyphenationDictionaries}
55
+ * run; the caller hyphenates nothing now.
56
+ */
57
+ declare const hyphenatorOrRequest: (dictionary: HyphenationDictionaryId) => HyphenateWord | undefined;
58
+ type CollectedHyphenationDictionaries<T> = {
59
+ result: T;
60
+ /** Dictionaries the run needed and did not have: loading or failed. */
61
+ missing: ReadonlySet<HyphenationDictionaryId>;
62
+ };
63
+ /** Run a synchronous layout pass and report the dictionaries it lacked. */
64
+ declare const collectRequestedHyphenationDictionaries: <T>(run: () => T) => CollectedHyphenationDictionaries<T>;
65
+ /**
66
+ * Test seam: forget every dictionary so a test observes the unloaded path, and
67
+ * optionally load through `loaders` (for example, ones that fail) until the
68
+ * next reset.
69
+ */
70
+ declare const resetHyphenationDictionaries: (loaders?: HyphenationDictionaryLoaders) => void;
71
+ /** Test seam: which dictionaries have been requested or loaded. */
72
+ declare const hyphenationDictionaryStatus: (dictionary: HyphenationDictionaryId) => HyphenationDictionaryState["status"];
73
+ //#endregion
74
+ export { CollectedHyphenationDictionaries, HyphenateWord, HyphenationDictionaryError, HyphenationDictionaryId, HyphenationDictionaryLoaders, SettledHyphenationDictionary, collectRequestedHyphenationDictionaries, getHyphenationDictionaryGeneration, getHyphenationDictionaryMisses, hyphenationDictionaryFor, hyphenationDictionaryStatus, hyphenatorOrRequest, requestHyphenationDictionary, resetHyphenationDictionaries };
@@ -0,0 +1,138 @@
1
+ import { recordHyphenationDictionaryError } from "../layoutInstrumentation.js";
2
+ import { TaggedError } from "better-result";
3
+ //#region src/layout-engine/measure/hyphenationDictionaries.ts
4
+ /**
5
+ * On-demand hyphenation dictionaries for automatic hyphenation.
6
+ *
7
+ * Each `hyphen` pattern set is tens to hundreds of kilobytes of source whose
8
+ * evaluation builds a pattern trie, and most documents never enable
9
+ * `w:autoHyphenation`. A dictionary is therefore imported only once measurement
10
+ * asks to hyphenate a word in its language. Measurement is synchronous, so the
11
+ * first request starts the import and hyphenates nothing. A layout run wrapped
12
+ * in {@link collectRequestedHyphenationDictionaries} learns which dictionaries
13
+ * it lacked, so its own editor can re-run layout on load or report the failure
14
+ * (`controller/hyphenationReadiness`). Hosts that must lay out correctly on the
15
+ * first pass (headless layout, tests) await `preloadHyphenationDictionaries`
16
+ * (`./hyphenationPreload`) before measuring.
17
+ *
18
+ * This module sits on every editor's import graph, so it speaks in its own
19
+ * settled-state union rather than `Result`, keeping the `Result` API out of
20
+ * adapters' type checks; the preload boundary converts.
21
+ */
22
+ const HYPHENATION_DICTIONARY_IDS = [
23
+ "cs",
24
+ "en-gb",
25
+ "en-us",
26
+ "sk"
27
+ ];
28
+ const HYPHENATION_DICTIONARY_LOADERS = {
29
+ cs: async () => (await import("hyphen/cs/index.js")).default.hyphenateSync,
30
+ "en-gb": async () => (await import("hyphen/en-gb/index.js")).default.hyphenateSync,
31
+ "en-us": async () => (await import("hyphen/en-us/index.js")).default.hyphenateSync,
32
+ sk: async () => (await import("hyphen/sk/index.js")).default.hyphenateSync
33
+ };
34
+ let dictionaryLoaders = HYPHENATION_DICTIONARY_LOADERS;
35
+ var HyphenationDictionaryError = class extends TaggedError("HyphenationDictionaryError") {};
36
+ const createInitialStates = () => ({
37
+ cs: { status: "unloaded" },
38
+ "en-gb": { status: "unloaded" },
39
+ "en-us": { status: "unloaded" },
40
+ sk: { status: "unloaded" }
41
+ });
42
+ let dictionaryStates = createInitialStates();
43
+ let dictionaryGeneration = 0;
44
+ let activeRequests;
45
+ let dictionaryMisses = 0;
46
+ /** Compare before and after a measurement to tell whether it lacked a dictionary. */
47
+ const getHyphenationDictionaryMisses = () => dictionaryMisses;
48
+ /** The dictionary that hyphenates text in `locale`, if one is bundled. */
49
+ const hyphenationDictionaryFor = (locale) => {
50
+ const normalized = locale?.trim().replaceAll("_", "-").toLowerCase();
51
+ if (!normalized) return;
52
+ return HYPHENATION_DICTIONARY_IDS.find((id) => normalized === id || normalized.startsWith(`${id}-`));
53
+ };
54
+ /** Changes whenever a dictionary finishes loading, so measurement caches cannot go stale. */
55
+ const getHyphenationDictionaryGeneration = () => dictionaryGeneration;
56
+ const importDictionary = (dictionary) => dictionaryLoaders[dictionary]().then((hyphenate) => ({
57
+ status: "loaded",
58
+ hyphenate
59
+ }), (cause) => ({
60
+ status: "failed",
61
+ error: new HyphenationDictionaryError({
62
+ message: `The ${dictionary} hyphenation dictionary could not be loaded.`,
63
+ dictionary,
64
+ cause
65
+ })
66
+ }));
67
+ const startLoad = (dictionary) => {
68
+ const promise = importDictionary(dictionary).then((settled) => {
69
+ if (dictionaryStates[dictionary].status !== "loading") return settled;
70
+ dictionaryStates[dictionary] = settled;
71
+ switch (settled.status) {
72
+ case "failed":
73
+ recordHyphenationDictionaryError(dictionary, settled.error);
74
+ return settled;
75
+ case "loaded":
76
+ dictionaryGeneration += 1;
77
+ return settled;
78
+ default: return settled;
79
+ }
80
+ });
81
+ dictionaryStates[dictionary] = {
82
+ status: "loading",
83
+ promise
84
+ };
85
+ return promise;
86
+ };
87
+ /** Load `dictionary` once; later calls share the load or its settled outcome. */
88
+ const requestHyphenationDictionary = (dictionary) => {
89
+ const state = dictionaryStates[dictionary];
90
+ switch (state.status) {
91
+ case "unloaded": return startLoad(dictionary);
92
+ case "loading": return state.promise;
93
+ case "loaded":
94
+ case "failed": return Promise.resolve(state);
95
+ default: return state;
96
+ }
97
+ };
98
+ /**
99
+ * The loaded hyphenator for `dictionary`, or `undefined` while it is not
100
+ * available. An unloaded dictionary starts loading, and a missing one is
101
+ * recorded against the enclosing {@link collectRequestedHyphenationDictionaries}
102
+ * run; the caller hyphenates nothing now.
103
+ */
104
+ const hyphenatorOrRequest = (dictionary) => {
105
+ const state = dictionaryStates[dictionary];
106
+ if (state.status === "loaded") return state.hyphenate;
107
+ dictionaryMisses += 1;
108
+ activeRequests?.add(dictionary);
109
+ if (state.status === "unloaded") startLoad(dictionary);
110
+ };
111
+ /** Run a synchronous layout pass and report the dictionaries it lacked. */
112
+ const collectRequestedHyphenationDictionaries = (run) => {
113
+ const outer = activeRequests;
114
+ const missing = /* @__PURE__ */ new Set();
115
+ activeRequests = missing;
116
+ try {
117
+ return {
118
+ result: run(),
119
+ missing
120
+ };
121
+ } finally {
122
+ activeRequests = outer;
123
+ }
124
+ };
125
+ /**
126
+ * Test seam: forget every dictionary so a test observes the unloaded path, and
127
+ * optionally load through `loaders` (for example, ones that fail) until the
128
+ * next reset.
129
+ */
130
+ const resetHyphenationDictionaries = (loaders = HYPHENATION_DICTIONARY_LOADERS) => {
131
+ dictionaryLoaders = loaders;
132
+ dictionaryStates = createInitialStates();
133
+ dictionaryGeneration += 1;
134
+ };
135
+ /** Test seam: which dictionaries have been requested or loaded. */
136
+ const hyphenationDictionaryStatus = (dictionary) => dictionaryStates[dictionary].status;
137
+ //#endregion
138
+ export { HyphenationDictionaryError, collectRequestedHyphenationDictionaries, getHyphenationDictionaryGeneration, getHyphenationDictionaryMisses, hyphenationDictionaryFor, hyphenationDictionaryStatus, hyphenatorOrRequest, requestHyphenationDictionary, resetHyphenationDictionaries };
@@ -0,0 +1,11 @@
1
+ import { HyphenationDictionaryError } from "./hyphenationDictionaries.js";
2
+ import { Result } from "better-result";
3
+ //#region src/layout-engine/measure/hyphenationPreload.d.ts
4
+ /**
5
+ * Load the dictionaries `locales` need before measuring, so a synchronous
6
+ * layout pass hyphenates on its first run. Locales without a dictionary are
7
+ * ignored.
8
+ */
9
+ declare const preloadHyphenationDictionaries: (locales: Iterable<string>) => Promise<Result<void, HyphenationDictionaryError>>;
10
+ //#endregion
11
+ export { preloadHyphenationDictionaries };
@@ -0,0 +1,24 @@
1
+ import { hyphenationDictionaryFor, requestHyphenationDictionary } from "./hyphenationDictionaries.js";
2
+ import { Result } from "better-result";
3
+ //#region src/layout-engine/measure/hyphenationPreload.ts
4
+ /**
5
+ * Awaitable dictionary loading for hosts that measure once and cannot re-run
6
+ * layout when a dictionary arrives (headless layout, tests).
7
+ */
8
+ /**
9
+ * Load the dictionaries `locales` need before measuring, so a synchronous
10
+ * layout pass hyphenates on its first run. Locales without a dictionary are
11
+ * ignored.
12
+ */
13
+ const preloadHyphenationDictionaries = async (locales) => {
14
+ const dictionaries = /* @__PURE__ */ new Set();
15
+ for (const locale of locales) {
16
+ const dictionary = hyphenationDictionaryFor(locale);
17
+ if (dictionary !== void 0) dictionaries.add(dictionary);
18
+ }
19
+ const settled = await Promise.all([...dictionaries].map(requestHyphenationDictionary));
20
+ for (const outcome of settled) if (outcome.status === "failed") return Result.err(outcome.error);
21
+ return Result.ok(void 0);
22
+ };
23
+ //#endregion
24
+ export { preloadHyphenationDictionaries };
@@ -34,11 +34,21 @@ type LineBreakProvider = {
34
34
  isHangingPunctuation?: (text: string, policy?: LineBreakPolicy) => boolean;
35
35
  };
36
36
  declare const MAX_HYPHENATION_WORD_LENGTH = 256;
37
+ declare const isNonBreakingGlue: (character: string | undefined) => boolean;
38
+ /**
39
+ * Whether glue characters permit a line break between `previous` and `next`.
40
+ * Only the character after the break matters here: glue characters are never
41
+ * break opportunities themselves, so a break after one is never proposed.
42
+ */
43
+ declare const gluePermitsBreakBefore: (previous: string | undefined, next: string | undefined) => boolean;
37
44
  declare const defaultLineBreakProvider: LineBreakProvider;
38
45
  declare const getLineBreakProvider: () => LineBreakProvider;
39
- /** 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;