@stll/folio-core 0.11.0 → 0.12.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 (194) hide show
  1. package/dist/ai-edits/apply.js +602 -50
  2. package/dist/ai-edits/block-identity.d.ts +4 -0
  3. package/dist/ai-edits/block-identity.js +9 -0
  4. package/dist/ai-edits/blockRange.d.ts +28 -1
  5. package/dist/ai-edits/blockRange.js +67 -1
  6. package/dist/ai-edits/headless.d.ts +4 -4
  7. package/dist/ai-edits/headless.js +9 -9
  8. package/dist/ai-edits/index.d.ts +2 -2
  9. package/dist/ai-edits/index.js +2 -2
  10. package/dist/ai-edits/read.d.ts +7 -7
  11. package/dist/ai-edits/read.js +110 -3
  12. package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
  13. package/dist/ai-edits/table-cell-mutations.js +237 -0
  14. package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
  15. package/dist/ai-edits/table-mutation-plan.js +90 -0
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +100 -0
  17. package/dist/ai-edits/table-row-column-mutations.js +300 -0
  18. package/dist/ai-edits/table-targets.d.ts +32 -0
  19. package/dist/ai-edits/table-targets.js +70 -0
  20. package/dist/ai-edits/types.d.ts +34 -0
  21. package/dist/compat/eigenpal.js +1 -1
  22. package/dist/content-controls/mutateContentControls.js +1 -1
  23. package/dist/controller/activeEditorStory.d.ts +36 -0
  24. package/dist/controller/activeEditorStory.js +30 -0
  25. package/dist/controller/contentControlWidgetController.d.ts +55 -0
  26. package/dist/controller/contentControlWidgetController.js +141 -0
  27. package/dist/controller/fontReadiness.js +2 -0
  28. package/dist/controller/headerFooterEditorManager.js +1 -1
  29. package/dist/controller/hiddenEditorApi.js +2 -2
  30. package/dist/controller/hiddenEditorManager.d.ts +12 -1
  31. package/dist/controller/hiddenEditorManager.js +32 -11
  32. package/dist/controller/layoutPipeline.d.ts +2 -0
  33. package/dist/controller/layoutPipeline.js +16 -1
  34. package/dist/controller/noteEditorManager.d.ts +36 -0
  35. package/dist/controller/noteEditorManager.js +210 -0
  36. package/dist/document-operations.d.ts +35 -3
  37. package/dist/document-operations.js +161 -3
  38. package/dist/docx/blockContentParser.js +2 -2
  39. package/dist/docx/commentParser.js +6 -1
  40. package/dist/docx/conformance.js +8 -1
  41. package/dist/docx/documentGrid.d.ts +8 -0
  42. package/dist/docx/documentGrid.js +8 -0
  43. package/dist/docx/drawingUtils.js +1 -1
  44. package/dist/docx/ensureParaIds.js +1 -1
  45. package/dist/docx/fieldParser.js +1 -1
  46. package/dist/docx/notePropertiesParser.js +1 -1
  47. package/dist/docx/numberingParser.js +1 -1
  48. package/dist/docx/paragraphParser.js +63 -58
  49. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  50. package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
  51. package/dist/docx/paragraphTraversal.js +1 -1
  52. package/dist/docx/parser.js +3 -3
  53. package/dist/docx/rezip.js +1 -1
  54. package/dist/docx/runParser.js +4 -2
  55. package/dist/docx/sdtProperties.js +1 -1
  56. package/dist/docx/sectionParser.js +1 -1
  57. package/dist/docx/selectiveSave.js +1 -1
  58. package/dist/docx/serializer/commentSerializer.js +1 -2
  59. package/dist/docx/serializer/documentSerializer.js +2 -2
  60. package/dist/docx/serializer/headerFooterSerializer.js +2 -2
  61. package/dist/docx/serializer/noteSerializer.js +2 -2
  62. package/dist/docx/serializer/paragraphSerializer.js +7 -1
  63. package/dist/docx/serializer/runSerializer.js +4 -2
  64. package/dist/docx/serializer/stylesSerializer.js +1 -1
  65. package/dist/docx/serializer/tableSerializer.d.ts +5 -4
  66. package/dist/docx/serializer/tableSerializer.js +17 -13
  67. package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
  68. package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
  69. package/dist/docx/server/boundedArchive.d.ts +14 -3
  70. package/dist/docx/server/boundedArchive.js +56 -11
  71. package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
  72. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
  73. package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
  74. package/dist/docx/server/inspectDocxPackage.js +217 -0
  75. package/dist/docx/server/validateDocxConformance.d.ts +41 -0
  76. package/dist/docx/server/validateDocxConformance.js +336 -0
  77. package/dist/docx/settingsParser.js +11 -1
  78. package/dist/docx/shapeParser.js +1 -1
  79. package/dist/docx/styleParser.js +4 -2
  80. package/dist/docx/tableParser.js +18 -7
  81. package/dist/docx/textBoxParser.d.ts +3 -3
  82. package/dist/docx/textBoxParser.js +16 -12
  83. package/dist/docx/xmlSafety.d.ts +5 -0
  84. package/dist/docx/xmlSafety.js +9 -0
  85. package/dist/fields/resolveFieldValues.js +1 -3
  86. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  87. package/dist/i18n/messages/catalogs.gen.js +17 -0
  88. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  89. package/dist/index.js +1 -1
  90. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  91. package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
  92. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  93. package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
  94. package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
  95. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  96. package/dist/layout-bridge/convert/toFlowBlocks.js +136 -20
  97. package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
  98. package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
  99. package/dist/layout-bridge/engine/clickToPosition.js +2 -2
  100. package/dist/layout-engine/index.d.ts +2 -2
  101. package/dist/layout-engine/index.js +43 -28
  102. package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
  103. package/dist/layout-engine/justificationCompatibility.js +8 -0
  104. package/dist/layout-engine/keep-together.js +11 -1
  105. package/dist/layout-engine/measure/cache.js +21 -5
  106. package/dist/layout-engine/measure/lineBreakProvider.js +53 -34
  107. package/dist/layout-engine/measure/measureBlocks.js +6 -3
  108. package/dist/layout-engine/measure/measureContainer.js +1 -1
  109. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  110. package/dist/layout-engine/measure/measureHelpers.js +3 -3
  111. package/dist/layout-engine/measure/measureParagraph.js +69 -25
  112. package/dist/layout-engine/measure/measureProvider.js +4 -3
  113. package/dist/layout-engine/measure/tableCellFlow.js +5 -2
  114. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
  115. package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
  116. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
  117. package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
  118. package/dist/layout-engine/paginator.js +5 -1
  119. package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
  120. package/dist/layout-engine/paragraphSpacing.js +107 -13
  121. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  122. package/dist/layout-engine/types.d.ts +26 -13
  123. package/dist/layout-painter/registry/modules/textBox.js +5 -1
  124. package/dist/layout-painter/renderPage.d.ts +5 -2
  125. package/dist/layout-painter/renderPage.js +56 -12
  126. package/dist/layout-painter/renderParagraph.js +22 -6
  127. package/dist/layout-painter/renderTable.d.ts +5 -1
  128. package/dist/layout-painter/renderTable.js +5 -2
  129. package/dist/layout-painter/renderTextBox.d.ts +2 -1
  130. package/dist/layout-painter/renderTextBox.js +25 -14
  131. package/dist/managers/DocumentLoaderManager.js +1 -1
  132. package/dist/model.js +1 -1
  133. package/dist/prosemirror/attrs/index.d.ts +4 -2
  134. package/dist/prosemirror/attrs/index.js +154 -18
  135. package/dist/prosemirror/commands/comments.d.ts +3 -3
  136. package/dist/prosemirror/commands/comments.js +248 -12
  137. package/dist/prosemirror/commands/contentControls.js +1 -1
  138. package/dist/prosemirror/commands/index.js +1 -1
  139. package/dist/prosemirror/commands/pageBreak.js +1 -1
  140. package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
  141. package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
  142. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
  143. package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
  144. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
  145. package/dist/prosemirror/conversion/fromProseDoc.js +286 -61
  146. package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
  147. package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
  148. package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
  149. package/dist/prosemirror/conversion/toProseDoc.js +347 -98
  150. package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
  151. package/dist/prosemirror/extensions/StarterKit.js +6 -1
  152. package/dist/prosemirror/extensions/core/ParagraphExtension.js +4 -1
  153. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
  154. package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
  155. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
  156. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
  157. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
  158. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
  159. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
  160. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
  161. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +24 -1
  162. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  163. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
  164. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
  165. package/dist/prosemirror/extensions/nodes/TableExtension.js +9 -3
  166. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
  167. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
  168. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
  169. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
  170. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +34 -8
  171. package/dist/prosemirror/plugins/contentControlWidgets.js +125 -91
  172. package/dist/prosemirror/plugins/selectionTracker.js +1 -0
  173. package/dist/prosemirror/plugins/suggestionMode.js +1 -1
  174. package/dist/prosemirror/schema/index.d.ts +2 -2
  175. package/dist/prosemirror/schema/marks.d.ts +5 -1
  176. package/dist/prosemirror/schema/nodes.d.ts +64 -7
  177. package/dist/prosemirror/selectionState.js +1 -0
  178. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  179. package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
  180. package/dist/prosemirror/tableCellMergeRevision.js +22 -0
  181. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
  182. package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
  183. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
  184. package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
  185. package/dist/prosemirror/validation.js +9 -1
  186. package/dist/server.d.ts +6 -2
  187. package/dist/server.js +5 -1
  188. package/dist/utils/fontFamilyMerge.js +9 -11
  189. package/dist/utils/fontLoader.js +3 -2
  190. package/dist/utils/fontResolver.js +22 -4
  191. package/dist/utils/fontWeights.d.ts +1 -1
  192. package/dist/utils/fontWeights.js +1 -1
  193. package/dist/utils/paragraphFormattingMerge.js +1 -0
  194. package/package.json +1 -1
