@stll/folio-core 0.47.7 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/controller/fontReadiness.js +3 -3
  2. package/dist/controller/hyphenationReadiness.d.ts +19 -0
  3. package/dist/controller/hyphenationReadiness.js +47 -0
  4. package/dist/controller/layoutPipeline.d.ts +3 -0
  5. package/dist/controller/layoutPipeline.js +59 -17
  6. package/dist/display-list/build/buildDisplayList.js +39 -12
  7. package/dist/display-list/build/pageFurniture.d.ts +7 -1
  8. package/dist/display-list/build/pageFurniture.js +21 -8
  9. package/dist/display-list/build/paragraphPrimitives.js +5 -4
  10. package/dist/display-list/build/tablePrimitives.js +10 -6
  11. package/dist/docx/attributeRemainder.js +10 -2
  12. package/dist/docx/blockContentParser.d.ts +2 -0
  13. package/dist/docx/blockContentParser.js +77 -62
  14. package/dist/docx/commentParser.d.ts +2 -1
  15. package/dist/docx/commentParser.js +57 -45
  16. package/dist/docx/containerChildren.d.ts +21 -8
  17. package/dist/docx/containerChildren.js +39 -9
  18. package/dist/docx/documentParser.d.ts +5 -1
  19. package/dist/docx/documentParser.js +8 -5
  20. package/dist/docx/documentSectionFacts.d.ts +22 -0
  21. package/dist/docx/documentSectionFacts.js +43 -0
  22. package/dist/docx/fieldParser.d.ts +8 -1
  23. package/dist/docx/fieldParser.js +47 -3
  24. package/dist/docx/fontTableParser.js +34 -27
  25. package/dist/docx/footnoteParser.d.ts +3 -2
  26. package/dist/docx/footnoteParser.js +15 -12
  27. package/dist/docx/groupDrawingParser.d.ts +2 -1
  28. package/dist/docx/groupDrawingParser.js +3 -6
  29. package/dist/docx/headerFooterParser.d.ts +7 -2
  30. package/dist/docx/headerFooterParser.js +15 -5
  31. package/dist/docx/hyperlinkParser.d.ts +48 -36
  32. package/dist/docx/hyperlinkParser.js +97 -82
  33. package/dist/docx/numberingParser.js +108 -84
  34. package/dist/docx/paragraphParser.d.ts +5 -1
  35. package/dist/docx/paragraphParser.js +381 -362
  36. package/dist/docx/paragraphProperties.js +123 -109
  37. package/dist/docx/paragraphPropertySource.d.ts +6 -1
  38. package/dist/docx/paragraphPropertySource.js +9 -1
  39. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  40. package/dist/docx/paragraphTextBoxEnrichment.js +17 -6
  41. package/dist/docx/parser.d.ts +7 -1
  42. package/dist/docx/parser.js +33 -16
  43. package/dist/docx/previewBudget.d.ts +55 -33
  44. package/dist/docx/previewBudget.js +59 -65
  45. package/dist/docx/rezip.js +41 -58
  46. package/dist/docx/runParser.d.ts +4 -1
  47. package/dist/docx/runParser.js +172 -184
  48. package/dist/docx/sdtProperties.js +87 -84
  49. package/dist/docx/sectionParser.js +169 -160
  50. package/dist/docx/serializer/paragraphSerializer.js +42 -38
  51. package/dist/docx/serializer/settingsSerializer.js +19 -1
  52. package/dist/docx/settingsParser.js +8 -0
  53. package/dist/docx/streamingXmlParser.d.ts +6 -2
  54. package/dist/docx/streamingXmlParser.js +12 -7
  55. package/dist/docx/styleParser.js +34 -8
  56. package/dist/docx/tableParser.d.ts +21 -12
  57. package/dist/docx/tableParser.js +518 -441
  58. package/dist/docx/textBoxParser.d.ts +8 -3
  59. package/dist/docx/textBoxParser.js +3 -3
  60. package/dist/docx/verbatimCapture.d.ts +10 -8
  61. package/dist/docx/verbatimCapture.js +45 -2
  62. package/dist/docx/vmlImageParser.d.ts +15 -13
  63. package/dist/docx/vmlImageParser.js +75 -60
  64. package/dist/docx/vmlPreview.d.ts +3 -3
  65. package/dist/docx/vmlPreview.js +3 -4
  66. package/dist/docx/xmlNamespaceContext.d.ts +9 -0
  67. package/dist/docx/xmlNamespaceContext.js +54 -0
  68. package/dist/docx/xmlParser.d.ts +6 -3
  69. package/dist/docx/xmlParser.js +11 -41
  70. package/dist/docx/xmlResourceLimits.d.ts +2 -1
  71. package/dist/docx/xmlResourceLimits.js +4 -1
  72. package/dist/fonts/headlessMeasure.js +0 -0
  73. package/dist/headless-layout.d.ts +11 -2
  74. package/dist/headless-layout.js +153 -107
  75. package/dist/internal/paragraphFormattingSerialization.js +3 -3
  76. package/dist/layout-bridge/convert/endnoteLayout.d.ts +65 -0
  77. package/dist/layout-bridge/convert/endnoteLayout.js +227 -0
  78. package/dist/layout-bridge/convert/footnoteLayout.d.ts +19 -3
  79. package/dist/layout-bridge/convert/footnoteLayout.js +40 -31
  80. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -7
  81. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -20
  82. package/dist/layout-bridge/convert/paragraphMarkFormatting.d.ts +10 -0
  83. package/dist/layout-bridge/convert/paragraphMarkFormatting.js +59 -0
  84. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +29 -1
  85. package/dist/layout-bridge/convert/toFlowBlocks.js +258 -35
  86. package/dist/layout-bridge/engine/hitTest.js +5 -3
  87. package/dist/layout-bridge/engine/selectionRects.js +5 -3
  88. package/dist/layout-engine/anchorLayoutInCellCompatibility.d.ts +10 -0
  89. package/dist/layout-engine/anchorLayoutInCellCompatibility.js +22 -0
  90. package/dist/layout-engine/index.d.ts +2 -2
  91. package/dist/layout-engine/index.js +70 -11
  92. package/dist/layout-engine/justificationCompatibility.d.ts +1 -1
  93. package/dist/layout-engine/justificationCompatibility.js +9 -5
  94. package/dist/layout-engine/keep-together.d.ts +11 -5
  95. package/dist/layout-engine/keep-together.js +39 -16
  96. package/dist/layout-engine/layoutInstrumentation.d.ts +8 -2
  97. package/dist/layout-engine/layoutInstrumentation.js +8 -1
  98. package/dist/layout-engine/measure/advanceComposition.js +4 -7
  99. package/dist/layout-engine/measure/hyphenationDictionaries.d.ts +74 -0
  100. package/dist/layout-engine/measure/hyphenationDictionaries.js +138 -0
  101. package/dist/layout-engine/measure/hyphenationPreload.d.ts +11 -0
  102. package/dist/layout-engine/measure/hyphenationPreload.js +24 -0
  103. package/dist/layout-engine/measure/lineBreakProvider.d.ts +12 -2
  104. package/dist/layout-engine/measure/lineBreakProvider.js +30 -30
  105. package/dist/layout-engine/measure/lineBreaks.d.ts +6 -1
  106. package/dist/layout-engine/measure/lineBreaks.js +10 -3
  107. package/dist/layout-engine/measure/listMarkerWidth.d.ts +9 -6
  108. package/dist/layout-engine/measure/listMarkerWidth.js +20 -22
  109. package/dist/layout-engine/measure/measureBlocks.js +42 -8
  110. package/dist/layout-engine/measure/measureContainer.js +5 -5
  111. package/dist/layout-engine/measure/measureHelpers.d.ts +15 -3
  112. package/dist/layout-engine/measure/measureHelpers.js +21 -5
  113. package/dist/layout-engine/measure/measureParagraph.js +109 -30
  114. package/dist/layout-engine/measure/measureTypes.d.ts +5 -0
  115. package/dist/layout-engine/measure/tabCalculator.d.ts +2 -0
  116. package/dist/layout-engine/measure/tabCalculator.js +2 -1
  117. package/dist/layout-engine/measure/tableCellFloating.d.ts +2 -0
  118. package/dist/layout-engine/measure/tableCellFloating.js +51 -19
  119. package/dist/layout-engine/measure/tableCellGrid.d.ts +28 -3
  120. package/dist/layout-engine/measure/tableCellGrid.js +45 -5
  121. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +2 -1
  122. package/dist/layout-engine/noteAreaFlow.d.ts +19 -0
  123. package/dist/layout-engine/noteAreaFlow.js +83 -0
  124. package/dist/layout-engine/paginator.d.ts +10 -1
  125. package/dist/layout-engine/paginator.js +13 -1
  126. package/dist/layout-engine/tableRowBreak.js +3 -1
  127. package/dist/layout-engine/types.d.ts +89 -3
  128. package/dist/layout-engine/types.js +96 -1
  129. package/dist/layout-painter/index.d.ts +3 -0
  130. package/dist/layout-painter/renderPage.js +32 -83
  131. package/dist/layout-painter/renderParagraph.d.ts +6 -1
  132. package/dist/layout-painter/renderParagraph.js +34 -22
  133. package/dist/layout-painter/renderTable.js +26 -13
  134. package/dist/prosemirror/attrs/index.d.ts +9 -9
  135. package/dist/prosemirror/attrs/index.js +43 -11
  136. package/dist/prosemirror/conversion/fromProseDoc.js +8 -1
  137. package/dist/prosemirror/conversion/markInterner.d.ts +21 -0
  138. package/dist/prosemirror/conversion/markInterner.js +90 -0
  139. package/dist/prosemirror/conversion/toProseDoc.js +77 -41
  140. package/dist/prosemirror/extensions/core/DocExtension.js +2 -1
  141. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  142. package/dist/prosemirror/extensions/marks/markUtils.d.ts +6 -2
  143. package/dist/prosemirror/extensions/marks/markUtils.js +39 -51
  144. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +2 -1
  145. package/dist/prosemirror/listMarker.d.ts +1 -0
  146. package/dist/prosemirror/listMarker.js +2 -0
  147. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  148. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  149. package/dist/prosemirror/schema/nodes.d.ts +13 -1
  150. package/dist/prosemirror/textBoxHostParagraph.d.ts +13 -0
  151. package/dist/prosemirror/textBoxHostParagraph.js +21 -0
  152. package/dist/prosemirror/validation.js +91 -80
  153. package/dist/utils/createDocument.js +10 -1
  154. package/dist/utils/scriptSegments.d.ts +33 -4
  155. package/dist/utils/scriptSegments.js +39 -6
  156. package/dist/utils/textFormattingMerge.d.ts +17 -3
  157. package/dist/utils/textFormattingMerge.js +18 -12
  158. package/dist/utils/trailingText.d.ts +22 -0
  159. package/dist/utils/trailingText.js +32 -0
  160. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, tableColumnsArePinned } from "./types.js";
