@stll/folio-core 0.11.0 → 0.13.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 (263) hide show
  1. package/dist/ai-edits/apply.d.ts +2 -1
  2. package/dist/ai-edits/apply.js +693 -63
  3. package/dist/ai-edits/block-identity.d.ts +4 -0
  4. package/dist/ai-edits/block-identity.js +9 -0
  5. package/dist/ai-edits/blockRange.d.ts +28 -1
  6. package/dist/ai-edits/blockRange.js +68 -2
  7. package/dist/ai-edits/clean-text.js +2 -1
  8. package/dist/ai-edits/headless.d.ts +4 -4
  9. package/dist/ai-edits/headless.js +9 -9
  10. package/dist/ai-edits/index.d.ts +2 -2
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.d.ts +7 -7
  13. package/dist/ai-edits/read.js +110 -3
  14. package/dist/ai-edits/snapshot.js +21 -3
  15. package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
  16. package/dist/ai-edits/table-cell-mutations.js +238 -0
  17. package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
  18. package/dist/ai-edits/table-mutation-plan.js +90 -0
  19. package/dist/ai-edits/table-row-column-mutations.d.ts +108 -0
  20. package/dist/ai-edits/table-row-column-mutations.js +300 -0
  21. package/dist/ai-edits/table-targets.d.ts +32 -0
  22. package/dist/ai-edits/table-targets.js +70 -0
  23. package/dist/ai-edits/types.d.ts +62 -1
  24. package/dist/ai-edits/word-diff.js +21 -0
  25. package/dist/compat/eigenpal.d.ts +3 -3
  26. package/dist/compat/eigenpal.js +3 -3
  27. package/dist/content-controls/mutateContentControls.js +1 -1
  28. package/dist/controller/activeEditorStory.d.ts +36 -0
  29. package/dist/controller/activeEditorStory.js +30 -0
  30. package/dist/controller/contentControlWidgetController.d.ts +55 -0
  31. package/dist/controller/contentControlWidgetController.js +141 -0
  32. package/dist/controller/fontReadiness.js +2 -0
  33. package/dist/controller/headerFooterEditorManager.js +2 -2
  34. package/dist/controller/hiddenEditorApi.js +2 -2
  35. package/dist/controller/hiddenEditorManager.d.ts +12 -1
  36. package/dist/controller/hiddenEditorManager.js +44 -12
  37. package/dist/controller/layoutPipeline.d.ts +2 -0
  38. package/dist/controller/layoutPipeline.js +17 -2
  39. package/dist/controller/noteEditorManager.d.ts +36 -0
  40. package/dist/controller/noteEditorManager.js +210 -0
  41. package/dist/document-operations.d.ts +43 -11
  42. package/dist/document-operations.js +186 -18
  43. package/dist/docx/blockContentParser.js +21 -7
  44. package/dist/docx/commentParser.js +6 -1
  45. package/dist/docx/commentRangeIntegrity.d.ts +6 -0
  46. package/dist/docx/commentRangeIntegrity.js +186 -0
  47. package/dist/docx/conformance.js +62 -1
  48. package/dist/docx/documentGrid.d.ts +8 -0
  49. package/dist/docx/documentGrid.js +8 -0
  50. package/dist/docx/drawingUtils.js +1 -1
  51. package/dist/docx/encryption/agileDecryption.js +12 -2
  52. package/dist/docx/encryption/compoundFile.js +6 -1
  53. package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
  54. package/dist/docx/encryption/encryptionInfo.js +18 -2
  55. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  56. package/dist/docx/ensureParaIds.js +8 -7
  57. package/dist/docx/fieldParser.js +1 -1
  58. package/dist/docx/groupDrawingParser.js +10 -5
  59. package/dist/docx/hyperlinkParser.js +1 -1
  60. package/dist/docx/imageParser.js +10 -6
  61. package/dist/docx/metadataPrivacy.js +17 -3
  62. package/dist/docx/notePropertiesParser.js +1 -1
  63. package/dist/docx/numberingParser.js +1 -1
  64. package/dist/docx/paragraphParser.js +70 -61
  65. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  66. package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
  67. package/dist/docx/paragraphTraversal.js +1 -1
  68. package/dist/docx/parser.js +3 -3
  69. package/dist/docx/rezip.js +20 -9
  70. package/dist/docx/runParser.js +17 -6
  71. package/dist/docx/sdtProperties.js +1 -1
  72. package/dist/docx/sectionParser.js +18 -5
  73. package/dist/docx/selectiveSave.js +5 -1
  74. package/dist/docx/selectiveXmlPatch.d.ts +22 -1
  75. package/dist/docx/selectiveXmlPatch.js +61 -1
  76. package/dist/docx/serializer/blockSdtSerializer.js +3 -2
  77. package/dist/docx/serializer/commentSerializer.js +5 -6
  78. package/dist/docx/serializer/documentSerializer.js +2 -2
  79. package/dist/docx/serializer/headerFooterSerializer.js +2 -2
  80. package/dist/docx/serializer/noteSerializer.js +2 -2
  81. package/dist/docx/serializer/paragraphSerializer.js +23 -15
  82. package/dist/docx/serializer/runSerializer.js +25 -21
  83. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
  84. package/dist/docx/serializer/stylesSerializer.js +1 -1
  85. package/dist/docx/serializer/tableSerializer.d.ts +5 -4
  86. package/dist/docx/serializer/tableSerializer.js +26 -20
  87. package/dist/docx/serializer/xmlUtils.d.ts +13 -1
  88. package/dist/docx/serializer/xmlUtils.js +27 -1
  89. package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
  90. package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
  91. package/dist/docx/server/boundedArchive.d.ts +14 -3
  92. package/dist/docx/server/boundedArchive.js +56 -11
  93. package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
  94. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
  95. package/dist/docx/server/extractDocxText.js +55 -8
  96. package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
  97. package/dist/docx/server/inspectDocxPackage.js +217 -0
  98. package/dist/docx/server/validateDocxConformance.d.ts +41 -0
  99. package/dist/docx/server/validateDocxConformance.js +336 -0
  100. package/dist/docx/settingsParser.js +26 -3
  101. package/dist/docx/shapeParser.js +1 -1
  102. package/dist/docx/styleParser.js +4 -2
  103. package/dist/docx/tableParser.d.ts +8 -1
  104. package/dist/docx/tableParser.js +31 -10
  105. package/dist/docx/textBoxParser.d.ts +3 -3
  106. package/dist/docx/textBoxParser.js +16 -12
  107. package/dist/docx/unzip.js +1 -0
  108. package/dist/docx/vmlImageParser.js +11 -3
  109. package/dist/docx/xmlParser.js +11 -1
  110. package/dist/docx/xmlSafety.d.ts +5 -0
  111. package/dist/docx/xmlSafety.js +9 -0
  112. package/dist/fields/resolveFieldValues.js +1 -3
  113. package/dist/fonts/embeddedFonts.d.ts +33 -5
  114. package/dist/fonts/embeddedFonts.js +37 -8
  115. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  116. package/dist/i18n/messages/catalogs.gen.js +17 -0
  117. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  118. package/dist/index.d.ts +3 -3
  119. package/dist/index.js +3 -3
  120. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  121. package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
  122. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  123. package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
  124. package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
  125. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  126. package/dist/layout-bridge/convert/toFlowBlocks.js +151 -20
  127. package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
  128. package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
  129. package/dist/layout-bridge/engine/clickToPosition.js +2 -2
  130. package/dist/layout-engine/index.d.ts +2 -2
  131. package/dist/layout-engine/index.js +58 -30
  132. package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
  133. package/dist/layout-engine/justificationCompatibility.js +8 -0
  134. package/dist/layout-engine/keep-together.js +11 -1
  135. package/dist/layout-engine/measure/cache.js +23 -5
  136. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  137. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  138. package/dist/layout-engine/measure/lineBreakProvider.js +56 -37
  139. package/dist/layout-engine/measure/measureBlocks.js +15 -4
  140. package/dist/layout-engine/measure/measureContainer.js +1 -1
  141. package/dist/layout-engine/measure/measureHelpers.d.ts +11 -2
  142. package/dist/layout-engine/measure/measureHelpers.js +15 -4
  143. package/dist/layout-engine/measure/measureParagraph.js +85 -25
  144. package/dist/layout-engine/measure/measureProvider.js +4 -3
  145. package/dist/layout-engine/measure/tableCellFlow.js +5 -2
  146. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
  147. package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
  148. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
  149. package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
  150. package/dist/layout-engine/paginator.js +5 -1
  151. package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
  152. package/dist/layout-engine/paragraphSpacing.js +107 -13
  153. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  154. package/dist/layout-engine/types.d.ts +48 -15
  155. package/dist/layout-painter/registry/modules/textBox.js +5 -1
  156. package/dist/layout-painter/renderImage.d.ts +12 -1
  157. package/dist/layout-painter/renderImage.js +21 -4
  158. package/dist/layout-painter/renderPage.d.ts +5 -2
  159. package/dist/layout-painter/renderPage.js +59 -14
  160. package/dist/layout-painter/renderParagraph.js +55 -15
  161. package/dist/layout-painter/renderTable.d.ts +5 -1
  162. package/dist/layout-painter/renderTable.js +7 -3
  163. package/dist/layout-painter/renderTextBox.d.ts +2 -1
  164. package/dist/layout-painter/renderTextBox.js +25 -14
  165. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  166. package/dist/layout-painter/renderWatermark.js +2 -1
  167. package/dist/managers/DocumentLoaderManager.js +1 -1
  168. package/dist/markdown/fromMarkdown.js +1 -1
  169. package/dist/model.js +1 -1
  170. package/dist/prosemirror/attrs/index.d.ts +4 -2
  171. package/dist/prosemirror/attrs/index.js +175 -18
  172. package/dist/prosemirror/commands/comments.d.ts +79 -4
  173. package/dist/prosemirror/commands/comments.js +622 -13
  174. package/dist/prosemirror/commands/contentControls.js +1 -1
  175. package/dist/prosemirror/commands/hyperlink.js +15 -1
  176. package/dist/prosemirror/commands/image.js +7 -1
  177. package/dist/prosemirror/commands/index.d.ts +2 -2
  178. package/dist/prosemirror/commands/index.js +2 -2
  179. package/dist/prosemirror/commands/pageBreak.js +1 -1
  180. package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
  181. package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
  182. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
  183. package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
  184. package/dist/prosemirror/commentIdAllocator.js +13 -3
  185. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
  186. package/dist/prosemirror/conversion/fromProseDoc.js +404 -62
  187. package/dist/prosemirror/conversion/index.js +1 -1
  188. package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
  189. package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
  190. package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
  191. package/dist/prosemirror/conversion/toProseDoc.js +364 -103
  192. package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
  193. package/dist/prosemirror/extensions/StarterKit.js +6 -1
  194. package/dist/prosemirror/extensions/core/ParagraphExtension.js +6 -2
  195. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
  196. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  197. package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
  198. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
  199. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
  200. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
  201. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
  202. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  203. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
  204. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  205. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
  206. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +70 -11
  207. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  208. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
  209. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
  210. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  211. package/dist/prosemirror/extensions/nodes/TableExtension.js +14 -6
  212. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
  213. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
  214. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
  215. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
  216. package/dist/prosemirror/index.d.ts +1 -1
  217. package/dist/prosemirror/index.js +1 -1
  218. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +36 -10
  219. package/dist/prosemirror/plugins/contentControlWidgets.js +128 -94
  220. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  221. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  222. package/dist/prosemirror/plugins/selectionTracker.js +1 -0
  223. package/dist/prosemirror/plugins/suggestionMode.js +5 -4
  224. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  225. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  226. package/dist/prosemirror/schema/index.d.ts +2 -2
  227. package/dist/prosemirror/schema/marks.d.ts +30 -3
  228. package/dist/prosemirror/schema/marks.js +15 -0
  229. package/dist/prosemirror/schema/nodes.d.ts +111 -6
  230. package/dist/prosemirror/selectionState.js +1 -0
  231. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  232. package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
  233. package/dist/prosemirror/tableCellMergeRevision.js +22 -0
  234. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
  235. package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
  236. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
  237. package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
  238. package/dist/prosemirror/validation.js +9 -1
  239. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  240. package/dist/server.d.ts +7 -3
  241. package/dist/server.js +7 -3
  242. package/dist/utils/clipboard.d.ts +10 -1
  243. package/dist/utils/clipboard.js +33 -6
  244. package/dist/utils/colorResolver.d.ts +8 -1
  245. package/dist/utils/colorResolver.js +35 -5
  246. package/dist/utils/fontFamilyMerge.js +9 -11
  247. package/dist/utils/fontLoader.js +3 -2
  248. package/dist/utils/fontResolver.d.ts +2 -1
  249. package/dist/utils/fontResolver.js +31 -7
  250. package/dist/utils/fontWeights.d.ts +1 -1
  251. package/dist/utils/fontWeights.js +1 -1
  252. package/dist/utils/hexId.d.ts +11 -1
  253. package/dist/utils/hexId.js +11 -1
  254. package/dist/utils/paragraphFormattingMerge.js +1 -0
  255. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  256. package/dist/utils/sanitizeImageSrc.js +30 -0
  257. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  258. package/dist/utils/stripXmlDeclarations.js +35 -0
  259. package/dist/version-comparison.d.ts +22 -2
  260. package/dist/version-comparison.js +41 -12
  261. package/dist/watermark/index.d.ts +11 -1
  262. package/dist/watermark/index.js +22 -1
  263. package/package.json +3 -2
