@stll/folio-core 0.4.0 → 0.6.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 (98) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +37 -4
  6. package/dist/ai-edits/headless.js +134 -16
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -137
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +139 -0
  19. package/dist/document-operations.js +578 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.js +2 -1
  22. package/dist/docx/encryption/compoundFile.js +4 -2
  23. package/dist/docx/footnoteParser.d.ts +1 -17
  24. package/dist/docx/footnoteParser.js +1 -27
  25. package/dist/docx/groupDrawingParser.d.ts +8 -0
  26. package/dist/docx/groupDrawingParser.js +131 -0
  27. package/dist/docx/numberingParser.d.ts +3 -1
  28. package/dist/docx/numberingParser.js +8 -2
  29. package/dist/docx/paragraphParser.js +9 -3
  30. package/dist/docx/runParser.js +8 -1
  31. package/dist/docx/selectiveXmlPatch.js +4 -10
  32. package/dist/docx/serializer/numberingSerializer.js +3 -1
  33. package/dist/docx/settingsParser.d.ts +6 -3
  34. package/dist/docx/settingsParser.js +2 -0
  35. package/dist/docx/xmlParser.js +11 -4
  36. package/dist/i18n/messages/catalogs.gen.d.ts +1054 -34
  37. package/dist/i18n/messages/catalogs.gen.js +1122 -68
  38. package/dist/i18n/messages/messages.gen.d.ts +62 -2
  39. package/dist/index.d.ts +3 -2
  40. package/dist/index.js +6 -5
  41. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  42. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  43. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +5 -7
  44. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -7
  45. package/dist/layout-bridge/convert/toFlowBlocks.js +56 -19
  46. package/dist/layout-engine/index.d.ts +6 -5
  47. package/dist/layout-engine/index.js +143 -30
  48. package/dist/layout-engine/keep-together.d.ts +13 -8
  49. package/dist/layout-engine/keep-together.js +40 -25
  50. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  51. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  52. package/dist/layout-engine/measure/measureBlocks.js +50 -18
  53. package/dist/layout-engine/measure/measureParagraph.js +61 -15
  54. package/dist/layout-engine/paginator.d.ts +2 -1
  55. package/dist/layout-engine/paginator.js +9 -3
  56. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  57. package/dist/layout-engine/textBoxGroup.js +8 -0
  58. package/dist/layout-engine/types.d.ts +30 -9
  59. package/dist/layout-engine/types.js +20 -2
  60. package/dist/layout-painter/renderPage.d.ts +15 -1
  61. package/dist/layout-painter/renderPage.js +27 -11
  62. package/dist/layout-painter/renderParagraph.js +18 -8
  63. package/dist/layout-painter/renderTable.js +10 -7
  64. package/dist/managers/DocumentLoaderManager.js +12 -5
  65. package/dist/model.js +1 -1
  66. package/dist/paged-layout/pageText.d.ts +14 -0
  67. package/dist/paged-layout/pageText.js +21 -0
  68. package/dist/prosemirror/attrs/index.js +2 -0
  69. package/dist/prosemirror/commands/comments.js +75 -5
  70. package/dist/prosemirror/commands/hyperlink.js +4 -2
  71. package/dist/prosemirror/commands/image.js +2 -1
  72. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  73. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  74. package/dist/prosemirror/conversion/fromProseDoc.js +14 -3
  75. package/dist/prosemirror/conversion/toProseDoc.js +29 -8
  76. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  77. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  78. package/dist/prosemirror/extensions/nodes/TableExtension.js +10 -11
  79. package/dist/prosemirror/findReplaceSelection.js +2 -1
  80. package/dist/prosemirror/schema/nodes.d.ts +10 -5
  81. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  82. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  83. package/dist/redline.d.ts +20 -0
  84. package/dist/redline.js +127 -0
  85. package/dist/server.d.ts +7 -3
  86. package/dist/server.js +6 -2
  87. package/dist/symbols.d.ts +35 -0
  88. package/dist/symbols.js +512 -0
  89. package/dist/utils/clipboard.js +3 -3
  90. package/dist/utils/fontLoader.js +3 -0
  91. package/dist/utils/fontResolver.js +48 -1
  92. package/dist/utils/zoom.d.ts +21 -0
  93. package/dist/utils/zoom.js +21 -0
  94. package/dist/version-comparison.d.ts +92 -0
  95. package/dist/version-comparison.js +436 -0
  96. package/package.json +3 -3
  97. package/dist/paged-layout/headerFooterMargins.d.ts +0 -75
  98. package/dist/paged-layout/headerFooterMargins.js +0 -122
@@ -3,7 +3,7 @@ import { ImagePosition, ImageWrap, SdtProperties, SdtType, TableWidthType } from
3
3
 
4
4
  //#region src/layout-engine/types.d.ts
5
5
  declare namespace types_d_exports {
6
- export { 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, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun };
6
+ export { 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, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned };
7
7
  }
8
8
  /**
9
9
  * Unique identifier for a block in the document.
@@ -304,7 +304,8 @@ type ParagraphAttrs = {
304
304
  keepNext?: boolean;
305
305
  keepLines?: boolean;
306
306
  widowControl?: boolean;
307
- pageBreakBefore?: boolean;
307
+ pageBreakBefore?: boolean; /** Word's cached pagination hint (`w:lastRenderedPageBreak`). */
308
+ renderedPageBreakBefore?: boolean;
308
309
  styleId?: string;
309
310
  contextualSpacing?: boolean;
310
311
  /**
@@ -459,7 +460,14 @@ type TableBlock = {
459
460
  rows: TableRow[];
460
461
  columnWidths?: number[]; /** Table width value (twips for dxa, 50ths of percent for pct). */
461
462
  width?: number; /** Table width type ('auto', 'pct', 'dxa', 'nil'). */