1
+ import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, MathRun, Measure, MeasuredLine, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, NoteAreaLayout, NoteSeparatorKind, NoteSeparatorRule, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, stripFlowBlockPmAnchors, tableColumnsArePinned } from "./types.js";
2
2
  import { PageState, Paginator, PaginatorOptions, createPaginator } from "./paginator.js";
3
3
  import { KeepNextChain, calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
4
4
  import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
@@ -42,4 +42,4 @@ declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], option
42
42
  */
43
43
  declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;
44
44
  //#endregion
45
- export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, SectionPageNumbering, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, resolveTableWidthPx, scheduleSectionBreak, tableColumnsArePinned };
45
+ export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, MathRun, Measure, MeasuredLine, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, NoteAreaLayout, NoteSeparatorKind, NoteSeparatorRule, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, SectionPageNumbering, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, resolveTableWidthPx, scheduleSectionBreak, stripFlowBlockPmAnchors, tableColumnsArePinned };
@@ -2,9 +2,10 @@ import { emuToPixels } from "../utils/units.js";
2
2
  import { reflowFootnoteColumns } from "./footnoteColumnReflow.js";
3
3
  import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
4
4
  import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
5
- import { measuredLineAdvance } from "./lineFlow.js";
5
+ import { measuredLineAdvance, measuredLineRangeHeight } from "./lineFlow.js";
6
6
  import { resolveFloatingTablePageX } from "./measure/floatingTablePosition.js";