@@ -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
  "!",
@@ -132,30 +136,6 @@ const isLegacyEthiopicBreakCharacter = (character, policy) => {
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";
@@ -439,8 +439,11 @@ function measureBlock(block, contentWidth, floatingZones, cumulativeY, fieldValu
439
439
  function measureTextBoxBlock(tb, fieldValues) {
440
440
  const margins = tb.margins ?? DEFAULT_TEXTBOX_MARGINS;
441
441
  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;
442
+ const innerMeasures = tb.content.map((block) => {
443
+ if (block.kind === "table") return measureTableBlock(block, innerWidth, fieldValues);
444
+ return measureParagraph(block, innerWidth, fieldValues ? { fieldValues } : void 0);
445
+ });
446
+ const contentBoxHeight = layoutTextBoxContent(tb.content, innerMeasures).totalHeight + margins.top + margins.bottom;
444
447
  const totalHeight = tb.autoFit === "shape" ? Math.max(tb.height ?? 0, contentBoxHeight) : tb.height ?? contentBoxHeight;
445
448
  return {
446
449
  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";
@@ -34,7 +34,7 @@ declare function getResolvedData(fontFamily: string): ResolvedFontCache;
34
34
  *
35
35
  * @example
36
36
  * buildFontString({ fontFamily: "Arial", fontSize: 12, bold: true })
37
- * // Returns: "800 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
37
+ * // Returns: "700 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
38
38
  */
39
39
  declare function buildFontString(style: FontStyle): string;
40
40
  /**
@@ -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.
@@ -73,13 +73,13 @@ function getResolvedFallback(fontFamily) {
73
73
  *
74
74
  * @example
75
75
  * buildFontString({ fontFamily: "Arial", fontSize: 12, bold: true })
76
- * // Returns: "800 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
76
+ * // Returns: "700 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
77
77
  */
78
78
  function buildFontString(style) {
79
79
  const parts = [];
80
80
  if (style.italic) parts.push("italic");
81
81
  if (style.fontVariant) parts.push(style.fontVariant);
82
- if (style.bold) parts.push("800");
82
+ if (style.bold) parts.push("700");
83
83
  const fontSizePx = ptToPx(style.fontSize ?? 11);
84
84
  parts.push(`${fontSizePx}px`);
85
85
  const fontFamily = style.fontFamily ?? "Calibri";
@@ -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 = "";
@@ -655,7 +685,7 @@ function measureParagraph(block, maxWidth, options) {
655
685
  totalHeight: 0
656
686
  };
657
687
  }
658
- const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY);
688
+ const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
659
689
  const outlineLineHeight = attrs?.reserveEmptyOutlineHeight ? emptyMetrics.lineHeight * 2 : emptyMetrics.lineHeight;
660
690
  lines.push({
661
691
  fromRun: 0,
@@ -675,9 +705,8 @@ function measureParagraph(block, maxWidth, options) {
675
705
  totalHeight
676
706
  };
677
707
  }
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);
708
+ if (runs.length > 0 && runs.every((run) => isTextRun(run) && isEmptyTextRun(run))) {
709
+ const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
681
710
  lines.push({
682
711
  fromRun: 0,
683
712
  fromChar: 0,
@@ -733,7 +762,7 @@ function measureParagraph(block, maxWidth, options) {
733
762
  finalTypography.lineHeight = objectHeight;
734
763
  finalTypography.ascent = objectHeight;
735
764
  finalTypography.descent = 0;
736
- return finalTypography;
765
+ return applyDocumentGrid(finalTypography, attrs, spacing);
737
766
  }
738
767
  if (soleRun && isImageRun(soleRun)) {
739
768
  finalTypography.lineHeight = objectHeight;
@@ -744,7 +773,7 @@ function measureParagraph(block, maxWidth, options) {
744
773
  finalTypography.ascent = objectHeight;
745
774
  }
746
775
  }
747
- return finalTypography;
776
+ return applyDocumentGrid(finalTypography, attrs, spacing);
748
777
  };
749
778
  const getPostWrapAvailableWidth = () => {
750
779
  if (!floatingZones || floatingZones.length === 0) return bodyContentWidth;
@@ -769,7 +798,8 @@ function measureParagraph(block, maxWidth, options) {
769
798
  toChar: currentLine.toChar,
770
799
  width: Math.max(0, currentLine.width - currentLine.trailingWhitespaceWidth),
771
800
  ...finalTypography,
772
- ...currentLine.discretionaryHyphen ? { discretionaryHyphen: currentLine.discretionaryHyphen } : {}
801
+ ...currentLine.discretionaryHyphen ? { discretionaryHyphen: currentLine.discretionaryHyphen } : {},
802
+ ...currentLine.renderedPageBreakBefore ? { renderedPageBreakBefore: true } : {}
773
803
  };
774
804
  if (currentLine.leftOffset > 0) line.leftOffset = currentLine.leftOffset;
775
805
  if (currentLine.rightOffset > 0) line.rightOffset = currentLine.rightOffset;
@@ -827,6 +857,22 @@ function measureParagraph(block, maxWidth, options) {
827
857
  for (let runIndex = 0; runIndex < runs.length; runIndex++) {
828
858
  if (crossRunResume && runIndex < crossRunResume.runIndex) continue;
829
859
  const run = runs[runIndex];
860
+ if (run.kind === "renderedPageBreak") {
861
+ if (!runs.slice(runIndex + 1).some((followingRun) => followingRun.kind !== "renderedPageBreak")) {
862
+ currentLine.toRun = runIndex;
863
+ currentLine.toChar = 0;
864
+ continue;
865
+ }
866
+ if (currentLine.width > 0 || currentLine.maxImageHeightPx > 0 || currentLine.maxMathHeightPx > 0) startNewLine(runIndex + 1, 0);
867
+ else {
868
+ currentLine.fromRun = runIndex + 1;
869
+ currentLine.toRun = runIndex + 1;
870
+ currentLine.fromChar = 0;
871
+ currentLine.toChar = 0;
872
+ }
873
+ currentLine.renderedPageBreakBefore = true;
874
+ continue;
875
+ }
830
876
  if (isLineBreakRun(run)) {
831
877
  currentLine.toRun = runIndex;
832
878
  currentLine.toChar = 0;
@@ -952,13 +998,11 @@ function measureParagraph(block, maxWidth, options) {
952
998
  const wordBreaks = findWordBreaks(text, breakPolicy);
953
999
  let charIndex = crossRunResume?.runIndex === runIndex ? crossRunResume.charIndex : 0;
954
1000
  if (crossRunResume?.runIndex === runIndex) crossRunResume = void 0;
1001
+ let wordBreakIndex = 0;
955
1002
  let activeHyphenationWord;
956
1003
  while (charIndex < text.length) {
957
- let nextBreak = text.length;
958
- for (const breakPoint of wordBreaks) if (breakPoint > charIndex) {
959
- nextBreak = breakPoint;
960
- break;
961
- }
1004
+ while ((wordBreaks[wordBreakIndex] ?? Number.POSITIVE_INFINITY) <= charIndex) wordBreakIndex += 1;
1005
+ const nextBreak = wordBreaks[wordBreakIndex] ?? text.length;
962
1006
  const word = text.slice(charIndex, nextBreak);
963
1007
  const measuredWord = trimTrailingSpacesAndTabs(word);
964
1008
  const wordWidth = measureTextWidth(measuredWord, style);
@@ -1,3 +1,4 @@
1
+ import { toPaintedText } from "./textMeasurementPolicy.js";
1
2
  import { panic } from "better-result";
2
3
  //#region src/layout-engine/measure/measureProvider.ts
3
4
  /**
@@ -28,8 +29,8 @@ const resetMeasureProvider = () => {
28
29
  activeMeasureProvider = throwingMeasureProvider;
29
30
  };
30
31
  const getFontMetrics = (style) => activeMeasureProvider.getFontMetrics(style);
31
- const measureTextWidth = (text, style) => activeMeasureProvider.measureTextWidth(text, style);
32
- const measureText = (text, style) => activeMeasureProvider.measureText(text, style);
33
- const measureRun = (text, style) => activeMeasureProvider.measureRun(text, style);
32
+ const measureTextWidth = (text, style) => activeMeasureProvider.measureTextWidth(toPaintedText(text), style);
33
+ const measureText = (text, style) => activeMeasureProvider.measureText(toPaintedText(text), style);
34
+ const measureRun = (text, style) => activeMeasureProvider.measureRun(toPaintedText(text), style);
34
35
  //#endregion
35
36
  export { getFontMetrics, getMeasureProvider, measureRun, measureText, measureTextWidth, resetMeasureProvider, setMeasureProvider };
@@ -1,6 +1,6 @@
1
1
  import { measuredLineRangeHeight } from "../lineFlow.js";
2
2
  import { isFloatingTextBoxBlock } from "../types.js";
3
- import { getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph } from "../paragraphSpacing.js";
3
+ import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph } from "../paragraphSpacing.js";
4
4
  //#region src/layout-engine/measure/tableCellFlow.ts
5
5
  const createTableCellFlowState = () => ({
6
6
  height: 0,
@@ -57,7 +57,10 @@ const placeTableCellBlock = (state, block, measure) => {
57
57
  }
58
58
  const spacing = paragraphSpacing(block, measure);
59
59
  const empty = isEmptyParagraph(block);
60
- const leadingSpacing = empty && state.previousParagraphWasEmpty ? spacing.before + state.trailingSpacing : Math.max(spacing.before, state.trailingSpacing);
60
+ const leadingSpacing = empty && state.previousParagraphWasEmpty ? spacing.before + state.trailingSpacing : collapseParagraphSpacing({
61
+ before: spacing.before,
62
+ after: state.trailingSpacing
63
+ });
61
64
  const contentTop = top + leadingSpacing;
62
65
  state.height = contentTop + contentHeight;
63
66
  state.previousParagraphWasEmpty = empty;
@@ -1,18 +1,20 @@
1
- import { ParagraphBlock, ParagraphMeasure } from "../types.js";
1
+ import { ParagraphBlock, ParagraphMeasure, TableBlock, TableMeasure } from "../types.js";
2
2
 
3
3
  //#region src/layout-engine/measure/textBoxParagraphLayout.d.ts
4
- type TextBoxParagraphPlacement = {
4
+ type TextBoxContentPlacement = {
5
5
  leadingSpacing: number;
6
6
  contentHeight: number;
7
7
  };
8
- type TextBoxParagraphLayout = {
9
- placements: TextBoxParagraphPlacement[];
8
+ type TextBoxContentLayout = {
9
+ placements: TextBoxContentPlacement[];
10
10
  totalHeight: number;
11
11
  };
12
+ type TextBoxContentBlock = ParagraphBlock | TableBlock;
13
+ type TextBoxContentMeasure = ParagraphMeasure | TableMeasure;
12
14
  /**
13
- * Lay out a text box's paragraph story as one unpaginated flow. Adjacent
15
+ * Lay out a text box's block story as one unpaginated flow. Adjacent paragraph
14
16
  * before/after spacing collapses to the larger value, matching body flow.
15
17
  */
16
- declare function layoutTextBoxParagraphs(blocks: readonly ParagraphBlock[], measures: readonly ParagraphMeasure[]): TextBoxParagraphLayout;
18
+ declare function layoutTextBoxContent(blocks: readonly TextBoxContentBlock[], measures: readonly TextBoxContentMeasure[]): TextBoxContentLayout;
17
19
  //#endregion
18
- export { TextBoxParagraphLayout, TextBoxParagraphPlacement, layoutTextBoxParagraphs };
20
+ export { TextBoxContentLayout, TextBoxContentPlacement, layoutTextBoxContent };
@@ -1,20 +1,36 @@
1
1
  import { measuredLineRangeHeight } from "../lineFlow.js";
2
- import { getParagraphSpacingAfter, getParagraphSpacingBefore } from "../paragraphSpacing.js";
2
+ import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore } from "../paragraphSpacing.js";
3
3
  import { panic } from "better-result";
4
4
  //#region src/layout-engine/measure/textBoxParagraphLayout.ts
5
5
  /**
6
- * Lay out a text box's paragraph story as one unpaginated flow. Adjacent
6
+ * Lay out a text box's block story as one unpaginated flow. Adjacent paragraph
7
7
  * before/after spacing collapses to the larger value, matching body flow.
8
8
  */
9
- function layoutTextBoxParagraphs(blocks, measures) {
10
- if (blocks.length !== measures.length) panic("layoutTextBoxParagraphs: block and measure counts must match");
9
+ function layoutTextBoxContent(blocks, measures) {
10
+ if (blocks.length !== measures.length) panic("layoutTextBoxContent: block and measure counts must match");
11
11
  const placements = [];
12
12
  let totalHeight = 0;
13
13
  let trailingSpacing = 0;
14
14
  for (let index = 0; index < blocks.length; index += 1) {
15
15
  const block = blocks[index];
16
16
  const measure = measures[index];
17
- const leadingSpacing = Math.max(getParagraphSpacingBefore(block), trailingSpacing);
17
+ if (block.kind === "table") {
18
+ if (measure.kind !== "table") panic("layoutTextBoxContent: block and measure kinds must match");
19
+ const leadingSpacing = trailingSpacing;
20
+ const contentHeight = measure.totalHeight;
21
+ placements.push({
22
+ leadingSpacing,
23
+ contentHeight
24
+ });
25
+ totalHeight += leadingSpacing + contentHeight;
26
+ trailingSpacing = 0;
27
+ continue;
28
+ }
29
+ if (measure.kind !== "paragraph") panic("layoutTextBoxContent: block and measure kinds must match");
30
+ const leadingSpacing = collapseParagraphSpacing({
31
+ before: getParagraphSpacingBefore(block),
32
+ after: trailingSpacing
33
+ });
18
34
  const contentHeight = measuredLineRangeHeight(measure.lines, 0, measure.lines.length);
19
35
  placements.push({
20
36
  leadingSpacing,
@@ -29,4 +45,4 @@ function layoutTextBoxParagraphs(blocks, measures) {
29
45
  };
30
46
  }
31
47
  //#endregion
32
- export { layoutTextBoxParagraphs };
48
+ export { layoutTextBoxContent };
@@ -14,5 +14,7 @@ declare function getRunFontKerningMode(run: RunKerningInput, fallbackFontSize: n
14
14
  declare function getFontKerningMode(style: Pick<FontStyle, "kerning">): FontKerningMode;
15
15
  /** Count ordinary ASCII spaces that justification may compress. */
16
16
  declare function countCompressibleSpaces(text: string): number;
17
+ /** Paint OOXML no-break hyphens with the ordinary hyphen glyph. */
18
+ declare function toPaintedText(text: string): string;
17
19
  //#endregion
18
- export { FONT_KERNING_MODE, FontKerningMode, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode };
20
+ export { FONT_KERNING_MODE, FontKerningMode, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode, toPaintedText };
@@ -3,6 +3,8 @@ const FONT_KERNING_MODE = {
3
3
  enabled: "normal",
4
4
  disabled: "none"
5
5
  };
6
+ const NO_BREAK_HYPHEN = "‑";
7
+ const PAINTED_HYPHEN = "-";
6
8
  /** Resolve an authored kerning threshold against the effective run size. */
7
9
  function getRunFontKerningMode(run, fallbackFontSize) {
8
10
  const threshold = run.kerningMinPt;
@@ -20,5 +22,9 @@ function countCompressibleSpaces(text) {
20
22
  for (const char of text) if (char === " ") count += 1;
21
23
  return count;
22
24
  }
25
+ /** Paint OOXML no-break hyphens with the ordinary hyphen glyph. */
26
+ function toPaintedText(text) {
27
+ return text.replaceAll(NO_BREAK_HYPHEN, PAINTED_HYPHEN);
28
+ }
23
29
  //#endregion
24
- export { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode };
30
+ export { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode, toPaintedText };
@@ -1,4 +1,5 @@
1
1
  import { FOOTNOTE_SEPARATOR_HEIGHT } from "./types.js";
2
+ import { collapseParagraphSpacing } from "./paragraphSpacing.js";
2
3
  import { panic } from "better-result";
3
4
  //#region src/layout-engine/paginator.ts
4
5
  /**
@@ -178,7 +179,10 @@ function createPaginator(options) {
178
179
  function addFragment(fragment, height, spaceBefore = 0, spaceAfter = 0) {
179
180
  const initialState = getCurrentState();
180
181
  const initialColumnIndex = initialState.columnIndex;
181
- const effectiveSpaceBefore = Math.max(spaceBefore, initialState.trailingSpacing);
182
+ const effectiveSpaceBefore = collapseParagraphSpacing({
183
+ before: spaceBefore,
184
+ after: initialState.trailingSpacing
185
+ });
182
186
  const state = ensureFits(effectiveSpaceBefore + height);
183
187
  const actualSpaceBefore = state === initialState && state.columnIndex === initialColumnIndex ? effectiveSpaceBefore : spaceBefore;
184
188
  const x = getColumnX(state.columnIndex);
@@ -1,17 +1,41 @@
1
- import { ParagraphBlock } from "./types.js";
1
+ import { FlowBlock, ParagraphBlock } from "./types.js";
2
2
 
3
3
  //#region src/layout-engine/paragraphSpacing.d.ts
4
+ type EffectiveParagraphSpacing = Readonly<{
5
+ before: number;
6
+ after: number;
7
+ }>;
8
+ type CollapseParagraphSpacingOptions = {
9
+ before: number;
10
+ after: number;
11
+ };
12
+ /** Collapse adjacent paragraph spacing to the larger authored side. */
13
+ declare const collapseParagraphSpacing: ({
14
+ before,
15
+ after
16
+ }: CollapseParagraphSpacingOptions) => number;
4
17
  /** Whether a paragraph has no visible run content. */
5
18
  declare function isEmptyParagraph(block: ParagraphBlock): boolean;
6
19
  /**
7
20
  * Resolve leading paragraph spacing after applying the empty-paragraph
8
21
  * inherited-spacing collapse rule.
9
22
  */
10
- declare function getParagraphSpacingBefore(block: ParagraphBlock): number;
23
+ declare const getParagraphSpacingBefore: (block: ParagraphBlock) => number;
11
24
  /**
12
25
  * Resolve trailing paragraph spacing after applying the empty-paragraph
13
26
  * inherited-spacing collapse rule.
14
27
  */
15
- declare function getParagraphSpacingAfter(block: ParagraphBlock): number;
28
+ declare const getParagraphSpacingAfter: (block: ParagraphBlock) => number;
29
+ /** Resolve empty-paragraph spacing without modifying the authored block. */
30
+ declare const resolveEffectiveParagraphSpacing: (block: ParagraphBlock) => EffectiveParagraphSpacing;
31
+ /**
32
+ * Derive sequence-level spacing suppression with structural sharing.
33
+ *
34
+ * Contextual and automatic list spacing affect layout values, not authored
35
+ * OOXML formatting. Only changed branches are cloned; the input tree remains
36
+ * untouched and repeated resolution is idempotent.
37
+ */
38
+ declare const resolveEffectiveParagraphSpacingTree: (blocks: FlowBlock[]) => FlowBlock[];
39
+ declare const paragraphsShareStyle: (previous: ParagraphBlock, current: ParagraphBlock) => boolean;
16
40
  //#endregion
17
- export { getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph };
41
+ export { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacing, resolveEffectiveParagraphSpacingTree };