462
- widthType?: TableWidthType; /** Table horizontal alignment */
463
+ widthType?: TableWidthType;
464
+ /**
465
+ * Table layout algorithm (`w:tblLayout`). "fixed" pins columns to their grid
466
+ * widths; "autofit" (the default when absent) lets Word size columns to
467
+ * content. Consulted alongside `widthType` to decide whether `w:noWrap` cells
468
+ * can be honored (see `tableColumnsArePinned`).
469
+ */
470
+ layout?: "fixed" | "autofit"; /** Table horizontal alignment */
463
471
  justification?: "left" | "center" | "right"; /** Table indent from left margin (in pixels, from w:tblInd) */
464
472
  indent?: number; /** Right-to-left column order (w:bidiVisual): logical column 0 paints on the right. */
465
473
  bidi?: boolean; /** Floating table properties (pixel values). */
@@ -903,7 +911,8 @@ type LayoutOptions = {
903
911
  headerContentHeights?: HeaderFooterContentHeights; /** Footer content heights by variant. */
904
912
  footerContentHeights?: HeaderFooterContentHeights; /** Whether section has different first page header/footer. */
905
913
  titlePage?: boolean; /** Whether section has different even/odd headers/footers. */
906
- evenAndOddHeaders?: boolean; /** Per-page footnote reserved heights (pageNumber height in pixels). */
914
+ evenAndOddHeaders?: boolean; /** Swap left/right margins on even physical pages. */
915
+ mirrorMargins?: boolean; /** Per-page footnote reserved heights (pageNumber → height in pixels). */
907
916
  footnoteReservedHeights?: Map<number, number>;
908
917
  /**
909
918
  * Footnote content heights keyed by internal footnote id (the OOXML
@@ -970,10 +979,8 @@ type HeaderFooterContent = {
970
979
  visualTop?: number; /** Bottom-most visual extent relative to the nominal flow origin. */
971
980
  visualBottom?: number;
972
981
  /**
973
- * Flow-only bounds used by `computeHeaderFooterMarginExtender` to push
974
- * body margins clear of HF overflow. Excludes `behindDoc` images that
975
- * the renderer paints behind body content; including them would reserve
976
- * a full-page letterhead as body push-down.
982
+ * Flow-only bounds for consumers that need to distinguish normal
983
+ * header/footer flow from floating or `behindDoc` artwork.
977
984
  */
978
985
  marginPushTop?: number;
979
986
  marginPushBottom?: number;
@@ -1031,5 +1038,19 @@ declare function floatingTextBoxWrapsText(block: TextBoxFlowAttrs): boolean;
1031
1038
  * Ported from eigenpal #694.
1032
1039
  */
1033
1040
  declare function floatingTextBoxReservesBand(block: TextBoxFlowAttrs): boolean;
1041
+ /**
1042
+ * A table's columns are pinned when the layout engine cannot widen a column to
1043
+ * fit content that overflows it: either the layout is explicitly fixed
1044
+ * (`w:tblLayout w:type="fixed"`) or an explicit total width (`w:tblW` `dxa`/
1045
+ * `pct`) is fully consumed by the grid.
1046
+ *
1047
+ * In OOXML, `w:noWrap` (§17.4.30) asks Word not to soft-wrap a cell, but Word
1048
+ * can only honor it by widening the column. When the columns are pinned it
1049
+ * cannot, so overflowing content wraps despite `w:noWrap`. An auto-width table
1050
+ * (`w:tblW` `auto`/`nil`/absent with autofit layout) lets Word widen the column
1051
+ * instead, so there `w:noWrap` genuinely keeps content on one line. Measurement
1052
+ * and the painter both consult this so their line count and `white-space` agree.
1053
+ */
1054
+ declare function tableColumnsArePinned(table: TableBlock): boolean;
1034
1055
  //#endregion
1035
- export { 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, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, types_d_exports };
1056
+ export { 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, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_d_exports };
@@ -11,7 +11,8 @@ var types_exports = /* @__PURE__ */ __exportAll({
11
11
  floatingTextBoxWrapsText: () => floatingTextBoxWrapsText,
12
12
  isFloatingImageRun: () => isFloatingImageRun,
13
13
  isFloatingTextBoxBlock: () => isFloatingTextBoxBlock,
14
- isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun
14
+ isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun,
15
+ tableColumnsArePinned: () => tableColumnsArePinned
15
16
  });
16
17
  /** Default internal margins for text boxes (OOXML defaults in pixels) */
17
18
  const DEFAULT_TEXTBOX_MARGINS = {
@@ -96,5 +97,22 @@ function floatingTextBoxWrapsText(block) {
96
97
  function floatingTextBoxReservesBand(block) {
97
98
  return isFloatingTextBoxBlock(block) && block.wrapType === "topAndBottom";
98
99
  }
100
+ /**
101
+ * A table's columns are pinned when the layout engine cannot widen a column to
102
+ * fit content that overflows it: either the layout is explicitly fixed
103
+ * (`w:tblLayout w:type="fixed"`) or an explicit total width (`w:tblW` `dxa`/
104
+ * `pct`) is fully consumed by the grid.
105
+ *
106
+ * In OOXML, `w:noWrap` (§17.4.30) asks Word not to soft-wrap a cell, but Word
107
+ * can only honor it by widening the column. When the columns are pinned it
108
+ * cannot, so overflowing content wraps despite `w:noWrap`. An auto-width table
109
+ * (`w:tblW` `auto`/`nil`/absent with autofit layout) lets Word widen the column
110
+ * instead, so there `w:noWrap` genuinely keeps content on one line. Measurement
111
+ * and the painter both consult this so their line count and `white-space` agree.
112
+ */
113
+ function tableColumnsArePinned(table) {
114
+ if (table.layout === "fixed") return true;
115
+ return table.widthType === "dxa" || table.widthType === "pct";
116
+ }
99
117
  //#endregion
100
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, types_exports };
118
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_exports };
@@ -42,6 +42,20 @@ type PageFloatingImage = {
42
42
  */
43
43
  behindDoc: boolean;
44
44
  };
45
+ type FloatingTableBandOptions = {
46
+ contentX: number;
47
+ tableWidth: number;
48
+ contentWidth: number;
49
+ distLeft: number;
50
+ distRight: number;
51
+ };
52
+ declare const floatingTableReservesBand: ({
53
+ contentX,
54
+ tableWidth,
55
+ contentWidth,
56
+ distLeft,
57
+ distRight
58
+ }: FloatingTableBandOptions) => boolean;
45
59
  /**
46
60
  * CSS class names for page elements
47
61
  */
@@ -276,4 +290,4 @@ declare function findPageShellForPmPos(container: HTMLElement, pmPos: number): {
276
290
  isExact: boolean;
277
291
  } | null;
278
292
  //#endregion
279
- export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
293
+ export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
@@ -19,6 +19,11 @@ import { panic } from "better-result";
19
19
  * Renders a single page from Layout data to DOM elements.
20
20
  * Each page contains positioned fragments within a content area.
21
21
  */
22
+ const floatingTableReservesBand = ({ contentX, tableWidth, contentWidth, distLeft, distRight }) => {
23
+ const leftClearance = Math.max(0, contentX - distLeft);
24
+ const rightClearance = Math.max(0, contentWidth - contentX - tableWidth - distRight);
25
+ return Math.max(leftClearance, rightClearance) < 24;
26
+ };
22
27
  /**
23
28
  * CSS class names for page elements
24
29
  */
@@ -874,7 +879,8 @@ function renderPage(page, context, options = {}) {
874
879
  ...img.wrapType !== void 0 ? { wrapType: img.wrapType } : {}
875
880
  });