7
7
  import { resolveTableInlineOffset } from "./measure/tableInlinePlacement.js";
8
+ import { annotateNoteSeparators, continuesNoteArea, createNoteAreaContinuation } from "./noteAreaFlow.js";
8
9
  import { SECTION_START_PLACEMENT, createPaginator } from "./paginator.js";
9
10
  import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
10
11
  import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacingTree } from "./paragraphSpacing.js";
@@ -14,7 +15,7 @@ import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, g
14
15
  import { applySectionVerticalAlignment } from "./sectionVerticalAlignment.js";
15
16
  import { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak } from "./tableRowBreak.js";
16
17
  import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
17
- import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, tableColumnsArePinned } from "./types.js";
18
+ import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, stripFlowBlockPmAnchors, tableColumnsArePinned } from "./types.js";
18
19
  import { panic } from "better-result";
19
20
  //#region src/layout-engine/index.ts
20
21
  /**
@@ -147,14 +148,14 @@ function applyContextualSpacing(blocks) {
147
148
  */
148
149
  function layoutDocument(blocks, measures, options) {
149
150
  const initialLayout = layoutDocumentPass(blocks, measures, options);
150
- return applySectionVerticalAlignment(options.footnoteHeightById ? reflowFootnoteColumns({
151
+ return annotateNoteSeparators(applySectionVerticalAlignment(options.footnoteHeightById ? reflowFootnoteColumns({
151
152
  initialLayout,
152
153
  ...options.footnoteReservedHeights ? { initialReserveFloors: options.footnoteReservedHeights } : {},
153
154
  runLayout: (reserveFloors) => layoutDocumentPass(blocks, measures, {
154
155
  ...options,
155
156
  footnoteReservedHeights: reserveFloors
156
157
  })
157
- }) : initialLayout, options.sectionVerticalAlignments);
158
+ }) : initialLayout, options.sectionVerticalAlignments), blocks, measures, options.noteAreas);
158
159
  }
159
160
  function layoutDocumentPass(blocks, measures, options) {
160
161
  if (blocks.length !== measures.length) panic(`layoutDocument: expected one measure per block (blocks=${blocks.length}, measures=${measures.length})`);
@@ -208,9 +209,11 @@ function layoutDocumentPass(blocks, measures, options) {
208
209
  let activeSectionPageHeight = initialConfig.pageSize.h;
209
210
  let activeSectionMarginBottom = initialConfig.margins.bottom;
210
211
  let renderedBreakState = INITIAL_RENDERED_BREAK_STATE;
212
+ const noteAreaContinuation = createNoteAreaContinuation(options.noteAreas, paginator);
211
213
  for (let i = 0; i < blocks.length; i++) {
212
214
  const block = blocks[i];
213
215
  const measure = measures[i];
216
+ if (noteAreaContinuation !== void 0) paginator.setPageTopContinuation(continuesNoteArea(options.noteAreas, block) ? noteAreaContinuation : void 0);
214
217
  const firstLine = measure.kind === "paragraph" ? measure.lines.at(0) : void 0;
215
218
  const firstLineAdvance = firstLine ? measuredLineAdvance(firstLine) : 0;
216
219
  const renderedBreakNeedsSnap = measure.kind === "paragraph" && (!paginator.fits(measure.totalHeight) || firstLineAdvance > 0 && paginator.getAvailableHeight() <= firstLineAdvance * RENDERED_BREAK_REFLOW_TOLERANCE_LINES);
@@ -225,13 +228,13 @@ function layoutDocumentPass(blocks, measures, options) {
225
228
  renderedBreakNeedsSnap
226
229
  });
227
230
  if (block.kind !== "pageBreak") {
228
- if (breakDecision.pageAdvance === PAGE_ADVANCE.PHYSICAL) paginator.forcePageBreak();
231
+ if (breakDecision.pageAdvance === PAGE_ADVANCE.PHYSICAL) paginator.forcePageBreak({ coalesceBlankPage: hasPageBreakBefore(block) });
229
232
  else if (breakDecision.pageAdvance === PAGE_ADVANCE.COALESCED) paginator.coalescePageBreak();
230
233
  }
231
234
  renderedBreakState = breakDecision.state;
232
235
  const chain = keepNextChains.get(i);
233
236
  if (chain && !midChainIndices.has(i)) {
234
- const chainHeight = calculateChainHeight(chain, blocks, measures);
237
+ const chainHeight = calculateChainHeight(chain, blocks, measures, paginator.getCurrentState().trailingSpacing);
235
238
  const pageBeforeChainLayout = paginator.getCurrentState().page.number;
236
239
  paginator.ensureFits(chainHeight);
237
240
  if (paginator.getCurrentState().page.number > pageBeforeChainLayout) renderedBreakState = recordReflowBoundary(renderedBreakState, true);
@@ -306,6 +309,7 @@ function layoutDocumentPass(blocks, measures, options) {
306
309
  previousPage: paginator.states[pageBeforeBlockLayout - 1]?.page
307
310
  });
308
311
  }
312
+ paginator.setPageTopContinuation(void 0);
309
313
  if (paginator.states.length === 0) paginator.getCurrentState();
310
314
  return {
311
315
  pageSize,
@@ -376,6 +380,16 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
376
380
  }
377
381
  const spaceBefore = suppressSpaceBefore ? 0 : getParagraphSpacingBefore(block);
378
382
  const spaceAfter = getParagraphSpacingAfter(block);
383
+ let keepLinesDemand;
384
+ if (lines.length > 1 && hasKeepLines(block)) {
385
+ let footnoteHeight = 0;
386
+ for (const line of lines) footnoteHeight += getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById).height;
387
+ keepLinesDemand = {
388
+ linesHeight: measuredLineRangeHeight(lines, 0, lines.length),
389
+ footnoteHeight
390
+ };
391
+ }
392
+ let keepLinesMoveAttempted = false;
379
393
  let currentLineIndex = 0;