@@ -1,10 +1,18 @@
1
+ import { clearFontResolvedCache } from "./measureHelpers.js";
1
2
  import { getLineBreakProviderGeneration } from "./lineBreakProvider.js";
2
3
  import { lineBreakPolicyCacheParts } from "./effectiveLineBreakPolicy.js";
3
4
  //#region src/layout-engine/measure/cache.ts
4
5
  /**
6
+ * Default max entries for text width cache
7
+ * Large documents (30+ pages) can generate 20,000+ unique text measurements.
8
+ * A generous default avoids cache thrashing on big docs.
9
+ */
10
+ const DEFAULT_TEXT_CACHE_SIZE = 2e4;
11
+ const TEXT_CACHE_REFRESH_AFTER_HITS = 16;
12
+ /**
5
13
  * Current max size for text width cache
6
14
  */
7
- let textCacheMaxSize = 2e4;
15
+ let textCacheMaxSize = DEFAULT_TEXT_CACHE_SIZE;
8
16
  /**
9
17
  * LRU cache for text width measurements
10
18
  * Key format: "text|font|letterSpacing"
@@ -34,8 +42,12 @@ function getCachedTextWidth(text, font, letterSpacing = 0) {
34
42
  const key = makeTextKey(text, font, letterSpacing);
35
43
  const entry = textWidthCache.get(key);
36
44
  if (entry !== void 0) {
37
- textWidthCache.delete(key);
38
- textWidthCache.set(key, entry);
45
+ entry.hitsSinceRefresh += 1;
46
+ if (entry.hitsSinceRefresh >= TEXT_CACHE_REFRESH_AFTER_HITS) {
47
+ entry.hitsSinceRefresh = 0;
48
+ textWidthCache.delete(key);
49
+ textWidthCache.set(key, entry);
50
+ }
39
51
  return entry.width;
40
52
  }
41
53
  }
@@ -44,8 +56,11 @@ function getCachedTextWidth(text, font, letterSpacing = 0) {
44
56
  */