876
881
  }
877
- if (options.blockLookup) for (const fragment of page.fragments) {
882
+ if (options.blockLookup) for (let fragmentIndex = 0; fragmentIndex < page.fragments.length; fragmentIndex++) {
883
+ const fragment = page.fragments[fragmentIndex];
878
884
  if (fragment.kind !== "table") continue;
879
885
  const blockData = options.blockLookup.get(String(fragment.blockId));
880
886
  if (blockData?.block.kind !== "table") continue;
@@ -886,9 +892,8 @@ function renderPage(page, context, options = {}) {
886
892
  const distBottom = floating.bottomFromText ?? 0;
887
893
  const distLeft = floating.leftFromText ?? 12;
888
894
  const distRight = floating.rightFromText ?? 12;
889
- const side = contentX < contentWidth / 2 ? "left" : "right";
890
- floatingRects.push({
891
- side,
895
+ const rect = {
896
+ side: contentX < contentWidth / 2 ? "left" : "right",
892
897
  x: contentX,
893
898
  y: contentY,
894
899
  width: fragment.width,
@@ -897,7 +902,19 @@ function renderPage(page, context, options = {}) {
897
902
  distBottom,
898
903
  distLeft,
899
904
  distRight
905
+ };
906
+ if (floatingTableReservesBand({
907
+ contentX,
908
+ tableWidth: fragment.width,
909
+ contentWidth,
910
+ distLeft,
911
+ distRight
912
+ })) rect.wrapType = "topAndBottom";
913
+ if (floating.vertAnchor === "text") scopedFloatingRects.push({
914
+ startsAtFragmentIndex: fragmentIndex,
915
+ rect
900
916
  });
917
+ else floatingRects.push(rect);
901
918
  }
902
919
  if (options.blockLookup) for (let fragmentIndex = 0; fragmentIndex < page.fragments.length; fragmentIndex++) {
903
920
  const fragment = page.fragments[fragmentIndex];
@@ -1557,22 +1574,21 @@ function renderPages(pages, container, options = {}) {
1557
1574
  const pageShells = [];
1558
1575
  for (let i = 0; i < pages.length; i++) {
1559
1576
  const page = pages[i];
1577
+ let pageEl;
1560
1578
  if (!useVirtualization) {
1561
1579
  const { context, pageOptions } = buildPageRenderArgs(page, totalPages, options);
1562
- const pageEl = renderPage(page, context, pageOptions);
1563
- container.insertBefore(pageEl, overlayBefore);
1564
- pageShells.push(pageEl);
1580
+ pageEl = renderPage(page, context, pageOptions);
1565
1581
  } else {
1566
1582
  const doc = options.document ?? document;
1567
- const pageEl = doc.createElement("div");
1583
+ pageEl = doc.createElement("div");
1568
1584
  pageEl.className = options.pageClassName ?? PAGE_CLASS_NAMES.page;
1569
1585
  pageEl.dataset["pageNumber"] = String(page.number);
1570
1586
  pageEl.dataset["pageIndex"] = String(i);
1571
1587
  applyPageStyles(pageEl, page.size.w, page.size.h, options);
1572
1588
  syncPageBorderOverlay(pageEl, page, options, doc);
1573
- container.insertBefore(pageEl, overlayBefore);
1574
- pageShells.push(pageEl);
1575
1589
  }
1590
+ container.insertBefore(pageEl, overlayBefore);
1591
+ pageShells.push(pageEl);
1576
1592
  }
1577
1593
  if (!useVirtualization) {
1578
1594
  emitPainterPainted(container);
@@ -1820,4 +1836,4 @@ function depopulatePageShell(shell, pageDataMap) {
1820
1836
  }
1821
1837
  }
1822
1838
  //#endregion
1823
- export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
1839
+ export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
@@ -330,8 +330,8 @@ function renderTabRun(run, doc, width, leader) {
330
330
  } else span.textContent = "\xA0";
331
331
  return span;
332
332
  }
333
- function canClampTabToRightEdge(alignment, hasPriorRenderedContent) {
334
- if (alignment === "start" || alignment === "default") return hasPriorRenderedContent;
333
+ function canClampTabToRightEdge(alignment, hasPriorRenderedContent, hasPriorTab, isLastLine) {
334
+ if (alignment === "start" || alignment === "default") return hasPriorRenderedContent && (hasPriorTab || isLastLine);
335
335
  return true;
336
336
  }
337
337
  function applyTabUnderline(element, run) {
@@ -905,24 +905,32 @@ function renderLine(block, line, alignment, doc, options) {
905
905
  lineEl.append(emptySpan);
906
906
  return lineEl;
907
907
  }
908
+ const hasTabRuns = runsForLine.some(isTabRun);
908
909
  if (alignment === "justify" && options) {
909
910
  if (!options.isLastLine || options.paragraphEndsWithLineBreak) {
910
- const overfullPx = line.width - options.availableWidth;
911
+ const firstLineIndentPx = options.isFirstLine ? options.firstLineIndentPx ?? 0 : 0;
912
+ const firstLineHangingPx = Math.max(0, -firstLineIndentPx);
913
+ const justifyCapacityPx = options.availableWidth + firstLineHangingPx;
914
+ const overfullPx = line.width - justifyCapacityPx;
911
915
  const shrinkableSpaces = countShrinkableSpaces(runsForLine);
912
916
  if (overfullPx > RIGHT_EDGE_EPSILON_PX && shrinkableSpaces > 0) {
913
917
  lineEl.style.textAlign = "left";
914
918
  lineEl.style.textAlignLast = "auto";
915
919
  lineEl.style.wordSpacing = `${-overfullPx / shrinkableSpaces}px`;
920
+ } else if (hasTabRuns && shrinkableSpaces > 0 && -overfullPx > RIGHT_EDGE_EPSILON_PX) {
921
+ lineEl.style.textAlign = "left";
922
+ lineEl.style.textAlignLast = "auto";
923
+ lineEl.style.wordSpacing = `${-overfullPx / shrinkableSpaces}px`;
916
924
  } else {
917
925
  lineEl.style.textAlign = "justify";
918
926
  lineEl.style.textAlignLast = "justify";
919
927
  }
920
- lineEl.style.width = `${options.availableWidth}px`;
928
+ const listFirstLineOffset = options.isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden ? options.firstLineIndentPx ?? 0 : 0;
929
+ lineEl.style.width = `${options.availableWidth - listFirstLineOffset}px`;
921
930
  }
922
931
  }
923
932
  lineEl.style.whiteSpace = "pre";
924
933
  lineEl.style.overflow = "visible";
925
- const hasTabRuns = runsForLine.some(isTabRun);
926
934
  let tabContext;
927
935
  const measureText = createTextMeasurer(doc);
928
936
  if (hasTabRuns) {
@@ -930,6 +938,7 @@ function renderLine(block, line, alignment, doc, options) {
930
938
  const leftIndentTwips = options?.leftIndentPx ? Math.round(options.leftIndentPx * 15) : 0;
931
939
  tabContext = {
932
940
  ...explicitStops !== void 0 ? { explicitStops } : {},
941
+ ...block.attrs?.defaultTabStopTwips !== void 0 ? { defaultTabInterval: block.attrs.defaultTabStopTwips } : {},
933
942
  leftIndent: leftIndentTwips
934
943
  };
935
944
  }
@@ -992,7 +1001,7 @@ function renderLine(block, line, alignment, doc, options) {
992
1001
  break;
993
1002
  }
994
1003
  let tabWidth = tabResult.width;
995
- if (lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck);
1004
+ if (lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0, runsForLine.slice(0, i).some(isTabRun), options?.isLastLine === true) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck);
996
1005
  const tabEl = renderTabRun(run, doc, tabWidth, tabResult.leader);
997
1006
  lineEl.append(tabEl);
998
1007
  currentX += tabWidth;
@@ -1240,7 +1249,7 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1240
1249
  } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1241
1250
  else if (hasFirstLine && !isFlexLine) lineEl.style.textIndent = `${indent.firstLine ?? 0}px`;
1242
1251
  } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1243
- else if (hasHanging) lineEl.style.paddingLeft = `${indent.hanging ?? 0}px`;
1252
+ else if (hasHanging && indentLeft === 0) lineEl.style.paddingLeft = `${indent.hanging ?? 0}px`;
1244
1253
  if (indentRight > 0) lineEl.style.paddingRight = `${indentRight}px`;
1245
1254
  if (isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden) {
1246
1255
  const hanging = indent?.hanging ?? 0;
@@ -1259,7 +1268,8 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1259
1268
  const markerFontFamily = block.attrs.listMarkerFontFamily ?? firstTextRun?.fontFamily ?? block.attrs.defaultFontFamily;
1260
1269
  const markerFontSize = block.attrs.listMarkerFontSize ?? firstTextRun?.fontSize ?? block.attrs.defaultFontSize;
1261
1270
  const marker = renderListMarker(block.attrs.listMarker, getListMarkerInlineWidth(block), doc, markerFontFamily, markerFontSize, block.attrs.listMarkerRevision, block.attrs.listMarkerSecondSlotOffsetTwips);
1262
- if (markerStart < 0 && indentLeft > 0) marker.style.marginLeft = `${markerStart}px`;
1271
+ const markerMarginLeft = markerStart - Math.min(indentLeft, 0);
1272
+ if (markerMarginLeft < 0 && indentLeft !== 0) marker.style.marginLeft = `${markerMarginLeft}px`;
1263
1273
  lineEl.prepend(marker);
1264
1274
  }
1265
1275
  fragmentEl.append(lineEl);
@@ -1,3 +1,4 @@
1
+ import { tableColumnsArePinned } from "../layout-engine/types.js";
1
2
  import { measureParagraph } from "../layout-engine/measure/measureParagraph.js";
2
3
  import { getAutomaticTextColorForBackground } from "./documentColors.js";
3
4
  import { renderParagraphFragment } from "./renderParagraph.js";
@@ -137,6 +138,7 @@ function renderNestedTable(block, measure, context, doc) {
137
138
  }
138
139
  rowYPositions.push(yPos);
139
140
  const spanningCells = /* @__PURE__ */ new Map();
141
+ const columnsPinned = tableColumnsArePinned(block);
140
142
  let y = 0;
141
143
  for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex++) {
142
144
  const row = block.rows[rowIndex];
@@ -146,7 +148,7 @@ function renderNestedTable(block, measure, context, doc) {
146
148
  y += rowMeasure.height;
147
149
  continue;
148
150
  }
149
- const rowEl = renderTableRow(row, rowMeasure, rowIndex, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, void 0, block.bidi);
151
+ const rowEl = renderTableRow(row, rowMeasure, rowIndex, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, void 0, block.bidi, columnsPinned);
150
152
  tableEl.append(rowEl);
151
153
  y += rowMeasure.height;
152
154
  }
@@ -169,7 +171,7 @@ function applyBorder(el, side, border) {
169
171
  /**
170
172
  * Render a single table cell
171
173
  */
172
- function renderTableCell(cell, cellMeasure, x, rowHeight, borderFlags, context, doc) {
174
+ function renderTableCell(cell, cellMeasure, x, rowHeight, borderFlags, columnsPinned, context, doc) {
173
175
  const cellEl = doc.createElement("div");
174
176
  cellEl.className = TABLE_CLASS_NAMES.cell;
175
177
  cellEl.style.position = "absolute";
@@ -195,7 +197,7 @@ function renderTableCell(cell, cellMeasure, x, rowHeight, borderFlags, context,
195
197
  const automaticTextColor = getAutomaticTextColorForBackground(cell.background);
196
198
  if (automaticTextColor) cellEl.style.color = automaticTextColor;
197
199
  }
198
- if (cell.noWrap) cellEl.style.whiteSpace = "nowrap";
200
+ if (cell.noWrap && !columnsPinned) cellEl.style.whiteSpace = "nowrap";
199
201
  if (cell.verticalAlign) {
200
202
  cellEl.style.display = "flex";
201
203
  cellEl.style.flexDirection = "column";
@@ -226,7 +228,7 @@ function renderTableCell(cell, cellMeasure, x, rowHeight, borderFlags, context,
226
228
  /**
227
229
  * Render a table row with rowSpan support
228
230
  */
229
- function renderTableRow(row, rowMeasure, rowIndex, y, columnWidths, totalRows, context, doc, spanningCells, rowYPositions, isFirstRowInFragment, bidi = false) {
231
+ function renderTableRow(row, rowMeasure, rowIndex, y, columnWidths, totalRows, context, doc, spanningCells, rowYPositions, isFirstRowInFragment, bidi = false, columnsPinned = false) {
230
232
  const rowEl = doc.createElement("div");
231
233
  rowEl.className = TABLE_CLASS_NAMES.row;
232
234
  const tableWidth = bidi ? columnWidths.reduce((sum, columnWidth) => sum + columnWidth, 0) : 0;
@@ -270,7 +272,7 @@ function renderTableRow(row, rowMeasure, rowIndex, y, columnWidths, totalRows, c
270
272
  isLastRow,
271
273
  isFirstCol: bidi ? atLogicalEnd : atLogicalStart,
272
274
  isLastCol: bidi ? atLogicalStart : atLogicalEnd
273
- }, context, doc);
275
+ }, columnsPinned, context, doc);
274
276
  cellEl.dataset["cellIndex"] = String(cellIndex);
275
277
  cellEl.dataset["columnIndex"] = String(columnIndex);
276
278
  if (rowSpan > 1) cellEl.dataset["rowSpan"] = String(rowSpan);
@@ -348,6 +350,7 @@ function renderTableFragment(fragment, block, measure, context, options = {}) {
348
350
  }
349
351
  rowYPositions.push(yPos);
350
352
  const spanningCells = /* @__PURE__ */ new Map();
353
+ const columnsPinned = tableColumnsArePinned(block);
351
354
  const headerRowCount = fragment.headerRowCount ?? 0;
352
355
  let y = 0;
353
356
  if (headerRowCount > 0 && fragment.continuesFromPrev) for (let hdrIdx = 0; hdrIdx < headerRowCount; hdrIdx++) {
@@ -358,7 +361,7 @@ function renderTableFragment(fragment, block, measure, context, options = {}) {
358
361
  y += hdrRowMeasure.height;
359
362
  continue;
360
363
  }
361
- const rowEl = renderTableRow(hdrRow, hdrRowMeasure, hdrIdx, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, hdrIdx === 0, block.bidi);
364
+ const rowEl = renderTableRow(hdrRow, hdrRowMeasure, hdrIdx, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, hdrIdx === 0, block.bidi, columnsPinned);
362
365
  rowEl.dataset["repeatedHeader"] = "true";
363
366
  tableEl.append(rowEl);
364
367
  y += hdrRowMeasure.height;
@@ -386,7 +389,7 @@ function renderTableFragment(fragment, block, measure, context, options = {}) {
386
389
  continue;
387
390
  }
388
391
  const isFirstRowInFragment = headerRowCount > 0 && fragment.continuesFromPrev ? false : fragment.continuesFromPrev && rowIndex === fragment.fromRow;
389
- const rowEl = renderTableRow(row, rowMeasure, rowIndex, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, isFirstRowInFragment, block.bidi);
392
+ const rowEl = renderTableRow(row, rowMeasure, rowIndex, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, isFirstRowInFragment, block.bidi, columnsPinned);
390
393
  contentParent.append(rowEl);
391
394
  y += rowMeasure.height;
392
395
  }
@@ -1,6 +1,7 @@
1
1
  import { resetAuthorColors } from "../utils/authorColors.js";
2
2
  import { loadFontsWithMapping } from "../utils/fontLoader.js";
3
3
  import { parseDocx } from "../docx/parser.js";
4
+ import { recordDocumentLoadPhase } from "../layout-engine/layoutInstrumentation.js";
4
5
  import { inspectDocxCompatibility } from "../docx/compatibility.js";
5
6
  //#region src/managers/DocumentLoaderManager.ts
6
7
  /**
@@ -48,11 +49,17 @@ var DocumentLoaderManager = class {
48
49
  const generation = ++this.loadGeneration;
49
50
  if (!(history.state !== null)) setDocumentLoadState({ status: "loading" });
50
51
  try {
51
- const doc = await parseDocx(buffer, {
52
- detectVariables: false,
53
- preloadFonts: false,
54
- password: options.password
55
- });
52
+ const parseStartedAt = performance.now();
53
+ let doc;
54
+ try {
55
+ doc = await parseDocx(buffer, {
56
+ detectVariables: false,
57
+ preloadFonts: false,
58
+ password: options.password
59
+ });
60
+ } finally {
61
+ recordDocumentLoadPhase("docx-parse", performance.now() - parseStartedAt);
62
+ }
56
63
  if (this.loadGeneration !== generation) return;
57
64
  this.loadParsedDocument(doc);
58
65
  } catch (error) {
package/dist/model.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BORDER_STYLE_VALUES, CONDITIONAL_STYLE_TYPE_VALUES, EMPHASIS_MARK_VALUES, FIELD_TYPE_VALUES, FLOATING_TABLE_X_SPEC_VALUES, FLOATING_TABLE_Y_SPEC_VALUES, FONT_THEME_VALUES, FRAME_WRAP_VALUES, FRAME_X_ALIGN_VALUES, FRAME_Y_ALIGN_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LEVEL_SUFFIX_VALUES, LINE_SPACING_RULE_VALUES, NUMBER_FORMAT_VALUES, OUTLINE_STYLE_ATTR_VALUES, OUTLINE_STYLE_CSS_ALIASES, OUTLINE_STYLE_CSS_ALIAS_VALUES, PARAGRAPH_ALIGNMENT_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, SHAPE_OUTLINE_STYLE_VALUES, SHAPE_TYPE_VALUES, STYLE_TYPE_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES } from "./types/documentEnumValues.js";
2
1
  import { deriveBlockId, getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
2
+ import { BORDER_STYLE_VALUES, CONDITIONAL_STYLE_TYPE_VALUES, EMPHASIS_MARK_VALUES, FIELD_TYPE_VALUES, FLOATING_TABLE_X_SPEC_VALUES, FLOATING_TABLE_Y_SPEC_VALUES, FONT_THEME_VALUES, FRAME_WRAP_VALUES, FRAME_X_ALIGN_VALUES, FRAME_Y_ALIGN_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LEVEL_SUFFIX_VALUES, LINE_SPACING_RULE_VALUES, NUMBER_FORMAT_VALUES, OUTLINE_STYLE_ATTR_VALUES, OUTLINE_STYLE_CSS_ALIASES, OUTLINE_STYLE_CSS_ALIAS_VALUES, PARAGRAPH_ALIGNMENT_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, SHAPE_OUTLINE_STYLE_VALUES, SHAPE_TYPE_VALUES, STYLE_TYPE_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES } from "./types/documentEnumValues.js";
3
3
  import { types_exports } from "./layout-engine/types.js";
4
4
  export { BORDER_STYLE_VALUES, CONDITIONAL_STYLE_TYPE_VALUES, EMPHASIS_MARK_VALUES, FIELD_TYPE_VALUES, FLOATING_TABLE_X_SPEC_VALUES, FLOATING_TABLE_Y_SPEC_VALUES, FONT_THEME_VALUES, FRAME_WRAP_VALUES, FRAME_X_ALIGN_VALUES, FRAME_Y_ALIGN_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LEVEL_SUFFIX_VALUES, LINE_SPACING_RULE_VALUES, types_exports as Layout, NUMBER_FORMAT_VALUES, OUTLINE_STYLE_ATTR_VALUES, OUTLINE_STYLE_CSS_ALIASES, OUTLINE_STYLE_CSS_ALIAS_VALUES, PARAGRAPH_ALIGNMENT_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, SHAPE_OUTLINE_STYLE_VALUES, SHAPE_TYPE_VALUES, STYLE_TYPE_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES, deriveBlockId, getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex, isFolioBlockId, isSequentialFolioBlockId };
@@ -0,0 +1,14 @@
1
+ import { Layout } from "../layout-engine/types.js";
2
+ import { Node } from "prosemirror-model";
3
+
4
+ //#region src/paged-layout/pageText.d.ts
5
+ /**
6
+ * The plain text of a single rendered page (1-based), read by joining each of
7
+ * the page's layout fragments' `[pmStart, pmEnd)` document text with a
8
+ * newline. Fragments with no `pmStart`/`pmEnd` (e.g. a page-shell placeholder)
9
+ * are skipped. Returns `null` when the layout hasn't been computed yet or the
10
+ * page number is out of range.
11
+ */
12
+ declare const getPageTextFromLayout: (layout: Layout | null, doc: Node, page: number) => string | null;
13
+ //#endregion
14
+ export { getPageTextFromLayout };
@@ -0,0 +1,21 @@
1
+ //#region src/paged-layout/pageText.ts
2
+ /**
3
+ * The plain text of a single rendered page (1-based), read by joining each of
4
+ * the page's layout fragments' `[pmStart, pmEnd)` document text with a
5
+ * newline. Fragments with no `pmStart`/`pmEnd` (e.g. a page-shell placeholder)
6
+ * are skipped. Returns `null` when the layout hasn't been computed yet or the
7
+ * page number is out of range.
8
+ */
9
+ const getPageTextFromLayout = (layout, doc, page) => {
10
+ if (!layout || !Number.isInteger(page) || page < 1) return null;
11
+ const layoutPage = layout.pages[page - 1];
12
+ if (!layoutPage) return null;
13
+ const fragmentTexts = [];
14
+ for (const fragment of layoutPage.fragments) {
15
+ if (typeof fragment.pmStart !== "number" || typeof fragment.pmEnd !== "number") continue;
16
+ fragmentTexts.push(doc.textBetween(fragment.pmStart, fragment.pmEnd, "\n"));
17
+ }
18
+ return fragmentTexts.join("\n");
19
+ };
20
+ //#endregion
21
+ export { getPageTextFromLayout };
@@ -174,6 +174,7 @@ const readParagraphAttrs = (node) => {
174
174
  });
175
175
  optionalOneOf(attrs, "sectionBreakType", "paragraph.attrs.sectionBreakType", issues, SECTION_BREAK_TYPES);
176
176
  optionalStringArray(attrs, "listLevelNumFmts", "paragraph.attrs.listLevelNumFmts", issues);
177
+ optionalNumberArray(attrs, "listLevelStarts", "paragraph.attrs.listLevelStarts", issues);
177
178
  optionalNumber(attrs, "listAbstractNumId", "paragraph.attrs.listAbstractNumId", issues);
178
179
  optionalBorderMap(attrs, "borders", "paragraph.attrs.borders", issues, [
179
180
  "top",
@@ -186,6 +187,7 @@ const readParagraphAttrs = (node) => {
186
187
  optionalShading(attrs, "shading", "paragraph.attrs.shading", issues);
187
188
  optionalTabStops(attrs, "tabs", "paragraph.attrs.tabs", issues);
188
189
  optionalRecord(attrs, "spacingExplicit", "paragraph.attrs.spacingExplicit", issues);
190
+ optionalRecord(attrs, "spacingFromDocDefaults", "paragraph.attrs.spacingFromDocDefaults", issues);
189
191
  optionalTextFormatting(attrs, "defaultTextFormatting", "paragraph.attrs.defaultTextFormatting", issues);
190
192
  optionalRecord(attrs, "numPr", "paragraph.attrs.numPr", issues);
191
193
  validateNumPr(attrs["numPr"], issues);
@@ -1,3 +1,4 @@
1
+ import { paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "./propertyChangeScope.js";
1
2
  //#region src/prosemirror/commands/comments.ts
2
3
  /**
3
4
  * Add a comment mark to the current selection.
@@ -59,21 +60,47 @@ function resolveChange(from, to, mode, revisionIds) {
59
60
  if (node.type.name === "paragraph") {
60
61
  const op = collectPPrMarkOp(node, pos, from, to, mode, revisionSet);
61
62
  if (op) pPrMarkOps.push(op);
63
+ const boundaryCovered = rangeCoversParagraphBoundary(from, to, pos, node);
64
+ let nextAttrs = null;
62
65
  const propertyChanges = node.attrs["_propertyChanges"];
63
- if (Array.isArray(propertyChanges) && propertyChanges.length > 0 && rangeCoversParagraphBoundary(from, to, pos, node)) {
66
+ if (Array.isArray(propertyChanges) && propertyChanges.length > 0 && boundaryCovered) {
64
67
  const matches = propertyChanges.filter((c) => revisionSet === null || c.info && revisionSet.has(c.info.id));
65
68
  if (matches.length > 0) {
66
69
  const remaining = propertyChanges.filter((c) => revisionSet !== null && (!c.info || !revisionSet.has(c.info.id)));
67
- const nextAttrs = {
70
+ nextAttrs = {
68
71
  ...node.attrs,
69
72
  _propertyChanges: remaining.length > 0 ? remaining : null
70
73
  };
71
- if (mode === "reject") {
72
- for (const change of matches.toReversed()) if (change.previousFormatting) for (const [key, val] of Object.entries(change.previousFormatting)) nextAttrs[key] = val;
74
+ if (mode === "reject") for (const change of matches.toReversed()) {
75
+ Object.assign(nextAttrs, paragraphRejectAttrPatch(change.previousFormatting));
76
+ nextAttrs["_originalFormatting"] = paragraphRejectOriginalFormatting(change.previousFormatting, node.attrs["_originalFormatting"]);
73
77
  }
74
- tr.setNodeMarkup(pos, void 0, nextAttrs);
75
78
  }
76
79
  }
80
+ const sectionProperties = node.attrs["_sectionProperties"];
81
+ const sectionChanges = sectionProperties?.propertyChanges;
82
+ if (sectionProperties && Array.isArray(sectionChanges) && sectionChanges.length > 0 && boundaryCovered) {
83
+ const matches = sectionChanges.filter((c) => revisionSet === null || c.info && revisionSet.has(c.info.id));
84
+ if (matches.length > 0) {
85
+ const remaining = sectionChanges.filter((c) => revisionSet !== null && (!c.info || !revisionSet.has(c.info.id)));
86
+ let restored = { ...sectionProperties };
87
+ if (mode === "reject") for (const change of matches.toReversed()) restored = sectionRejectProperties(restored, change.previousProperties);
88
+ delete restored.propertyChanges;
89
+ if (remaining.length > 0) restored.propertyChanges = remaining;
90
+ nextAttrs = nextAttrs ?? { ...node.attrs };
91
+ nextAttrs["_sectionProperties"] = restored;
92
+ nextAttrs["sectionBreakType"] = sectionBreakTypeFromSectionStart(restored.sectionStart);
93
+ }
94
+ }
95
+ if (nextAttrs) tr.setNodeMarkup(pos, void 0, nextAttrs);
96
+ return true;
97
+ }
98
+ const tableChangeAttrName = TABLE_PROPERTY_CHANGE_ATTR_BY_NODE[node.type.name];
99
+ if (tableChangeAttrName !== void 0) {
100
+ if (rangeCoversNode(from, to, pos, node)) {
101
+ const nextAttrs = resolveTablePropertyChangeAttrs(node, tableChangeAttrName, mode, revisionSet);
102
+ if (nextAttrs) tr.setNodeMarkup(pos, void 0, nextAttrs);
103
+ }
77
104
  return true;
78
105
  }
79
106
  if (!node.isInline) return true;
@@ -124,6 +151,49 @@ function rangeCoversParagraphBoundary(from, to, pos, node) {
124
151
  const boundaryTo = pos + node.nodeSize;
125
152
  return from <= boundaryFrom && to >= boundaryTo;
126
153
  }
154
+ /** Whether [from, to] fully covers the node — the range property-change cards
155
+ * and accept-all / reject-all sweeps supply for table-level records. */
156
+ function rangeCoversNode(from, to, pos, node) {
157
+ return from <= pos && to >= pos + node.nodeSize;
158
+ }
159
+ const SECTION_BREAK_TYPE_VALUES = [
160
+ "nextPage",
161
+ "continuous",
162
+ "oddPage",
163
+ "evenPage"
164
+ ];
165
+ function sectionBreakTypeFromSectionStart(sectionStart) {
166
+ return SECTION_BREAK_TYPE_VALUES.find((value) => value === sectionStart) ?? null;
167
+ }
168
+ /** PM node type name → the attr its tracked property-change records live on. */
169
+ const TABLE_PROPERTY_CHANGE_ATTR_BY_NODE = {
170
+ table: "tblPrChange",
171
+ tableRow: "trPrChange",
172
+ tableCell: "tcPrChange",
173
+ tableHeader: "tcPrChange"
174
+ };
175
+ /**
176
+ * Resolve the tracked property-change records on one table / row / cell node.
177
+ * Accept keeps the live formatting and clears the matched records; reject
178
+ * additionally restores the stored previous formatting wholesale (the change
179
+ * element stores the complete old property set — see propertyChangeScope.ts).
180
+ * Returns the next attrs, or `null` when no record matches.
181
+ */
182
+ function resolveTablePropertyChangeAttrs(node, attrName, mode, revisionSet) {
183
+ const changes = node.attrs[attrName];
184
+ if (!Array.isArray(changes) || changes.length === 0) return null;
185
+ const matches = changes.filter((c) => revisionSet === null || c.info && revisionSet.has(c.info.id));
186
+ if (matches.length === 0) return null;
187
+ const remaining = changes.filter((c) => revisionSet !== null && (!c.info || !revisionSet.has(c.info.id)));
188
+ const nextAttrs = {
189
+ ...node.attrs,
190
+ [attrName]: remaining.length > 0 ? remaining : null
191
+ };
192
+ if (mode === "reject") for (const change of matches.toReversed()) if (attrName === "tblPrChange") Object.assign(nextAttrs, tableRejectAttrPatch(change.previousFormatting));
193
+ else if (attrName === "trPrChange") Object.assign(nextAttrs, tableRowRejectAttrPatch(change.previousFormatting));
194
+ else Object.assign(nextAttrs, tableCellRejectAttrPatch(change.previousFormatting, node.attrs["_originalFormatting"]));
195
+ return nextAttrs;
196
+ }
127
197
  function isPPrMarkAttr(value) {
128
198
  if (typeof value !== "object" || value === null) return false;
129
199
  const kind = value.kind;
@@ -39,7 +39,8 @@ const editHyperlinkAtCursor = (view, { displayText, href }) => {
39
39
  const { $from } = view.state.selection;
40
40
  const linkMark = $from.marks().find((m) => m.type === hlType);
41
41
  if (!linkMark) return true;
42
- const targetRange = rangeAtCursor(collectHyperlinkRanges($from.parent, $from.start(), hlType, hyperlinkHref(linkMark)), $from.pos);
42
+ const ranges = collectHyperlinkRanges($from.parent, $from.start(), hlType, hyperlinkHref(linkMark));
43
+ const targetRange = rangeAtCursor(ranges, $from.pos);
43
44
  if (!targetRange) return false;
44
45
  const { tooltip } = expectHyperlinkMarkAttrs(linkMark);
45
46
  const newMark = hlType.create({
@@ -69,7 +70,8 @@ const removeHyperlinkAtCursor = (view, { popupHref }) => {
69
70
  }
70
71
  });
71
72
  if (!linkMark) return false;
72
- const targetRange = rangeAtCursor(collectHyperlinkRanges($from.parent, $from.start(), hlType, hyperlinkHref(linkMark)), $from.pos);
73
+ const ranges = collectHyperlinkRanges($from.parent, $from.start(), hlType, hyperlinkHref(linkMark));
74
+ const targetRange = rangeAtCursor(ranges, $from.pos);
73
75
  if (!targetRange) return false;
74
76
  const tr = view.state.tr.removeMark(targetRange.start, targetRange.end, hlType);
75
77
  view.dispatch(tr.scrollIntoView());
@@ -124,7 +124,8 @@ const applyImageWrapType = (view, pos, wrapType) => {
124
124
  const applyImageTransform = (view, pos, action) => {
125
125
  const node = imageNodeAt(view, pos);
126
126
  if (!node) return false;
127
- const transform = computeImageTransform(expectImageAttrs(node).transform ?? "", action);
127
+ const currentTransform = expectImageAttrs(node).transform ?? "";
128
+ const transform = computeImageTransform(currentTransform, action);
128
129
  const tr = view.state.tr.setNodeMarkup(pos, void 0, mergeImageAttrs(node, { transform }));
129
130
  view.dispatch(tr.scrollIntoView());
130
131
  return true;