380
394
  while (currentLineIndex < lines.length) {
381
395
  const state = paginator.getCurrentState();
@@ -400,6 +414,19 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
400
414
  continue;
401
415
  }
402
416
  }
417
+ if (keepLinesDemand !== void 0 && !keepLinesMoveAttempted && currentLineIndex === 0 && state.cursorY !== state.topMargin) {
418
+ const wholeHeight = keepLinesDemand.linesHeight + projectedFootnoteReserveGrowth(state, keepLinesDemand.footnoteHeight);
419
+ const collapsedLead = collapseParagraphSpacing({
420
+ before: spaceBefore,
421
+ after: state.trailingSpacing
422
+ });
423
+ const columnCapacity = state.contentBottom - state.topMargin;
424
+ if (collapsedLead + wholeHeight > availableHeight && spaceBefore + wholeHeight <= columnCapacity) {
425
+ keepLinesMoveAttempted = true;
426
+ paginator.ensureFits(collapsedLead + wholeHeight);
427
+ continue;
428
+ }
429
+ }
403
430
  let linesHeight = 0;
404
431
  let linesFnHeight = 0;
405
432
  const linesFnIds = [];
@@ -561,13 +588,19 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
561
588
  return previous?.kind === "table" && previous.blockId === block.id && previous.toRow === rowIndex && previous.y + previous.height === state.cursorY;
562
589
  };
563
590
  const shouldRepeatHeaderRows = (rowIndex, consumed, state = paginator.getCurrentState()) => headerRowCount > 0 && rowIndex >= headerRowCount && !(consumed === 0 && hasAdjacentPriorTableRows(rowIndex, state));
591
+ const rowOpensFlowRegion = (rowIndex, state) => {
592
+ if (state.cursorY === state.topMargin) return true;
593
+ const previous = state.page.fragments.at(-1);
594
+ return rowIndex > 0 && rowIndex <= headerRowCount && previous?.kind === "table" && previous.blockId === block.id && previous.fromRow === 0 && previous.toRow === rowIndex && previous.y === state.topMargin && previous.y + previous.height === state.cursorY;
595
+ };
564
596
  const canSplitRow = (rowIndex, state = paginator.getCurrentState()) => {
565
597
  const row = rows[rowIndex];
566
598
  const sourceRow = block.rows[rowIndex];
567
- if (!row || !sourceRow || sourceRow.cantSplit || sourceRow.isHeader || verticallyMergedRows.has(rowIndex)) return false;
599
+ if (!row || !sourceRow || sourceRow.isHeader || verticallyMergedRows.has(rowIndex)) return false;
568
600
  if ((breakInfo.breakOffsets[rowIndex]?.length ?? 0) <= 1) return false;
569
601
  const freshHeaderOverhead = headerRowCount > 0 && rowIndex >= headerRowCount ? headerRowsHeight : 0;
570
602
  const oversized = row.height + freshHeaderOverhead > getCurrentRowCapacity(state);
603
+ if (sourceRow.cantSplit) return oversized && sourceRow.heightRule !== "exact" && rowOpensFlowRegion(rowIndex, state);
571
604
  if (!oversized && (state.footnoteHeight > 0 || (rowFootnoteIds[rowIndex]?.length ?? 0) > 0)) return false;
572
605
  const currentHeaderOverhead = shouldRepeatHeaderRows(rowIndex, 0, state) ? headerRowsHeight : 0;
573
606
  const currentAvailableHeight = paginator.getAvailableHeight() - currentHeaderOverhead - state.trailingSpacing;
@@ -955,18 +988,44 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
955
988
  paginator.addUnflowedFragment(fragment);
956
989
  return;
957
990
  }
958
- const state = paginator.ensureFits(measure.height);
991
+ const effect = block.effectExtent;
992
+ const occupiedHeight = measure.height + (effect?.top ?? 0) + (effect?.bottom ?? 0);
993
+ const occupiedWidth = measure.width + (effect?.left ?? 0) + (effect?.right ?? 0);
994
+ const host = block.hostParagraph;
959
995
  const fragment = {
960
996
  kind: "textBox",
961
997
  blockId: block.id,
962
- x: paginator.getColumnX(state.columnIndex),
998
+ x: 0,
963
999
  y: 0,
964
1000
  width: measure.width,
965
1001
  height: measure.height,
966
1002
  ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
967
1003
  ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
968
1004
  };