45
57
  function setCachedTextWidth(text, font, letterSpacing, width) {
46
58
  const key = makeTextKey(text, font, letterSpacing);
47
- textWidthCache.set(key, { width });
48
- evictTextEntries();
59
+ textWidthCache.set(key, {
60
+ hitsSinceRefresh: 0,
61
+ width
62
+ });
63
+ if (textWidthCache.size > textCacheMaxSize) evictTextEntries();
49
64
  }
50
65
  /**
51
66
  * Clear the text width cache
@@ -169,6 +184,8 @@ function hashParagraphBlock(block) {
169
184
  if (attrs.defaultFontFamily != null) parts.push(`dff:${attrs.defaultFontFamily}`);
170
185
  if (attrs.suppressEmptyParagraphHeight) parts.push("sup");
171
186
  if (attrs.reserveEmptyOutlineHeight) parts.push("outline-empty-reserve");
187
+ if (attrs.documentGridLinePitch !== void 0) parts.push(`documentGrid:${attrs.documentGridLinePitch}|${attrs.snapToGrid !== false}`);
188
+ if (attrs.justificationCompatibility) parts.push(`justify-compat:${attrs.justificationCompatibility.type}`);
172
189
  parts.push(...lineBreakPolicyCacheParts(attrs));
173
190
  const borders = attrs.borders;
174
191
  if (borders) {
@@ -239,6 +256,7 @@ function clearAllCaches() {
239
256
  clearTextWidthCache();
240
257
  clearFontMetricsCache();
241
258
  clearParagraphMeasureCache();
259
+ clearFontResolvedCache();
242
260
  }
243
261
  /**
244
262
  * Get total size of all caches
@@ -12,8 +12,8 @@ const resolveEffectiveLineBreakPolicy = ({ attrs, run }) => {
12
12
  provider: {
13
13
  ...locale ? { locale } : {},
14
14
  ...attrs?.kinsoku !== void 0 ? { kinsoku: attrs.kinsoku } : {},
15
- ...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: before.characters } : {},
16
- ...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: after.characters } : {},
15
+ ...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: toCodePointSet(before.characters) } : {},
16
+ ...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: toCodePointSet(after.characters) } : {},
17
17
  ...rules?.useLegacyEthiopicAmharicRules ? { useLegacyEthiopicAmharicRules: true } : {},
18
18
  ...automaticHyphenation?.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: automaticHyphenation.doNotHyphenateCaps } : {},
19
19
  ...run.allCaps === true ? { renderedAllCaps: true } : {}
@@ -56,5 +56,11 @@ const languageMatches = (ruleLanguage, locale) => {
56
56
  const active = locale.toLowerCase();
57
57
  return active === rule || active.startsWith(`${rule}-`) || rule.startsWith(`${active}-`);
58
58
  };
59
+ /**
60
+ * Split an already length-capped kinsoku character list (settingsParser.ts)
61
+ * into a code-point Set so line-edge membership checks are O(1) instead of
62
+ * an O(n) `String.includes` scan per character measured.
63
+ */
64
+ const toCodePointSet = (characters) => new Set(Array.from(characters));
59
65
  //#endregion
60
66
  export { lineBreakPolicyCacheParts, resolveEffectiveLineBreakPolicy };