969
- fragment.y = paginator.addFragment(fragment, measure.height, 0, 0).y;
1005
+ const result = paginator.addFragment(fragment, occupiedHeight, host?.spacing?.before ?? 0, host?.spacing?.after ?? 0);
1006
+ fragment.x = result.x + hostParagraphLineOffset(host, paginator.columnWidth, occupiedWidth) + (effect?.left ?? 0);
1007
+ fragment.y = result.y + (effect?.top ?? 0);
1008
+ }
1009
+ /**
1010
+ * Where a line holding only an object of `width` starts within a column:
1011
+ * after the host paragraph's start and first-line indents, then aligned in
1012
+ * what remains before its end indent.
1013
+ */
1014
+ function hostParagraphLineOffset(host, columnWidth, width) {
1015
+ if (!host) return 0;
1016
+ const indent = host.indent;
1017
+ const start = (indent?.left ?? 0) + (indent?.firstLine ?? 0) - (indent?.hanging ?? 0);
1018
+ const slack = Math.max(0, columnWidth - start - (indent?.right ?? 0) - width);
1019
+ switch (host.alignment) {
1020
+ case "center": return start + slack / 2;
1021
+ case "right": return start + slack;
1022
+ case "left":
1023
+ case "justify":
1024
+ case void 0: return start;
1025
+ default:
1026
+ host.alignment;
1027
+ return start;
1028
+ }
970
1029
  }
971
1030
  /**
972
1031
  * A topAndBottom text box whose vertical anchor pins it to the page frame
@@ -1072,4 +1131,4 @@ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionTyp
1072
1131
  paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
1073
1132
  }
1074
1133
  //#endregion
1075
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, resolveTableWidthPx, scheduleSectionBreak, tableColumnsArePinned };
1134
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, resolveTableWidthPx, scheduleSectionBreak, stripFlowBlockPmAnchors, tableColumnsArePinned };
@@ -1,5 +1,5 @@
1
1
  import { ParagraphAttrs } from "./types.js";
2
2
  //#region src/layout-engine/justificationCompatibility.d.ts
3
- declare function resolveJustificationCompatibility(compatibilityMode: number | undefined): NonNullable<ParagraphAttrs["justificationCompatibility"]> | undefined;
3
+ declare const resolveJustificationCompatibility: (compatibilityMode: number | undefined) => NonNullable<ParagraphAttrs["justificationCompatibility"]> | undefined;
4
4
  //#endregion
5
5
  export { resolveJustificationCompatibility };
@@ -1,8 +1,12 @@
1
1
  //#region src/layout-engine/justificationCompatibility.ts
2
- const LEGACY_JUSTIFICATION_MAX_COMPATIBILITY_MODE = 14;
3
- function resolveJustificationCompatibility(compatibilityMode) {
4
- if (compatibilityMode === void 0 || compatibilityMode > LEGACY_JUSTIFICATION_MAX_COMPATIBILITY_MODE) return;
5
- return { type: "legacy" };
6
- }
2
+ /**
3
+ * First `compatibilityMode` that fits justified lines with the current rules.
4
+ *
5
+ * Earlier modes keep strict justified fitting. A document that declares no
6
+ * `compatSetting` at all is read with the oldest semantics, so it takes the
7
+ * legacy rule too.
8
+ */
9
+ const FIRST_MODERN_JUSTIFICATION_COMPATIBILITY_MODE = 15;
10
+ const resolveJustificationCompatibility = (compatibilityMode) => compatibilityMode !== void 0 && compatibilityMode >= FIRST_MODERN_JUSTIFICATION_COMPATIBILITY_MODE ? void 0 : { type: "legacy" };
7
11
  //#endregion
8
12
  export { resolveJustificationCompatibility };
@@ -30,12 +30,18 @@ declare function computeKeepNextChains(blocks: FlowBlock[]): Map<number, KeepNex
30
30
  * Calculate the height needed to keep consecutive paragraph boundaries from
31
31
  * breaking across pages.
32
32
  *
33
- * Single-line and `keepLines` members are indivisible, so the reservation must
34
- * continue through them to the anchor's first line. A multi-line member without
35
- * `keepLines` may itself split; only its first line is needed to satisfy the
36
- * preceding member's `keepNext`, and the chain can stop there.
33
+ * Each successor (later member or the anchor) must bring its minimum
34
+ * unbreakable opening onto the page: see {@link minimumOpeningLineCount}. When
35
+ * that opening is the whole paragraph, the successor is indivisible and the
36
+ * reservation continues through it; otherwise the chain stops after the
37
+ * opening, because a later split cannot separate it from its predecessor.
38
+ *
39
+ * A paragraph measure's `totalHeight` already includes its own spacing before
40
+ * and after. The chain accounts for spacing separately (collapsing each gap to
41
+ * the larger side, as the paginator does), so members contribute only their
42
+ * line advances; otherwise every gap would be reserved twice.
37
43
  */
38
- declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[]): number;
44
+ declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[], incomingSpacing?: number): number;
39
45
  /**
40
46
  * Get the set of indices that are mid-chain (not chain starters).
41
47
  * These should skip the keepNext check since their chain starter already decided.
@@ -1,3 +1,4 @@
1
+ import { measuredLineRangeHeight } from "./lineFlow.js";
1
2
  import { collapseParagraphSpacing, isAuthoredEmptyParagraph, isEmptyParagraph } from "./paragraphSpacing.js";
2
3
  //#region src/layout-engine/keep-together.ts
3
4
  /**
@@ -6,6 +7,8 @@ import { collapseParagraphSpacing, isAuthoredEmptyParagraph, isEmptyParagraph }
6
7
  * DOCX paragraphs can have keepNext (keep with next paragraph) and keepLines
7
8
  * (keep all lines together) properties that affect pagination.
8
9
  */
10
+ /** Lines §17.3.1.44 widow control keeps on each side of a paragraph split. */
11
+ const MIN_WIDOW_CONTROL_SPLIT_LINES = 2;
9
12
  /**
10
13
  * Whether an empty paragraph is only a structural separator through which a
11
14
  * keep-with-next chain may pass. An explicitly styled or directly formatted
@@ -74,23 +77,47 @@ function computeKeepNextChains(blocks) {
74
77
  return chains;
75
78
  }
76
79
  /**
80
+ * Lines of a paragraph that must share a page with the paragraph before it for
81
+ * a `w:keepNext` link (§17.3.1.15) to hold.
82
+ *
83
+ * - `w:keepLines` (§17.3.1.14): the paragraph never splits, so all of it.
84
+ * - `w:widowControl` (§17.3.1.44): a split leaves at least two lines on each
85
+ * side, so a paragraph shorter than four lines cannot split at all and a
86
+ * longer one needs its first two lines.
87
+ * - Otherwise a single line.
88
+ */
89
+ function minimumOpeningLineCount(block, measure) {
90
+ const lineCount = measure.lines.length;
91
+ if (block.attrs?.keepLines === true) return lineCount;
92
+ if (block.attrs?.widowControl !== false) return lineCount < MIN_WIDOW_CONTROL_SPLIT_LINES * 2 ? lineCount : MIN_WIDOW_CONTROL_SPLIT_LINES;
93
+ return Math.min(1, lineCount);
94
+ }
95
+ /**
77
96
  * Calculate the height needed to keep consecutive paragraph boundaries from
78
97
  * breaking across pages.
79
98
  *
80
- * Single-line and `keepLines` members are indivisible, so the reservation must
81
- * continue through them to the anchor's first line. A multi-line member without
82
- * `keepLines` may itself split; only its first line is needed to satisfy the
83
- * preceding member's `keepNext`, and the chain can stop there.
99
+ * Each successor (later member or the anchor) must bring its minimum
100
+ * unbreakable opening onto the page: see {@link minimumOpeningLineCount}. When
101
+ * that opening is the whole paragraph, the successor is indivisible and the
102
+ * reservation continues through it; otherwise the chain stops after the
103
+ * opening, because a later split cannot separate it from its predecessor.
104
+ *
105
+ * A paragraph measure's `totalHeight` already includes its own spacing before
106
+ * and after. The chain accounts for spacing separately (collapsing each gap to
107
+ * the larger side, as the paginator does), so members contribute only their
108
+ * line advances; otherwise every gap would be reserved twice.
84
109
  */