@@ -10,8 +10,8 @@
10
10
  type LineBreakPolicy = {
11
11
  /** BCP-47 language tag resolved from `w:lang` for this run. */locale?: string; /** Apply East Asian first/last-character restrictions (`w:kinsoku`). */
12
12
  kinsoku?: boolean; /** Document replacement list: characters that may not begin a line. */
13
- noLineBreaksBefore?: string; /** Document replacement list: characters that may not end a line. */
14
- noLineBreaksAfter?: string; /** Use the legacy Ethiopic/Amharic compatibility behavior. */
13
+ noLineBreaksBefore?: ReadonlySet<string>; /** Document replacement list: characters that may not end a line. */
14
+ noLineBreaksAfter?: ReadonlySet<string>; /** Use the legacy Ethiopic/Amharic compatibility behavior. */
15
15
  useLegacyEthiopicAmharicRules?: boolean; /** Keep words made entirely of capital letters unhyphenated. */
16
16
  doNotHyphenateCaps?: boolean; /** The run renders with the DOCX all-caps transform. */
17
17
  renderedAllCaps?: boolean;
@@ -27,6 +27,13 @@ const segmenterLocale = (locale) => {
27
27
  };
28
28
  const DICTIONARY_BREAK_SCRIPT = /[\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
29
29
  const CJK_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
30
+ const WORD_HANGING_PUNCTUATION_LANGUAGES = /* @__PURE__ */ new Set([
31
+ "ii",
32
+ "ja",
33
+ "ko",
34
+ "zh"
35
+ ]);
36
+ const SIMPLE_BREAK_TEXT = /^[\p{Script=Latin}\p{Script=Cyrillic}\p{Script=Greek}\p{Number}\p{Punctuation}\p{White_Space}]*$/u;
30
37
  const BREAK_AFTER_CHARACTER = /* @__PURE__ */ new Set([
31
38
  "-",
32
39
  "֊",
@@ -42,13 +49,10 @@ const BREAK_AFTER_CHARACTER = /* @__PURE__ */ new Set([
42
49
  "​",
43
50
  "­"
44
51
  ]);
45
- const CZECH_ONE_LETTER_PREPOSITIONS = /* @__PURE__ */ new Set([
46
- "k",
47
- "o",
48
- "s",
49
- "u",
50
- "v",
51
- "z"
52
+ const NONBREAKING_SPACES = /* @__PURE__ */ new Set([
53
+ "\xA0",
54
+ "",
55
+ ""
52
56
  ]);
53
57
  const DEFAULT_PROHIBITED_LINE_START = /* @__PURE__ */ new Set([
54
58
  "!",
@@ -124,38 +128,14 @@ const previousCodePoint = (text, index) => {
124
128
  if (trailing >= 56320 && trailing <= 57343 && index >= 2) return text.slice(index - 2, index);
125
129
  return text[index - 1];
126
130
  };
127
- const isProhibitedLineStart = (character, policy) => character !== void 0 && policy?.kinsoku !== false && (policy?.noLineBreaksBefore !== void 0 ? policy.noLineBreaksBefore.includes(character) : DEFAULT_PROHIBITED_LINE_START.has(character));
128
- const isProhibitedLineEnd = (character, policy) => character !== void 0 && policy?.kinsoku !== false && (policy?.noLineBreaksAfter !== void 0 ? policy.noLineBreaksAfter.includes(character) : DEFAULT_PROHIBITED_LINE_END.has(character));
129
- const isCjkLineBreakParticipant = (character, policy) => character !== void 0 && (CJK_SCRIPT.test(character) || DEFAULT_PROHIBITED_LINE_START.has(character) || DEFAULT_PROHIBITED_LINE_END.has(character) || policy?.noLineBreaksBefore?.includes(character) === true || policy?.noLineBreaksAfter?.includes(character) === true);
131
+ const isProhibitedLineStart = (character, policy) => character !== void 0 && policy?.kinsoku !== false && (policy?.noLineBreaksBefore !== void 0 ? policy.noLineBreaksBefore.has(character) : DEFAULT_PROHIBITED_LINE_START.has(character));
132
+ const isProhibitedLineEnd = (character, policy) => character !== void 0 && policy?.kinsoku !== false && (policy?.noLineBreaksAfter !== void 0 ? policy.noLineBreaksAfter.has(character) : DEFAULT_PROHIBITED_LINE_END.has(character));
133
+ const isCjkLineBreakParticipant = (character, policy) => character !== void 0 && (CJK_SCRIPT.test(character) || policy?.kinsoku !== false && (DEFAULT_PROHIBITED_LINE_START.has(character) || DEFAULT_PROHIBITED_LINE_END.has(character) || policy?.noLineBreaksBefore?.has(character) === true || policy?.noLineBreaksAfter?.has(character) === true));
130
134
  const isLegacyEthiopicBreakCharacter = (character, policy) => {
131
135
  if (!policy?.useLegacyEthiopicAmharicRules || character === void 0) return false;
132
136
  const codePoint = character.codePointAt(0);
133
137
  return codePoint !== void 0 && codePoint >= 4961 && codePoint <= 4968;
134
138
  };
135
- const czechProtectedBreaks = (text, policy) => {
136
- const breaks = /* @__PURE__ */ new Set();
137
- if (segmenterLocale(policy?.locale) !== "cs") return breaks;
138
- let tokenStart = 0;
139
- let index = 0;
140
- while (index < text.length) {
141
- const character = firstCodePoint(text, index);
142
- if (character === void 0) break;
143
- if (!/\s/u.test(character)) {
144
- index += character.length;
145
- continue;
146
- }
147
- const token = text.slice(tokenStart, index);
148
- const protectsNextWord = token.length === 1 && CZECH_ONE_LETTER_PREPOSITIONS.has(token.toLocaleLowerCase("cs"));
149
- while (index < text.length) {
150
- const whitespace = firstCodePoint(text, index);
151
- if (whitespace === void 0 || !/\s/u.test(whitespace)) break;
152
- index += whitespace.length;
153
- if (protectsNextWord) breaks.add(index);
154
- }
155
- tokenStart = index;
156
- }
157
- return breaks;
158
- };
159
139
  const allowsBreak = (text, index, policy) => {
160
140
  const previous = previousCodePoint(text, index);
161
141
  const next = firstCodePoint(text, index);
@@ -165,7 +145,20 @@ const pushBreak = (breaks, text, index, policy) => {
165
145
  if (index <= 0 || index > text.length || breaks.at(-1) === index) return;
166
146
  if (allowsBreak(text, index, policy)) breaks.push(index);
167
147
  };
148
+ const findSimpleGraphemeBreaks = (text) => {
149
+ if (!SIMPLE_BREAK_TEXT.test(text)) return;
150
+ const breaks = [];
151
+ for (let index = 0; index < text.length; index += 1) {
152
+ const codeUnit = text.charCodeAt(index);
153
+ if (codeUnit >= 55296 && codeUnit <= 57343) return;
154
+ if (codeUnit === 13 && text.charCodeAt(index + 1) === 10) index += 1;
155
+ breaks.push(index + 1);
156
+ }
157
+ return breaks;
158
+ };
168
159
  const findUnicodeGraphemeBreaks = (text, policy) => {
160
+ const simpleBreaks = findSimpleGraphemeBreaks(text);
161
+ if (simpleBreaks !== void 0) return simpleBreaks;
169
162
  const breaks = [];
170
163
  for (const segment of getSegmenter(graphemeSegmenters, "grapheme", policy?.locale).segment(text)) {
171
164
  const end = segment.index + segment.segment.length;
@@ -173,13 +166,38 @@ const findUnicodeGraphemeBreaks = (text, policy) => {
173
166
  }
174
167
  return breaks;
175
168
  };
169
+ const isSimpleWhitespace = (character, codePoint) => codePoint === 32 || codePoint >= 9 && codePoint <= 13 || codePoint > 127 && /\s/u.test(character);
170
+ /**
171
+ * Find the complete set of Unicode line-break opportunities for simple text
172
+ * without invoking `Intl.Segmenter`.
173
+ *
174
+ * Latin, Cyrillic, and Greek letters plus ordinary numbers, punctuation, and
175
+ * spacing are single grapheme clusters, except CRLF, which Unicode treats as
176
+ * one cluster. Returning `undefined` for combining marks, astral characters,
177
+ * dictionary-segmented scripts, CJK, and other complex input keeps the full
178
+ * Unicode provider responsible for those cases.
179
+ */
180
+ const findSimpleBreaks = (text, policy) => {
181
+ if (!SIMPLE_BREAK_TEXT.test(text)) return;
182
+ const breaks = [];
183
+ for (let index = 0; index < text.length; index += 1) {
184
+ const codePoint = text.charCodeAt(index);
185
+ if (codePoint >= 55296 && codePoint <= 57343) return;
186
+ if (codePoint === 13 && text.charCodeAt(index + 1) === 10) continue;
187
+ const character = text[index];
188
+ if (isSimpleWhitespace(character, codePoint) && !NONBREAKING_SPACES.has(character) || BREAK_AFTER_CHARACTER.has(character) || isLegacyEthiopicBreakCharacter(character, policy)) pushBreak(breaks, text, index + 1, policy);
189
+ }
190
+ return breaks;
191
+ };
176
192
  const findUnicodeBreaks = (text, policy) => {
177
193
  if (text.length === 0) return [];
194
+ const simpleBreaks = findSimpleBreaks(text, policy);
195
+ if (simpleBreaks !== void 0) return simpleBreaks;
178
196
  const breaks = [];
179
197
  const graphemeBreaks = findUnicodeGraphemeBreaks(text, policy);
180
198
  for (const index of graphemeBreaks) {
181
199
  const previous = previousCodePoint(text, index);
182
- if (previous !== void 0 && (/\s/u.test(previous) || BREAK_AFTER_CHARACTER.has(previous) || isLegacyEthiopicBreakCharacter(previous, policy))) pushBreak(breaks, text, index, policy);
200
+ if (previous !== void 0 && (/\s/u.test(previous) && !NONBREAKING_SPACES.has(previous) || BREAK_AFTER_CHARACTER.has(previous) || isLegacyEthiopicBreakCharacter(previous, policy))) pushBreak(breaks, text, index, policy);
183
201
  }
184
202
  const wordSegments = [...getSegmenter(wordSegmenters, "word", policy?.locale).segment(text)];
185
203
  for (const [segmentIndex, segment] of wordSegments.entries()) {
@@ -191,8 +209,7 @@ const findUnicodeBreaks = (text, policy) => {
191
209
  const previous = previousCodePoint(text, index);
192
210
  if (isCjkLineBreakParticipant(previous, policy)) pushBreak(breaks, text, index, policy);
193
211
  }
194
- const protectedBreaks = czechProtectedBreaks(text, policy);
195
- return [...new Set(breaks)].filter((index) => !protectedBreaks.has(index)).sort((left, right) => left - right);
212
+ return [...new Set(breaks)].sort((left, right) => left - right);
196
213
  };
197
214
  const hyphenatorFor = (locale) => {
198
215
  const normalized = locale?.trim().replaceAll("_", "-").toLowerCase();
@@ -226,6 +243,8 @@ const findPatternHyphenationBreaks = (text, policy) => {
226
243
  return sourceOffset === text.length ? breaks : [];
227
244
  };
228
245
  const isDefaultHangingPunctuation = (text, policy) => {
246
+ const language = segmenterLocale(policy?.locale);
247
+ if (!language || !WORD_HANGING_PUNCTUATION_LANGUAGES.has(language)) return false;
229
248
  const characters = [...text];
230
249
  return characters.length > 0 && characters.every((character) => isProhibitedLineStart(character, policy));
231
250
  };
@@ -2,7 +2,7 @@ import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, floatingTextBoxWr
2
2
  import { findClearLineY, measureParagraph } from "./measureParagraph.js";
3
3
  import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
4
4
  import { createTableCellFlowState, finishTableCellFlow, placeTableCellBlock } from "./tableCellFlow.js";
5
- import { layoutTextBoxParagraphs } from "./textBoxParagraphLayout.js";
5
+ import { layoutTextBoxContent } from "./textBoxParagraphLayout.js";
6
6
  import { getTextBoxGroupId } from "../textBoxGroup.js";
7
7
  import { isParagraphFrameTextBox } from "../paragraphFrame.js";
8
8
  import { recordMeasureBlock, recordMeasureBlockError } from "../layoutInstrumentation.js";
@@ -17,6 +17,14 @@ import { buildTableCellGrid, getFirstAvailableColumn, getTableCellVerticalBorder
17
17
  */
18
18
  const NO_WRAP_MEASURE_WIDTH = 1e6;
19
19
  /**
20
+ * Sanity cap on the table column count derived from summed cell colSpans
21
+ * when a table has no `tblGrid`/explicit column widths. Mirrors the
22
+ * `MAX_TABLE_COLUMNS` clamp applied to `w:gridSpan` at parse time
23
+ * (`docx/tableParser.ts`); enforced again here since a hostile colSpan sum
24
+ * would otherwise size the fallback column array and cell grid unbounded.
25
+ */
26
+ const MAX_TABLE_COLUMNS = 63;
27
+ /**
20
28
  * Check if an image run is a *text-wrapping* floating image — it
21
29
  * occupies an exclusion zone the body text should flow around.
22
30
  *
@@ -59,7 +67,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
59
67
  let columnWidths = tableBlock.columnWidths ?? [];
60
68
  const explicitWidthPx = resolveTableWidthPx(tableBlock.width, tableBlock.widthType, contentWidth);
61
69
  if (columnWidths.length === 0 && tableBlock.rows.length > 0) {
62
- const colCount = tableBlock.rows[0].cells.reduce((sum, cell) => sum + (cell.colSpan ?? 1), 0);
70
+ const colCount = Math.min(tableBlock.rows[0].cells.reduce((sum, cell) => sum + (cell.colSpan ?? 1), 0), MAX_TABLE_COLUMNS);
63
71
  const equalWidth = (explicitWidthPx ?? contentWidth) / Math.max(1, colCount);
64
72
  columnWidths = Array.from({ length: colCount }, () => equalWidth);
65
73
  } else if (columnWidths.length > 0 && explicitWidthPx) {
@@ -439,8 +447,11 @@ function measureBlock(block, contentWidth, floatingZones, cumulativeY, fieldValu
439
447
  function measureTextBoxBlock(tb, fieldValues) {
440
448
  const margins = tb.margins ?? DEFAULT_TEXTBOX_MARGINS;
441
449
  const innerWidth = tb.width - margins.left - margins.right;
442
- const innerMeasures = tb.content.map((p) => measureParagraph(p, innerWidth, fieldValues ? { fieldValues } : void 0));
443
- const contentBoxHeight = layoutTextBoxParagraphs(tb.content, innerMeasures).totalHeight + margins.top + margins.bottom;
450
+ const innerMeasures = tb.content.map((block) => {
451
+ if (block.kind === "table") return measureTableBlock(block, innerWidth, fieldValues);
452
+ return measureParagraph(block, innerWidth, fieldValues ? { fieldValues } : void 0);
453
+ });
454
+ const contentBoxHeight = layoutTextBoxContent(tb.content, innerMeasures).totalHeight + margins.top + margins.bottom;
444
455
  const totalHeight = tb.autoFit === "shape" ? Math.max(tb.height ?? 0, contentBoxHeight) : tb.height ?? contentBoxHeight;
445
456
  return {
446
457
  kind: "textBox",
@@ -1,5 +1,5 @@
1
- import { setMeasureProvider } from "./measureProvider.js";
2
1
  import { getFontKerningMode } from "./textMeasurementPolicy.js";
2
+ import { setMeasureProvider } from "./measureProvider.js";
3
3
  import { buildFontString, getResolvedData, ptToPx } from "./measureHelpers.js";
4
4
  import { hasCjk, isCjkCodePoint, segmentByScript } from "../../utils/scriptSegments.js";
5
5
  import { getCachedFontMetrics, getCachedTextWidth, getTextWidthCacheGeneration, setCachedFontMetrics, setCachedTextWidth } from "./cache.js";
@@ -22,6 +22,15 @@ type ResolvedFontCache = {
22
22
  * Get the resolved font data for a font family, with caching.
23
23
  */
24
24
  declare function getResolvedData(fontFamily: string): ResolvedFontCache;
25
+ /**
26
+ * Clear the resolved-font-family cache. `resolveFontFamily`'s output for a
27
+ * given key can change even when the raw font names don't — e.g. when the
28
+ * active document's embedded-font family map changes (see
29
+ * `utils/fontResolver.ts`'s `setEmbeddedFontFamilyMap`) — so this must be
30
+ * cleared whenever that happens, not just on font-name changes. Folded into
31
+ * `clearAllCaches` (`./cache.ts`) so callers get it for free.
32
+ */
33
+ declare function clearFontResolvedCache(): void;
25
34
  /**
26
35
  * Build a CSS font string from styling properties
27
36
  *
@@ -34,7 +43,7 @@ declare function getResolvedData(fontFamily: string): ResolvedFontCache;
34
43
  *
35
44
  * @example
36
45
  * buildFontString({ fontFamily: "Arial", fontSize: 12, bold: true })
37
- * // Returns: "800 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
46
+ * // Returns: "700 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
38
47
  */
39
48
  declare function buildFontString(style: FontStyle): string;
40
49
  /**
@@ -79,4 +88,4 @@ declare function halfPtToPx(halfPt: number): number;
79
88
  */
80
89
  declare function pxToHalfPt(px: number): number;
81
90
  //#endregion
82
- export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
91
+ export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
@@ -1,6 +1,6 @@
1
1
  import { resolveFontFamily } from "../../utils/fontResolver.js";
2
- import "../../utils/fontWeights.js";
3
2
  import { FONT_KERNING_MODE, getRunFontKerningMode } from "./textMeasurementPolicy.js";
3
+ import "../../utils/fontWeights.js";
4
4
  //#region src/layout-engine/measure/measureHelpers.ts
5
5
  /**
6
6
  * Pure measurement helpers — no canvas, no DOM, no worker.
@@ -62,6 +62,17 @@ function getResolvedFallback(fontFamily) {
62
62
  return getResolvedData(fontFamily).cssFallback;
63
63
  }
64
64
  /**
65
+ * Clear the resolved-font-family cache. `resolveFontFamily`'s output for a
66
+ * given key can change even when the raw font names don't — e.g. when the
67
+ * active document's embedded-font family map changes (see
68
+ * `utils/fontResolver.ts`'s `setEmbeddedFontFamilyMap`) — so this must be
69
+ * cleared whenever that happens, not just on font-name changes. Folded into
70
+ * `clearAllCaches` (`./cache.ts`) so callers get it for free.
71
+ */
72
+ function clearFontResolvedCache() {
73
+ fontResolvedCache.clear();
74
+ }
75
+ /**
65
76
  * Build a CSS font string from styling properties
66
77
  *
67
78
  * Font sizes are in points and need to be converted to pixels for canvas.
@@ -73,13 +84,13 @@ function getResolvedFallback(fontFamily) {
73
84
  *
74
85
  * @example
75
86
  * buildFontString({ fontFamily: "Arial", fontSize: 12, bold: true })
76
- * // Returns: "800 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
87
+ * // Returns: "700 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
77
88
  */
78
89
  function buildFontString(style) {
79
90
  const parts = [];
80
91
  if (style.italic) parts.push("italic");
81
92
  if (style.fontVariant) parts.push(style.fontVariant);
82
- if (style.bold) parts.push("800");
93
+ if (style.bold) parts.push("700");
83
94
  const fontSizePx = ptToPx(style.fontSize ?? 11);
84
95
  parts.push(`${fontSizePx}px`);
85
96
  const fontFamily = style.fontFamily ?? "Calibri";
@@ -156,4 +167,4 @@ function pxToHalfPt(px) {
156
167
  return pxToPt(px) * 2;
157
168
  }
158
169
  //#endregion
159
- export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
170
+ export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
@@ -1,8 +1,8 @@
1
1
  import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
2
- import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
3
2
  import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
3
+ import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
4
4
  import { buildRunFontStyle, ptToPx, twipsToPx } from "./measureHelpers.js";
5
- import "./lineBreakProvider.js";
5
+ import { defaultLineBreakProvider, getLineBreakProvider } from "./lineBreakProvider.js";
6
6
  import { calculateTabWidth, pixelsToTwips } from "../../prosemirror/utils/tabCalculator.js";
7
7
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
8
8
  import { hasCjk } from "../../utils/scriptSegments.js";
@@ -27,7 +27,9 @@ const WIDTH_TOLERANCE = .5;
27
27
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
28
28
  const JUSTIFY_SPACE_CONTRACTION_RATIO = .075;
29
29
  const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
30
- const JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO = .23;
30
+ const JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO = .022;
31
+ const JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO = .32;
32
+ const JUSTIFY_LIST_CONTINUATION_MAX_SHRINK_TOLERANCE_RATIO = .015;
31
33
  const JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO = .015;
32
34
  const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
33
35
  const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
@@ -137,7 +139,7 @@ const WORD_SINGLE_LINE_FLOOR = 1.15;
137
139
  * `auto`/`atLeast` line rules; `exact` means exact (per OOXML §17.3.1.33)
138
140
  * and stays untouched.
139
141
  */
140
- function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily) {
142
+ function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily, attrs) {
141
143
  const result = calculateTypographyMetrics(fontSize, spacing, getFontMetrics({
142
144
  fontSize,
143
145
  fontFamily: fontFamily ?? DEFAULT_FONT_FAMILY
@@ -145,12 +147,22 @@ function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily) {
145
147
  const lineRule = spacing?.lineRule ?? "auto";
146
148
  if (lineRule === "auto" || lineRule === "atLeast") {
147
149
  const floor = ptToPx(fontSize) * WORD_SINGLE_LINE_FLOOR;
148
- if (result.lineHeight < floor) return {
150
+ if (result.lineHeight < floor) return applyDocumentGrid({
149
151
  ...result,
150
152
  lineHeight: floor
151
- };
153
+ }, attrs, spacing);
152
154
  }
153
- return result;
155
+ return applyDocumentGrid(result, attrs, spacing);
156
+ }
157
+ function applyDocumentGrid(typography, attrs, spacing) {
158
+ const pitch = attrs?.documentGridLinePitch;
159
+ if (pitch === void 0 || pitch <= 0 || attrs?.snapToGrid === false) return typography;
160
+ if (spacing?.lineRule === "exact") return typography;
161
+ const lineHeight = Math.ceil(typography.lineHeight / pitch - 1e-9) * pitch;
162
+ return lineHeight === typography.lineHeight ? typography : {
163
+ ...typography,
164
+ lineHeight
165
+ };
154
166
  }
155
167
  /**
156
168
  * Check if a run is a text run
@@ -344,6 +356,7 @@ function uppercaseLetterRatio(text) {
344
356
  return letters === 0 ? 0 : uppercase / letters;
345
357
  }
346
358
  function resolveJustifyFitStrategy(block, isFirstLine, profile) {
359
+ if (block.attrs?.justificationCompatibility?.type === "legacy") return { type: "strict" };
347
360
  if (isShallowFullHangingListContinuation(block, isFirstLine)) return { type: "rounding" };
348
361
  if (block.attrs?.listMarker !== void 0) {
349
362
  const hanging = block.attrs.indent?.hanging ?? 0;
@@ -352,7 +365,7 @@ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
352
365
  ratio: JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO
353
366
  } : {
354
367
  type: "width",
355
- ratio: JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO
368
+ ratio: JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO
356
369
  };
357
370
  const left = block.attrs.indent?.left ?? 0;
358
371
  if (hanging > 0 && left > hanging) return {
@@ -361,7 +374,8 @@ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
361
374
  };
362
375
  return {
363
376
  type: "space",
364
- ratio: JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO
377
+ ratio: JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO,
378
+ maxWidthRatio: JUSTIFY_LIST_CONTINUATION_MAX_SHRINK_TOLERANCE_RATIO
365
379
  };
366
380
  }
367
381
  const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
@@ -391,9 +405,12 @@ function compressibleSpaceWidth(text, style) {
391
405
  return count === 0 ? 0 : count * measureTextWidth(" ", style);
392
406
  }
393
407
  function justifyFitTolerance(line, strategy, candidateSpaceWidth) {
408
+ if (strategy.type === "strict") return 0;
394
409
  if (strategy.type === "rounding") return WIDTH_TOLERANCE;
395
410
  if (strategy.type === "width") return Math.max(WIDTH_TOLERANCE, line.availableWidth * strategy.ratio);
396
- return Math.max(WIDTH_TOLERANCE, (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio);
411
+ const spaceTolerance = (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio;
412
+ const boundedTolerance = strategy.maxWidthRatio === void 0 ? spaceTolerance : Math.min(spaceTolerance, line.availableWidth * strategy.maxWidthRatio);
413
+ return Math.max(WIDTH_TOLERANCE, boundedTolerance);
397
414
  }
398
415
  function isShallowFullHangingListContinuation(block, isFirstLine) {
399
416
  if (isFirstLine || block.attrs?.listMarker === void 0) return false;
@@ -410,8 +427,20 @@ function trimTrailingSpacesAndTabs(text) {
410
427
  }
411
428
  return text.slice(0, end);
412
429
  }
430
+ function trailingCodePoint(text) {
431
+ if (text.length === 0) return;
432
+ const trailing = text.charCodeAt(text.length - 1);
433
+ const preceding = text.charCodeAt(text.length - 2);
434
+ const start = trailing >= 56320 && trailing <= 57343 && preceding >= 55296 && preceding <= 56319 ? text.length - 2 : text.length - 1;
435
+ return text.slice(start);
436
+ }
437
+ const usesDefaultHangingPunctuationClassifier = () => {
438
+ return (getLineBreakProvider().isHangingPunctuation ?? defaultLineBreakProvider.isHangingPunctuation) === defaultLineBreakProvider.isHangingPunctuation;
439
+ };
413
440
  function trailingHangingPunctuationWidth(text, style, policy, enabled) {
414
441
  if (!enabled || text.length === 0) return 0;
442
+ const trailing = trailingCodePoint(text);
443
+ if (usesDefaultHangingPunctuationClassifier() && !isHangingPunctuation(trailing ?? "", policy)) return 0;
415
444
  const lastStart = findGraphemeBreaks(text, policy).at(-2) ?? 0;
416
445
  const lastGrapheme = text.slice(lastStart);
417
446
  if (!isHangingPunctuation(lastGrapheme, policy)) return 0;
@@ -448,13 +477,14 @@ function computeProtectedCrossRunGlueWidths(block) {
448
477
  for (let index = 0; index < block.runs.length; index++) {
449
478
  const run = block.runs[index];
450
479
  if (!run || !isTextRun(run)) continue;
451
- const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
452
- const token = trailing?.[1];
453
480
  const policy = resolveEffectiveLineBreakPolicy({
454
481
  attrs: block.attrs,
455
482
  run
456
483
  }).provider;
457
- if (!token || token.length !== 1 || !policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
484
+ if (!policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
485
+ const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
486
+ const token = trailing?.[1];
487
+ if (!token || token.length !== 1) continue;
458
488
  let separator = trailing[2] ?? "";
459
489
  let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
460
490
  let followingWord = "";
@@ -485,12 +515,20 @@ function computeProtectedCrossRunGlueWidths(block) {
485
515
  }
486
516
  return widths;
487
517
  }
518
+ /**
519
+ * Cap on the number of runs `collectCrossRunWord` traverses per call. A
520
+ * paragraph split into many tiny (including empty) runs would otherwise let
521
+ * the search walk the full run list for every word-start position it is
522
+ * invoked from, without the per-word text-length budget ever kicking in.
523
+ */
524
+ const MAX_CROSS_RUN_SEGMENTS = 64;
488
525
  /** Collect one lexical word continued through adjacent formatting runs. */
489
526
  function collectCrossRunWord({ block, startRunIndex, startChar }) {
490
527
  const segments = [];
491
528
  let text = "";
492
529
  let width = 0;
493
530
  for (let runIndex = startRunIndex; runIndex < block.runs.length; runIndex++) {
531
+ if (runIndex - startRunIndex >= MAX_CROSS_RUN_SEGMENTS) return;
494
532
  const run = block.runs[runIndex];
495
533
  if (!run || !isTextRun(run)) break;
496
534
  const start = runIndex === startRunIndex ? startChar : 0;
@@ -655,7 +693,7 @@ function measureParagraph(block, maxWidth, options) {
655
693
  totalHeight: 0
656
694
  };
657
695
  }
658
- const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY);
696
+ const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
659
697
  const outlineLineHeight = attrs?.reserveEmptyOutlineHeight ? emptyMetrics.lineHeight * 2 : emptyMetrics.lineHeight;
660
698
  lines.push({
661
699
  fromRun: 0,
@@ -675,9 +713,8 @@ function measureParagraph(block, maxWidth, options) {
675
713
  totalHeight
676
714
  };
677
715
  }
678
- if (runs.length === 1 && isTextRun(runs[0]) && isEmptyTextRun(runs[0])) {
679
- const run = runs[0];
680
- const emptyMetrics = calculateEmptyParagraphMetrics(run.fontSize ?? attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, run.fontFamily ?? attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY);
716
+ if (runs.length > 0 && runs.every((run) => isTextRun(run) && isEmptyTextRun(run))) {
717
+ const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
681
718
  lines.push({
682
719
  fromRun: 0,
683
720
  fromChar: 0,
@@ -733,7 +770,7 @@ function measureParagraph(block, maxWidth, options) {
733
770
  finalTypography.lineHeight = objectHeight;
734
771
  finalTypography.ascent = objectHeight;
735
772
  finalTypography.descent = 0;
736
- return finalTypography;
773
+ return applyDocumentGrid(finalTypography, attrs, spacing);
737
774
  }
738
775
  if (soleRun && isImageRun(soleRun)) {
739
776
  finalTypography.lineHeight = objectHeight;
@@ -744,7 +781,7 @@ function measureParagraph(block, maxWidth, options) {
744
781
  finalTypography.ascent = objectHeight;
745
782
  }
746
783
  }
747
- return finalTypography;
784
+ return applyDocumentGrid(finalTypography, attrs, spacing);
748
785
  };
749
786
  const getPostWrapAvailableWidth = () => {
750
787
  if (!floatingZones || floatingZones.length === 0) return bodyContentWidth;
@@ -769,7 +806,8 @@ function measureParagraph(block, maxWidth, options) {
769
806
  toChar: currentLine.toChar,
770
807
  width: Math.max(0, currentLine.width - currentLine.trailingWhitespaceWidth),
771
808
  ...finalTypography,
772
- ...currentLine.discretionaryHyphen ? { discretionaryHyphen: currentLine.discretionaryHyphen } : {}
809
+ ...currentLine.discretionaryHyphen ? { discretionaryHyphen: currentLine.discretionaryHyphen } : {},
810
+ ...currentLine.renderedPageBreakBefore ? { renderedPageBreakBefore: true } : {}
773
811
  };
774
812
  if (currentLine.leftOffset > 0) line.leftOffset = currentLine.leftOffset;
775
813
  if (currentLine.rightOffset > 0) line.rightOffset = currentLine.rightOffset;
@@ -827,6 +865,22 @@ function measureParagraph(block, maxWidth, options) {
827
865
  for (let runIndex = 0; runIndex < runs.length; runIndex++) {
828
866
  if (crossRunResume && runIndex < crossRunResume.runIndex) continue;
829
867
  const run = runs[runIndex];
868
+ if (run.kind === "renderedPageBreak") {
869
+ if (!runs.slice(runIndex + 1).some((followingRun) => followingRun.kind !== "renderedPageBreak")) {
870
+ currentLine.toRun = runIndex;
871
+ currentLine.toChar = 0;
872
+ continue;
873
+ }
874
+ if (currentLine.width > 0 || currentLine.maxImageHeightPx > 0 || currentLine.maxMathHeightPx > 0) startNewLine(runIndex + 1, 0);
875
+ else {
876
+ currentLine.fromRun = runIndex + 1;
877
+ currentLine.toRun = runIndex + 1;
878
+ currentLine.fromChar = 0;
879
+ currentLine.toChar = 0;
880
+ }
881
+ currentLine.renderedPageBreakBefore = true;
882
+ continue;
883
+ }
830
884
  if (isLineBreakRun(run)) {
831
885
  currentLine.toRun = runIndex;
832
886
  currentLine.toChar = 0;
@@ -952,13 +1006,11 @@ function measureParagraph(block, maxWidth, options) {
952
1006
  const wordBreaks = findWordBreaks(text, breakPolicy);
953
1007
  let charIndex = crossRunResume?.runIndex === runIndex ? crossRunResume.charIndex : 0;
954
1008
  if (crossRunResume?.runIndex === runIndex) crossRunResume = void 0;
1009
+ let wordBreakIndex = 0;
955
1010
  let activeHyphenationWord;
956
1011
  while (charIndex < text.length) {
957
- let nextBreak = text.length;
958
- for (const breakPoint of wordBreaks) if (breakPoint > charIndex) {
959
- nextBreak = breakPoint;
960
- break;
961
- }
1012
+ while ((wordBreaks[wordBreakIndex] ?? Number.POSITIVE_INFINITY) <= charIndex) wordBreakIndex += 1;
1013
+ const nextBreak = wordBreaks[wordBreakIndex] ?? text.length;
962
1014
  const word = text.slice(charIndex, nextBreak);
963
1015
  const measuredWord = trimTrailingSpacesAndTabs(word);
964
1016
  const wordWidth = measureTextWidth(measuredWord, style);
@@ -1094,6 +1146,14 @@ function measureParagraph(block, maxWidth, options) {
1094
1146
  }
1095
1147
  }
1096
1148
  }
1149
+ const listParagraphMarkFontSize = attrs?.listParagraphMarkFontSize;
1150
+ if (listParagraphMarkFontSize !== void 0 && listParagraphMarkFontSize > currentLine.maxFontSize) {
1151
+ const fontFamily = currentLine.maxFontMetrics?.fontFamily ?? attrs?.defaultFontFamily;
1152
+ updateMaxFont({
1153
+ fontSize: listParagraphMarkFontSize,
1154
+ ...fontFamily === void 0 ? {} : { fontFamily }
1155
+ });
1156
+ }
1097
1157
  finalizeLine();
1098
1158
  let totalWithSpacing = lines.reduce((sum, line) => sum + measuredLineAdvance(line), 0);
1099
1159
  if (spacing?.before) totalWithSpacing += spacing.before;