85
- function calculateChainHeight(chain, blocks, measures) {
110
+ function calculateChainHeight(chain, blocks, measures, incomingSpacing = 0) {
86
111
  const firstMemberIndex = chain.memberIndices.at(0);
87
112
  if (firstMemberIndex === void 0) return 0;
88
113
  const firstBlock = blocks[firstMemberIndex];
89
114
  const firstMeasure = measures[firstMemberIndex];
90
115
  if (firstBlock?.kind !== "paragraph" || firstMeasure?.kind !== "paragraph") return 0;
91
- let totalHeight = (firstBlock.attrs?.spacing?.before ?? 0) + firstMeasure.totalHeight;
116
+ let totalHeight = collapseParagraphSpacing({
117
+ before: firstBlock.attrs?.spacing?.before ?? 0,
118
+ after: incomingSpacing
119
+ }) + measuredLineRangeHeight(firstMeasure.lines, 0, firstMeasure.lines.length);
92
120
  let trailingSpacing = firstBlock.attrs?.spacing?.after ?? 0;
93
- const startsWithTrailingTableSeparator = blocks[firstMemberIndex - 1]?.kind === "table" && isEmptyParagraph(firstBlock) && !firstBlock.attrs?.keepNext;
94
121
  const successorIndices = [...chain.memberIndices.slice(1)];
95
122
  if (chain.anchorIndex !== -1) successorIndices.push(chain.anchorIndex);
96
123
  for (let index = 0; index < successorIndices.length; index++) {
@@ -103,16 +130,12 @@ function calculateChainHeight(chain, blocks, measures) {
103
130
  before: successorBlock.attrs?.spacing?.before ?? 0,
104
131
  after: trailingSpacing
105
132
  });
106
- const firstLine = successorMeasure.lines.at(0);
107
- if (!firstLine) return totalHeight;
133
+ const lineCount = successorMeasure.lines.length;
134
+ if (lineCount === 0) return totalHeight;
108
135
  const isAnchor = index === successorIndices.length - 1 && chain.anchorIndex !== -1;
109
- const isSplittable = successorMeasure.lines.length > 1 && !successorBlock.attrs?.keepLines;
110
- if (isAnchor && startsWithTrailingTableSeparator && successorBlock.attrs?.widowControl !== false && successorMeasure.lines.length > 1) {
111
- const secondLine = successorMeasure.lines.at(1);
112
- return totalHeight + firstLine.lineHeight + (secondLine?.lineHeight ?? 0);
113
- }
114
- if (isAnchor || isSplittable) return totalHeight + firstLine.lineHeight;
115
- totalHeight += successorMeasure.totalHeight;
136
+ const openingLines = minimumOpeningLineCount(successorBlock, successorMeasure);
137
+ totalHeight += measuredLineRangeHeight(successorMeasure.lines, 0, openingLines);
138
+ if (isAnchor || openingLines < lineCount) return totalHeight;
116
139
  trailingSpacing = successorBlock.attrs?.spacing?.after ?? 0;
117
140
  }
118
141
  return totalHeight;
@@ -1,6 +1,6 @@
1
1
  import { FlowBlock } from "./types.js";
2
2
  //#region src/layout-engine/layoutInstrumentation.d.ts
3
- type LayoutRunReason = "font-ready" | "initial" | "layout-input" | "manual" | "transaction";
3
+ type LayoutRunReason = "font-ready" | "hyphenation-ready" | "initial" | "layout-input" | "manual" | "transaction";
4
4
  type LayoutPhase = "flow-blocks" | "header-footer" | "initial-fonts" | "layout-document" | "measure-blocks" | "render-pages";
5
5
  type HiddenEditorStateReason = "external-document" | "mount";
6
6
  type HiddenEditorPhase = "editor-state" | "editor-view" | "to-prose-doc" | "update-state";
@@ -18,6 +18,10 @@ type LayoutInstrumentation = {
18
18
  onHiddenEditorStateCreate?: (event: {
19
19
  reason: HiddenEditorStateReason;
20
20
  }) => void;
21
+ onHyphenationDictionaryError?: (event: {
22
+ dictionary: string;
23
+ message: string;
24
+ }) => void;
21
25
  onLayoutComplete?: (event: {
22
26
  reason: LayoutRunReason;
23
27
  }) => void;
@@ -51,9 +55,11 @@ declare function recordMeasureBlockError(blockIndex: number, block: FlowBlock, e
51
55
  declare function recordLayoutComplete(reason?: LayoutRunReason): void;
52
56
  declare function recordLayoutStart(reason: LayoutRunReason): void;
53
57
  declare function recordLayoutError(reason: LayoutRunReason, error: unknown): void;
58
+ /** A dictionary failed to load; layout continues without hyphenating that language. */
59
+ declare function recordHyphenationDictionaryError(dictionary: string, error: Error): void;
54
60
  declare function recordLayoutPhase(reason: LayoutRunReason, phase: LayoutPhase, durationMs: number): void;
55
61
  declare function recordHiddenEditorStateCreate(reason: HiddenEditorStateReason): void;
56
62
  declare function recordHiddenEditorPhase(reason: HiddenEditorStateReason, phase: HiddenEditorPhase, durationMs: number): void;
57
63
  declare function recordDocumentLoadPhase(phase: DocumentLoadPhase, durationMs: number): void;
58
64
  //#endregion
59
- export { DocumentLoadPhase, HiddenEditorPhase, HiddenEditorStateReason, LayoutInstrumentation, LayoutPhase, LayoutRunReason, recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError };
65
+ export { DocumentLoadPhase, HiddenEditorPhase, HiddenEditorStateReason, LayoutInstrumentation, LayoutPhase, LayoutRunReason, recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordHyphenationDictionaryError, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError };
@@ -25,6 +25,13 @@ function recordLayoutError(reason, error) {
25
25
  reason
26
26
  });
27
27
  }
28
+ /** A dictionary failed to load; layout continues without hyphenating that language. */
29
+ function recordHyphenationDictionaryError(dictionary, error) {
30
+ globalThis.__folioLayoutInstrumentation?.onHyphenationDictionaryError?.({
31
+ dictionary,
32
+ message: error.message
33
+ });
34
+ }
28
35
  function recordLayoutPhase(reason, phase, durationMs) {
29
36
  globalThis.__folioLayoutInstrumentation?.onLayoutPhase?.({
30
37
  durationMs,
@@ -49,4 +56,4 @@ function recordDocumentLoadPhase(phase, durationMs) {
49
56
  });
50
57
  }
51
58
  //#endregion
52
- export { recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError };
59
+ export { recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordHyphenationDictionaryError, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError };
@@ -1,5 +1,5 @@
1
1
  import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
2
- import { SCRIPT_CLASS, hasComplexScript, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
2
+ import { SCRIPT_CLASS, hasComplexScript, hasEastAsiaSlotText, scriptClassOf, segmentByScript } from "../../utils/scriptSegments.js";
3
3
  import { applyComplexScriptFormatting, hasComplexScriptFormatting } from "./complexScriptFormatting.js";
4
4
  //#region src/layout-engine/measure/advanceComposition.ts
5
5
  /**
@@ -48,10 +48,7 @@ const glyphAdvanceStyle = (style) => {
48
48
  if (style.kerning !== void 0) result.kerning = style.kerning;
49
49
  return result;
50
50
  };
51
- const needsPerScriptFonts = (style, text) => style.eastAsiaFontFamily !== void 0 && [...text].some((c) => isEastAsia(c)) || hasComplexScriptFormatting(style) && hasComplexScript(text);
52
- const isEastAsia = (char) => {
53
- return scriptClassOf(char.codePointAt(0)) === SCRIPT_CLASS.eastAsia;
54
- };
51
+ const needsPerScriptFonts = (style, text) => style.eastAsiaFontFamily !== void 0 && hasEastAsiaSlotText(text, style.eastAsiaHint) || hasComplexScriptFormatting(style) && hasComplexScript(text);
55
52
  /**
56
53
  * Width of a string: the advance width used for line breaking, never the
57
54
  * painted (ink) extent, which includes overhang a break decision must ignore.
@@ -62,7 +59,7 @@ const composeTextWidth = ({ text, style: source, advanceOf }) => {
62
59
  const transformed = applyMeasurementTextTransform(text, style);
63
60
  const letterSpacing = style.letterSpacing ?? 0;
64
61
  const horizontalScale = getHorizontalScaleFactor(style.horizontalScale);
65
- const glyphWidth = !source.forceComplexScript && !letterSpacing && needsPerScriptFonts(style, transformed) ? segmentByScript(transformed).reduce((sum, segment) => sum + advanceOf(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script))), 0) : advanceOf(transformed, style);
62
+ const glyphWidth = !source.forceComplexScript && !letterSpacing && needsPerScriptFonts(style, transformed) ? segmentByScript(transformed, style.eastAsiaHint).reduce((sum, segment) => sum + advanceOf(segment.text, glyphAdvanceStyle(scriptStyle(style, segment.script))), 0) : advanceOf(transformed, style);
66
63
  const codePoints = countCodePoints(transformed);
67
64
  return (letterSpacing && codePoints > 1 ? glyphWidth + letterSpacing * (codePoints - 1) : glyphWidth) * horizontalScale;
68
65
  };
@@ -97,7 +94,7 @@ const composeRunMeasurement = ({ text, style: source, metrics, advanceOf, advanc
97
94
  let codePointIndex = 0;
98
95
  for (const char of text) {
99
96
  const cp = char.codePointAt(0);
100
- const charStyle = perScript ? scriptStyle(style, scriptClassOf(cp)) : style;
97
+ const charStyle = perScript ? scriptStyle(style, scriptClassOf(cp, style.eastAsiaHint)) : style;
101
98
  let charWidth = shaped.get(codePointIndex) ?? advanceOf(applyMeasurementTextTransform(char, style), charStyle);
102
99
  if (letterSpacing && offset + char.length < text.length) charWidth += letterSpacing;
103
100
  charWidth *= scale;
@@ -0,0 +1,74 @@
1
+ //#region src/layout-engine/measure/hyphenationDictionaries.d.ts
2
+ /**
3
+ * On-demand hyphenation dictionaries for automatic hyphenation.
4
+ *
5
+ * Each `hyphen` pattern set is tens to hundreds of kilobytes of source whose
6
+ * evaluation builds a pattern trie, and most documents never enable
7
+ * `w:autoHyphenation`. A dictionary is therefore imported only once measurement
8
+ * asks to hyphenate a word in its language. Measurement is synchronous, so the
9
+ * first request starts the import and hyphenates nothing. A layout run wrapped
10
+ * in {@link collectRequestedHyphenationDictionaries} learns which dictionaries
11
+ * it lacked, so its own editor can re-run layout on load or report the failure
12
+ * (`controller/hyphenationReadiness`). Hosts that must lay out correctly on the
13
+ * first pass (headless layout, tests) await `preloadHyphenationDictionaries`
14
+ * (`./hyphenationPreload`) before measuring.
15
+ *
16
+ * This module sits on every editor's import graph, so it speaks in its own
17
+ * settled-state union rather than `Result`, keeping the `Result` API out of
18
+ * adapters' type checks; the preload boundary converts.
19
+ */
20
+ type HyphenateWord = (text: string) => string;
21
+ declare const HYPHENATION_DICTIONARY_IDS: readonly ["cs", "en-gb", "en-us", "sk"];
22
+ type HyphenationDictionaryId = (typeof HYPHENATION_DICTIONARY_IDS)[number];
23
+ type HyphenationDictionaryLoaders = Readonly<Record<HyphenationDictionaryId, () => Promise<HyphenateWord>>>;
24
+ declare const HyphenationDictionaryError_base: import("better-result").TaggedErrorClass<"HyphenationDictionaryError">;
25
+ declare class HyphenationDictionaryError extends HyphenationDictionaryError_base<{
26
+ message: string;
27
+ dictionary: HyphenationDictionaryId;
28
+ cause: unknown;
29
+ }> {}
30
+ type SettledHyphenationDictionary = Readonly<{
31
+ status: "loaded";
32
+ hyphenate: HyphenateWord;
33
+ }> | Readonly<{
34
+ status: "failed";
35
+ error: HyphenationDictionaryError;
36
+ }>;
37
+ type HyphenationDictionaryState = Readonly<{
38
+ status: "unloaded";
39
+ }> | Readonly<{
40
+ status: "loading";
41
+ promise: Promise<SettledHyphenationDictionary>;
42
+ }> | SettledHyphenationDictionary;
43
+ /** Compare before and after a measurement to tell whether it lacked a dictionary. */
44
+ declare const getHyphenationDictionaryMisses: () => number;
45
+ /** The dictionary that hyphenates text in `locale`, if one is bundled. */
46
+ declare const hyphenationDictionaryFor: (locale: string | undefined) => HyphenationDictionaryId | undefined;
47
+ /** Changes whenever a dictionary finishes loading, so measurement caches cannot go stale. */
48
+ declare const getHyphenationDictionaryGeneration: () => number;
49
+ /** Load `dictionary` once; later calls share the load or its settled outcome. */
50
+ declare const requestHyphenationDictionary: (dictionary: HyphenationDictionaryId) => Promise<SettledHyphenationDictionary>;
51
+ /**
52
+ * The loaded hyphenator for `dictionary`, or `undefined` while it is not
53
+ * available. An unloaded dictionary starts loading, and a missing one is
54
+ * recorded against the enclosing {@link collectRequestedHyphenationDictionaries}
55
+ * run; the caller hyphenates nothing now.
56
+ */
57
+ declare const hyphenatorOrRequest: (dictionary: HyphenationDictionaryId) => HyphenateWord | undefined;
58
+ type CollectedHyphenationDictionaries<T> = {
59
+ result: T;
60
+ /** Dictionaries the run needed and did not have: loading or failed. */
61
+ missing: ReadonlySet<HyphenationDictionaryId>;
62
+ };
63
+ /** Run a synchronous layout pass and report the dictionaries it lacked. */
64
+ declare const collectRequestedHyphenationDictionaries: <T>(run: () => T) => CollectedHyphenationDictionaries<T>;
65
+ /**
66
+ * Test seam: forget every dictionary so a test observes the unloaded path, and
67
+ * optionally load through `loaders` (for example, ones that fail) until the
68
+ * next reset.
69
+ */
70
+ declare const resetHyphenationDictionaries: (loaders?: HyphenationDictionaryLoaders) => void;
71
+ /** Test seam: which dictionaries have been requested or loaded. */
72
+ declare const hyphenationDictionaryStatus: (dictionary: HyphenationDictionaryId) => HyphenationDictionaryState["status"];
73
+ //#endregion
74
+ export { CollectedHyphenationDictionaries, HyphenateWord, HyphenationDictionaryError, HyphenationDictionaryId, HyphenationDictionaryLoaders, SettledHyphenationDictionary, collectRequestedHyphenationDictionaries, getHyphenationDictionaryGeneration, getHyphenationDictionaryMisses, hyphenationDictionaryFor, hyphenationDictionaryStatus, hyphenatorOrRequest, requestHyphenationDictionary, resetHyphenationDictionaries };