@stll/folio-core 0.7.0 → 0.8.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 (92) hide show
  1. package/dist/ai-edits/blockRange.d.ts +13 -2
  2. package/dist/ai-edits/blockRange.js +23 -2
  3. package/dist/ai-edits/index.d.ts +4 -2
  4. package/dist/ai-edits/index.js +3 -1
  5. package/dist/ai-edits/scoped-reading.d.ts +9 -0
  6. package/dist/ai-edits/scoped-reading.js +60 -0
  7. package/dist/ai-edits/snapshot.js +13 -4
  8. package/dist/ai-edits/types.d.ts +44 -2
  9. package/dist/compat/eigenpal.d.ts +5 -2
  10. package/dist/compat/eigenpal.js +4 -1
  11. package/dist/controller/fontReadiness.d.ts +29 -0
  12. package/dist/controller/fontReadiness.js +139 -0
  13. package/dist/controller/layoutPipeline.js +76 -9
  14. package/dist/docx/blockContentParser.js +51 -8
  15. package/dist/docx/capabilities.d.ts +41 -0
  16. package/dist/docx/capabilities.js +322 -0
  17. package/dist/docx/compatibility.d.ts +34 -2
  18. package/dist/docx/compatibility.js +128 -38
  19. package/dist/docx/conformance.d.ts +6 -0
  20. package/dist/docx/conformance.js +18 -0
  21. package/dist/docx/ensureParaIds.d.ts +29 -0
  22. package/dist/docx/ensureParaIds.js +423 -0
  23. package/dist/docx/numberingParser.js +1 -0
  24. package/dist/docx/paragraphParser.js +9 -0
  25. package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
  26. package/dist/docx/parser.js +2 -0
  27. package/dist/docx/selectiveXmlPatch.d.ts +10 -1
  28. package/dist/docx/selectiveXmlPatch.js +1 -1
  29. package/dist/docx/serializer/paragraphSerializer.js +4 -0
  30. package/dist/docx/serializer/runSerializer.js +7 -2
  31. package/dist/docx/serializer/tableSerializer.js +4 -0
  32. package/dist/docx/tableParser.js +8 -0
  33. package/dist/docx/textBoxParser.js +6 -1
  34. package/dist/docx/vmlImageParser.js +145 -1
  35. package/dist/index.d.ts +5 -2
  36. package/dist/index.js +4 -1
  37. package/dist/layout-bridge/convert/headerFooterLayout.js +16 -2
  38. package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
  39. package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
  40. package/dist/layout-bridge/convert/toFlowBlocks.js +42 -3
  41. package/dist/layout-bridge/engine/hitTest.js +2 -1
  42. package/dist/layout-bridge/engine/selectionRects.js +2 -1
  43. package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
  44. package/dist/layout-engine/headerFooterRefs.js +40 -0
  45. package/dist/layout-engine/index.d.ts +3 -2
  46. package/dist/layout-engine/index.js +36 -82
  47. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  48. package/dist/layout-engine/measure/listMarkerWidth.js +23 -4
  49. package/dist/layout-engine/measure/measureBlocks.d.ts +3 -0
  50. package/dist/layout-engine/measure/measureBlocks.js +133 -51
  51. package/dist/layout-engine/measure/measureParagraph.js +13 -4
  52. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
  53. package/dist/layout-engine/measure/tableCellGrid.js +62 -0
  54. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
  55. package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
  56. package/dist/layout-engine/paginator.d.ts +2 -1
  57. package/dist/layout-engine/paginator.js +7 -6
  58. package/dist/layout-engine/paragraphFrame.d.ts +22 -0
  59. package/dist/layout-engine/paragraphFrame.js +17 -0
  60. package/dist/layout-engine/paragraphSequence.d.ts +7 -0
  61. package/dist/layout-engine/paragraphSequence.js +25 -0
  62. package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
  63. package/dist/layout-engine/paragraphSpacing.js +30 -0
  64. package/dist/layout-engine/renderedBreakReconciliation.d.ts +58 -0
  65. package/dist/layout-engine/renderedBreakReconciliation.js +63 -0
  66. package/dist/layout-engine/types.d.ts +17 -8
  67. package/dist/layout-engine/types.js +15 -2
  68. package/dist/layout-painter/renderPage.js +2 -2
  69. package/dist/layout-painter/renderParagraph.js +6 -5
  70. package/dist/layout-painter/renderTable.js +42 -12
  71. package/dist/layout-painter/renderTextBox.js +17 -7
  72. package/dist/managers/DocumentLoaderManager.js +1 -1
  73. package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
  74. package/dist/paged-layout/sectionBlockWidths.js +9 -0
  75. package/dist/paged-layout/sectionGeometry.js +1 -1
  76. package/dist/prosemirror/attrs/index.js +11 -0
  77. package/dist/prosemirror/conversion/fromProseDoc.js +2 -0
  78. package/dist/prosemirror/conversion/toProseDoc.js +27 -23
  79. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  80. package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
  81. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
  82. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  83. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  84. package/dist/prosemirror/schema/nodes.d.ts +4 -2
  85. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  86. package/dist/server.d.ts +4 -2
  87. package/dist/server.js +3 -1
  88. package/dist/types/index.d.ts +2 -1
  89. package/dist/utils/createDocument.js +26 -18
  90. package/dist/utils/hexId.d.ts +8 -1
  91. package/dist/utils/hexId.js +11 -3
  92. package/package.json +1 -1
@@ -0,0 +1,15 @@
1
+ import { TableCell, TableRow } from "../types.js";
2
+
3
+ //#region src/layout-engine/measure/tableCellGrid.d.ts
4
+ type TableCellGrid = {
5
+ occupiedColumnsByRow: ReadonlyMap<number, ReadonlySet<number>>;
6
+ sourceCellsByRow: ReadonlyMap<number, ReadonlyMap<number, TableCell>>;
7
+ sourceColumnsByCell: ReadonlyMap<TableCell, number>;
8
+ };
9
+ declare const buildTableCellGrid: (rows: readonly TableRow[], columnCount: number) => TableCellGrid;
10
+ declare const getFirstAvailableColumn: (grid: TableCellGrid, rowIndex: number, startingColumn: number) => number;
11
+ declare const getSourceCellAt: (grid: TableCellGrid, rowIndex: number, columnIndex: number) => TableCell | undefined;
12
+ declare const getSourceCellColumn: (grid: TableCellGrid, cell: TableCell) => number | undefined;
13
+ declare const getTableCellVerticalBorderHeight: (grid: TableCellGrid, cell: TableCell | undefined, rowIndex: number) => number;
14
+ //#endregion
15
+ export { TableCellGrid, buildTableCellGrid, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
@@ -0,0 +1,62 @@
1
+ //#region src/layout-engine/measure/tableCellGrid.ts
2
+ const buildTableCellGrid = (rows, columnCount) => {
3
+ const occupiedColumnsByRow = /* @__PURE__ */ new Map();
4
+ const sourceCellsByRow = /* @__PURE__ */ new Map();
5
+ const sourceColumnsByCell = /* @__PURE__ */ new Map();
6
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
7
+ const row = rows[rowIndex];
8
+ if (!row) continue;
9
+ let columnIndex = firstAvailableColumn(occupiedColumnsByRow.get(rowIndex), row.gridBefore ?? 0);
10
+ for (const cell of row.cells) {
11
+ const columnSpan = cell.colSpan ?? 1;
12
+ const rowSpan = cell.rowSpan ?? 1;
13
+ sourceColumnsByCell.set(cell, columnIndex);
14
+ const rowEnd = Math.min(rows.length, rowIndex + rowSpan);
15
+ const columnEnd = Math.min(columnCount, columnIndex + columnSpan);
16
+ for (let gridRowIndex = rowIndex; gridRowIndex < rowEnd; gridRowIndex++) {
17
+ const cellsByColumn = getOrCreateCellRow(sourceCellsByRow, gridRowIndex);
18
+ for (let gridColumnIndex = columnIndex; gridColumnIndex < columnEnd; gridColumnIndex++) cellsByColumn.set(gridColumnIndex, cell);
19
+ }
20
+ if (rowSpan > 1) for (let spannedRowIndex = rowIndex + 1; spannedRowIndex < rowIndex + rowSpan; spannedRowIndex++) {
21
+ const occupied = getOrCreateOccupiedRow(occupiedColumnsByRow, spannedRowIndex);
22
+ for (let columnOffset = 0; columnOffset < columnSpan; columnOffset++) occupied.add(columnIndex + columnOffset);
23
+ }
24
+ columnIndex = firstAvailableColumn(occupiedColumnsByRow.get(rowIndex), columnIndex + columnSpan);
25
+ }
26
+ }
27
+ return {
28
+ occupiedColumnsByRow,
29
+ sourceCellsByRow,
30
+ sourceColumnsByCell
31
+ };
32
+ };
33
+ const getFirstAvailableColumn = (grid, rowIndex, startingColumn) => firstAvailableColumn(grid.occupiedColumnsByRow.get(rowIndex), startingColumn);
34
+ const getSourceCellAt = (grid, rowIndex, columnIndex) => grid.sourceCellsByRow.get(rowIndex)?.get(columnIndex);
35
+ const getSourceCellColumn = (grid, cell) => grid.sourceColumnsByCell.get(cell);
36
+ const getTableCellVerticalBorderHeight = (grid, cell, rowIndex) => {
37
+ const sourceColumn = cell ? getSourceCellColumn(grid, cell) : void 0;
38
+ const aboveBottom = (sourceColumn === void 0 ? void 0 : getSourceCellAt(grid, rowIndex - 1, sourceColumn))?.borders?.bottom;
39
+ const aboveOwnsEdge = aboveBottom !== void 0 && aboveBottom.width !== 0 && aboveBottom.style !== "none" && aboveBottom.style !== "nil";
40
+ return (rowIndex === 0 || !aboveOwnsEdge ? cell?.borders?.top?.width ?? 0 : 0) + (cell?.borders?.bottom?.width ?? 0);
41
+ };
42
+ const firstAvailableColumn = (occupiedColumns, startingColumn) => {
43
+ let columnIndex = startingColumn;
44
+ while (occupiedColumns?.has(columnIndex)) columnIndex++;
45
+ return columnIndex;
46
+ };
47
+ const getOrCreateCellRow = (sourceCellsByRow, rowIndex) => {
48
+ const existing = sourceCellsByRow.get(rowIndex);
49
+ if (existing !== void 0) return existing;
50
+ const cellsByColumn = /* @__PURE__ */ new Map();
51
+ sourceCellsByRow.set(rowIndex, cellsByColumn);
52
+ return cellsByColumn;
53
+ };
54
+ const getOrCreateOccupiedRow = (occupiedColumnsByRow, rowIndex) => {
55
+ const existing = occupiedColumnsByRow.get(rowIndex);
56
+ if (existing !== void 0) return existing;
57
+ const occupiedColumns = /* @__PURE__ */ new Set();
58
+ occupiedColumnsByRow.set(rowIndex, occupiedColumns);
59
+ return occupiedColumns;
60
+ };
61
+ //#endregion
62
+ export { buildTableCellGrid, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
@@ -0,0 +1,18 @@
1
+ import { ParagraphBlock, ParagraphMeasure } from "../types.js";
2
+
3
+ //#region src/layout-engine/measure/textBoxParagraphLayout.d.ts
4
+ type TextBoxParagraphPlacement = {
5
+ leadingSpacing: number;
6
+ contentHeight: number;
7
+ };
8
+ type TextBoxParagraphLayout = {
9
+ placements: TextBoxParagraphPlacement[];
10
+ totalHeight: number;
11
+ };
12
+ /**
13
+ * Lay out a text box's paragraph story as one unpaginated flow. Adjacent
14
+ * before/after spacing collapses to the larger value, matching body flow.
15
+ */
16
+ declare function layoutTextBoxParagraphs(blocks: readonly ParagraphBlock[], measures: readonly ParagraphMeasure[]): TextBoxParagraphLayout;
17
+ //#endregion
18
+ export { TextBoxParagraphLayout, TextBoxParagraphPlacement, layoutTextBoxParagraphs };
@@ -0,0 +1,32 @@
1
+ import { measuredLineRangeHeight } from "../lineFlow.js";
2
+ import { getParagraphSpacingAfter, getParagraphSpacingBefore } from "../paragraphSpacing.js";
3
+ import { panic } from "better-result";
4
+ //#region src/layout-engine/measure/textBoxParagraphLayout.ts
5
+ /**
6
+ * Lay out a text box's paragraph story as one unpaginated flow. Adjacent
7
+ * before/after spacing collapses to the larger value, matching body flow.
8
+ */
9
+ function layoutTextBoxParagraphs(blocks, measures) {
10
+ if (blocks.length !== measures.length) panic("layoutTextBoxParagraphs: block and measure counts must match");
11
+ const placements = [];
12
+ let totalHeight = 0;
13
+ let trailingSpacing = 0;
14
+ for (let index = 0; index < blocks.length; index += 1) {
15
+ const block = blocks[index];
16
+ const measure = measures[index];
17
+ const leadingSpacing = Math.max(getParagraphSpacingBefore(block), trailingSpacing);
18
+ const contentHeight = measuredLineRangeHeight(measure.lines, 0, measure.lines.length);
19
+ placements.push({
20
+ leadingSpacing,
21
+ contentHeight
22
+ });
23
+ totalHeight += leadingSpacing + contentHeight;
24
+ trailingSpacing = getParagraphSpacingAfter(block);
25
+ }
26
+ return {
27
+ placements,
28
+ totalHeight: totalHeight + trailingSpacing
29
+ };
30
+ }
31
+ //#endregion
32
+ export { layoutTextBoxParagraphs };
@@ -35,7 +35,8 @@ type PaginatorOptions = {
35
35
  * its title page (typically extended top margin to clear an overflowing
36
36
  * first-page header) vs. its body pages. Pages 2+ use `margins`.
37
37
  */
38
- firstPageMargins?: PageMargins; /** Column configuration (optional). */
38
+ firstPageMargins?: PageMargins; /** Per-section body margins used on even section pages. */
39
+ sectionEvenPageMargins?: (PageMargins | undefined)[]; /** Column configuration (optional). */
39
40
  columns?: ColumnLayout; /** Per-page footnote reserved heights (pageNumber → height in pixels). */
40
41
  footnoteReservedHeights?: Map<number, number>; /** Header/footer refs by section index. */
41
42
  sectionHeaderFooterRefs?: PageHeaderFooterRefs[]; /** Callback when a new page is created. */
@@ -47,7 +47,7 @@ function createPaginator(options) {
47
47
  }
48
48
  function currentPageUsesActiveLayout(state) {
49
49
  if (pendingPageSize || pendingMargins) return false;
50
- return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, getPageMargins(state.page.number));
50
+ return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, getPageMargins(state.page.number, state.page.sectionPageNumber ?? currentSectionPageNumber));
51
51
  }
52
52
  if (getContentHeight() <= 0) panic("Paginator: page size and margins yield no content area");
53
53
  let columnWidths = calculateColumnWidths(pageSize.w, margins.left, margins.right, columns);
@@ -62,8 +62,9 @@ function createPaginator(options) {
62
62
  if (getContentHeight() <= 0) panic("Paginator: section page size and margins yield no content area");
63
63
  recalculateColumnWidths();
64
64
  }
65
- function getPageMargins(pageNumber) {
66
- const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins };
65
+ function getPageMargins(pageNumber, sectionPageNumber) {
66
+ const evenMargins = sectionPageNumber % 2 === 0 ? options.sectionEvenPageMargins?.[currentSectionIndex] : void 0;
67
+ const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...evenMargins ?? margins };
67
68
  if (options.mirrorMargins === true && pageNumber % 2 === 0) return {
68
69
  ...pageMargins,
69
70
  left: pageMargins.right,
@@ -77,7 +78,7 @@ function createPaginator(options) {
77
78
  * Get X position for a given column index.
78
79
  */
79
80
  function getColumnX(columnIndex) {
80
- let x = states.at(-1)?.page.margins.left ?? getPageMargins(1).left;
81
+ let x = states.at(-1)?.page.margins.left ?? getPageMargins(1, 1).left;
81
82
  for (let index = 0; index < columnIndex; index++) x += (columnWidths[index] ?? columnWidths[0] ?? 0) + gapAfterColumn(columns, index);
82
83
  return x;
83
84
  }
@@ -88,7 +89,7 @@ function createPaginator(options) {
88
89
  if (pendingPageSize || pendingMargins) applyPendingLayout();
89
90
  const pageNumber = pages.length + 1;
90
91
  currentSectionPageNumber += 1;
91
- const pageMargins = getPageMargins(pageNumber);
92
+ const pageMargins = getPageMargins(pageNumber, currentSectionPageNumber);
92
93
  const topMargin = pageMargins.top;
93
94
  const contentBottom = pageSize.h - pageMargins.bottom;
94
95
  const footnoteHeight = options.footnoteReservedHeights?.get(pageNumber) ?? 0;
@@ -244,7 +245,7 @@ function createPaginator(options) {
244
245
  const current = states.at(-1);
245
246
  if (!current || current.page.fragments.length > 0 || current.cursorY !== current.topMargin) return false;
246
247
  if (pendingPageSize || pendingMargins) applyPendingLayout();
247
- const pageMargins = getPageMargins(current.page.number);
248
+ const pageMargins = getPageMargins(current.page.number, 1);
248
249
  const topMargin = pageMargins.top;
249
250
  const rawContentBottom = pageSize.h - pageMargins.bottom;
250
251
  const footnoteHeight = options.footnoteReservedHeights?.get(current.page.number) ?? 0;
@@ -0,0 +1,22 @@
1
+ import { ParagraphBlock, TextBoxBlock } from "./types.js";
2
+
3
+ //#region src/layout-engine/paragraphFrame.d.ts
4
+ type ParagraphFrame = {
5
+ width?: number;
6
+ height?: number;
7
+ hSpace?: number;
8
+ vSpace?: number;
9
+ hAnchor?: "text" | "margin" | "page";
10
+ vAnchor?: "text" | "margin" | "page";
11
+ x?: number;
12
+ y?: number;
13
+ xAlign?: "left" | "center" | "right" | "inside" | "outside";
14
+ yAlign?: "top" | "center" | "bottom" | "inside" | "outside" | "inline";
15
+ wrap?: "around" | "auto" | "none" | "notBeside" | "through" | "tight";
16
+ };
17
+ declare function setParagraphFrame(block: ParagraphBlock, frame: ParagraphFrame): void;
18
+ declare function getParagraphFrame(block: ParagraphBlock): ParagraphFrame | undefined;
19
+ declare function markParagraphFrameTextBox(block: TextBoxBlock): void;
20
+ declare function isParagraphFrameTextBox(block: TextBoxBlock): boolean;
21
+ //#endregion
22
+ export { ParagraphFrame, getParagraphFrame, isParagraphFrameTextBox, markParagraphFrameTextBox, setParagraphFrame };
@@ -0,0 +1,17 @@
1
+ //#region src/layout-engine/paragraphFrame.ts
2
+ const paragraphFrames = /* @__PURE__ */ new WeakMap();
3
+ const paragraphFrameTextBoxes = /* @__PURE__ */ new WeakSet();
4
+ function setParagraphFrame(block, frame) {
5
+ paragraphFrames.set(block, frame);
6
+ }
7
+ function getParagraphFrame(block) {
8
+ return paragraphFrames.get(block);
9
+ }
10
+ function markParagraphFrameTextBox(block) {
11
+ paragraphFrameTextBoxes.add(block);
12
+ }
13
+ function isParagraphFrameTextBox(block) {
14
+ return paragraphFrameTextBoxes.has(block);
15
+ }
16
+ //#endregion
17
+ export { getParagraphFrame, isParagraphFrameTextBox, markParagraphFrameTextBox, setParagraphFrame };
@@ -0,0 +1,7 @@
1
+ import { FlowBlock } from "./types.js";
2
+
3
+ //#region src/layout-engine/paragraphSequence.d.ts
4
+ declare const continuesNumberedSequence: (previous: FlowBlock | undefined, current: FlowBlock) => boolean;
5
+ declare const continuesTabbedParagraphSequence: (previous: FlowBlock | undefined, current: FlowBlock) => boolean;
6
+ //#endregion
7
+ export { continuesNumberedSequence, continuesTabbedParagraphSequence };
@@ -0,0 +1,25 @@
1
+ //#region src/layout-engine/paragraphSequence.ts
2
+ const continuesNumberedSequence = (previous, current) => {
3
+ if (previous?.kind !== "paragraph" || current.kind !== "paragraph") return false;
4
+ const previousNumPr = previous.attrs?.numPr;
5
+ const currentNumPr = current.attrs?.numPr;
6
+ if (previousNumPr?.numId === void 0 || currentNumPr?.numId === void 0) return false;
7
+ return previousNumPr.numId === currentNumPr.numId && previousNumPr.ilvl === currentNumPr.ilvl;
8
+ };
9
+ const continuesTabbedParagraphSequence = (previous, current) => {
10
+ if (previous?.kind !== "paragraph" || current.kind !== "paragraph") return false;
11
+ if (!previous.attrs?.styleId || previous.attrs.styleId !== current.attrs?.styleId) return false;
12
+ if (!previous.runs.some((run) => run.kind === "tab") || !current.runs.some((run) => run.kind === "tab")) return false;
13
+ const previousIndent = previous.attrs.indent;
14
+ const currentIndent = current.attrs.indent;
15
+ if (previousIndent?.left !== currentIndent?.left || previousIndent?.right !== currentIndent?.right || previousIndent?.firstLine !== currentIndent?.firstLine || previousIndent?.hanging !== currentIndent?.hanging) return false;
16
+ const previousTabs = previous.attrs.tabs ?? [];
17
+ const currentTabs = current.attrs.tabs ?? [];
18
+ return previousTabs.length === currentTabs.length && previousTabs.every((tab, index) => {
19
+ const currentTab = currentTabs[index];
20
+ if (!currentTab) return false;
21
+ return tab.val === currentTab.val && tab.pos === currentTab.pos && tab.leader === currentTab.leader;
22
+ });
23
+ };
24
+ //#endregion
25
+ export { continuesNumberedSequence, continuesTabbedParagraphSequence };
@@ -0,0 +1,17 @@
1
+ import { ParagraphBlock } from "./types.js";
2
+
3
+ //#region src/layout-engine/paragraphSpacing.d.ts
4
+ /** Whether a paragraph has no visible run content. */
5
+ declare function isEmptyParagraph(block: ParagraphBlock): boolean;
6
+ /**
7
+ * Resolve leading paragraph spacing after applying the empty-paragraph
8
+ * inherited-spacing collapse rule.
9
+ */
10
+ declare function getParagraphSpacingBefore(block: ParagraphBlock): number;
11
+ /**
12
+ * Resolve trailing paragraph spacing after applying the empty-paragraph
13
+ * inherited-spacing collapse rule.
14
+ */
15
+ declare function getParagraphSpacingAfter(block: ParagraphBlock): number;
16
+ //#endregion
17
+ export { getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph };
@@ -0,0 +1,30 @@
1
+ //#region src/layout-engine/paragraphSpacing.ts
2
+ /** Whether a paragraph has no visible run content. */
3
+ function isEmptyParagraph(block) {
4
+ if (block.runs.length === 0) return true;
5
+ if (block.runs.length !== 1) return false;
6
+ const run = block.runs.at(0);
7
+ return run?.kind === "text" && run.text === "";
8
+ }
9
+ /**
10
+ * Resolve leading paragraph spacing after applying the empty-paragraph
11
+ * inherited-spacing collapse rule.
12
+ */
13
+ function getParagraphSpacingBefore(block) {
14
+ const value = block.attrs?.spacing?.before ?? 0;
15
+ if (value === 0) return 0;
16
+ if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.hasDirectParagraphFormatting && !block.attrs?.hasDirectParagraphMarkFormatting && !block.attrs?.spacingExplicit?.before) return 0;
17
+ return value;
18
+ }
19
+ /**
20
+ * Resolve trailing paragraph spacing after applying the empty-paragraph
21
+ * inherited-spacing collapse rule.
22
+ */
23
+ function getParagraphSpacingAfter(block) {
24
+ const value = block.attrs?.spacing?.after ?? 0;
25
+ if (value === 0) return 0;
26
+ if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.hasDirectParagraphFormatting && !block.attrs?.hasDirectParagraphMarkFormatting && !block.attrs?.spacingExplicit?.after) return 0;
27
+ return value;
28
+ }
29
+ //#endregion
30
+ export { getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph };
@@ -0,0 +1,58 @@
1
+ import { FlowBlock, Page } from "./types.js";
2
+
3
+ //#region src/layout-engine/renderedBreakReconciliation.d.ts
4
+ type RenderedBreakState = {
5
+ type: "noPageAdvance";
6
+ } | {
7
+ type: "pageAdvance";
8
+ reason: "ordinary" | "reflowBoundary";
9
+ };
10
+ declare const INITIAL_RENDERED_BREAK_STATE: RenderedBreakState;
11
+ type ReconcileBeforeBlockOptions = {
12
+ state: RenderedBreakState;
13
+ block: FlowBlock;
14
+ previousBlock: FlowBlock | undefined;
15
+ page: Page;
16
+ blocksById: ReadonlyMap<string, FlowBlock>;
17
+ hasExplicitPageBreak: boolean;
18
+ renderedBreakNeedsSnap: boolean;
19
+ };
20
+ type BreakDecision = {
21
+ forcePageBreak: boolean;
22
+ state: RenderedBreakState;
23
+ };
24
+ /**
25
+ * Reconcile an authored or cached break before laying out one block.
26
+ *
27
+ * Cached markers describe a boundary at a paragraph, not an ordinal page.
28
+ * Natural reflow may therefore satisfy the marker before layout reaches it.
29
+ * Every marker consumes the accumulated advance state so later markers make
30
+ * independent decisions.
31
+ */
32
+ declare const reconcileBreakBeforeBlock: ({
33
+ state,
34
+ block,
35
+ previousBlock,
36
+ page,
37
+ blocksById,
38
+ hasExplicitPageBreak,
39
+ renderedBreakNeedsSnap
40
+ }: ReconcileBeforeBlockOptions) => BreakDecision;
41
+ declare const recordReflowBoundary: (state: RenderedBreakState, crossedPageBoundary: boolean) => RenderedBreakState;
42
+ type ReconcileAfterBlockOptions = {
43
+ state: RenderedBreakState;
44
+ block: FlowBlock;
45
+ pageNumberBefore: number;
46
+ pageNumberAfter: number;
47
+ previousPage: Page | undefined;
48
+ };
49
+ /** Record page movement caused by laying out a block, or reset at a structural break. */
50
+ declare const reconcileAfterBlock: ({
51
+ state,
52
+ block,
53
+ pageNumberBefore,
54
+ pageNumberAfter,
55
+ previousPage
56
+ }: ReconcileAfterBlockOptions) => RenderedBreakState;
57
+ //#endregion
58
+ export { INITIAL_RENDERED_BREAK_STATE, RenderedBreakState, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
@@ -0,0 +1,63 @@
1
+ import { isEmptyParagraph } from "./paragraphSpacing.js";
2
+ import { continuesNumberedSequence, continuesTabbedParagraphSequence } from "./paragraphSequence.js";
3
+ //#region src/layout-engine/renderedBreakReconciliation.ts
4
+ const INITIAL_RENDERED_BREAK_STATE = { type: "noPageAdvance" };
5
+ /**
6
+ * Reconcile an authored or cached break before laying out one block.
7
+ *
8
+ * Cached markers describe a boundary at a paragraph, not an ordinal page.
9
+ * Natural reflow may therefore satisfy the marker before layout reaches it.
10
+ * Every marker consumes the accumulated advance state so later markers make
11
+ * independent decisions.
12
+ */
13
+ const reconcileBreakBeforeBlock = ({ state, block, previousBlock, page, blocksById, hasExplicitPageBreak, renderedBreakNeedsSnap }) => {
14
+ if (hasExplicitPageBreak) return {
15
+ forcePageBreak: true,
16
+ state: INITIAL_RENDERED_BREAK_STATE
17
+ };
18
+ if (block.kind !== "paragraph" || block.attrs?.renderedPageBreakBefore !== true) return {
19
+ forcePageBreak: false,
20
+ state
21
+ };
22
+ const markerAlreadySatisfied = pageStartsWithPreviousParagraphContinuation(page, previousBlock) || state.type === "pageAdvance" && state.reason === "reflowBoundary" || state.type === "pageAdvance" && isPaginationEmptyParagraph(block) || state.type === "pageAdvance" && (continuesNumberedSequence(previousBlock, block) || continuesTabbedParagraphSequence(previousBlock, block));
23
+ return {
24
+ forcePageBreak: renderedBreakNeedsSnap && !markerAlreadySatisfied && pageHasVisibleBodyContent(page, blocksById),
25
+ state: INITIAL_RENDERED_BREAK_STATE
26
+ };
27
+ };
28
+ const recordReflowBoundary = (state, crossedPageBoundary) => crossedPageBoundary ? {
29
+ type: "pageAdvance",
30
+ reason: "reflowBoundary"
31
+ } : state;
32
+ /** Record page movement caused by laying out a block, or reset at a structural break. */
33
+ const reconcileAfterBlock = ({ state, block, pageNumberBefore, pageNumberAfter, previousPage }) => {
34
+ if (isStructuralBreak(block)) return INITIAL_RENDERED_BREAK_STATE;
35
+ if (!isVisibleBodyBlock(block) || pageNumberAfter <= pageNumberBefore) return state;
36
+ return {
37
+ type: "pageAdvance",
38
+ reason: previousPage?.fragments.some(({ blockId }) => blockId === block.id) ? "reflowBoundary" : "ordinary"
39
+ };
40
+ };
41
+ const isPaginationEmptyParagraph = (block) => block.runs.every((run) => {
42
+ if (run.kind === "text") return run.text.trim().length === 0;
43
+ return run.kind === "tab" || run.kind === "lineBreak";
44
+ });
45
+ const pageHasVisibleBodyContent = (page, blocksById) => {
46
+ for (const fragment of page.fragments) {
47
+ if (fragment.kind !== "paragraph") return true;
48
+ const block = blocksById.get(String(fragment.blockId));
49
+ if (block?.kind !== "paragraph" || !isPaginationEmptyParagraph(block)) return true;
50
+ }
51
+ return false;
52
+ };
53
+ const pageStartsWithPreviousParagraphContinuation = (page, previousBlock) => {
54
+ if (previousBlock?.kind !== "paragraph") return false;
55
+ return page.fragments.some((fragment) => fragment.kind === "paragraph" && String(fragment.blockId) === String(previousBlock.id) && fragment.continuesFromPrev === true);
56
+ };
57
+ const isStructuralBreak = (block) => block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak";
58
+ const isVisibleBodyBlock = (block) => {
59
+ if (block.kind === "paragraph") return !isEmptyParagraph(block);
60
+ return !isStructuralBreak(block);
61
+ };
62
+ //#endregion
63
+ export { INITIAL_RENDERED_BREAK_STATE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary };
@@ -1,9 +1,9 @@
1
1
  import { OutlineStyleAttr } from "../types/documentEnumValues.js";
2
- import { ImagePosition, ImageWrap, SdtProperties, SdtType, TableWidthType } from "@stll/docx-core/model";
2
+ import { ImagePosition, ImageWrap, SdtProperties, SdtType, ShapeTextBody, TableWidthType } from "@stll/docx-core/model";
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, tableColumnsArePinned };
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, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned };
7
7
  }
8
8
  /**
9
9
  * Unique identifier for a block in the document.
@@ -310,7 +310,8 @@ type ParagraphAttrs = {
310
310
  * source `w:pPr`. Word treats such a blank as authored and keeps its
311
311
  * inherited spacing; a bare empty paragraph still collapses that spacing.
312
312
  */
313
- hasDirectParagraphFormatting?: boolean;
313
+ hasDirectParagraphFormatting?: boolean; /** Whether an empty paragraph carries direct formatting on its paragraph mark. */
314
+ hasDirectParagraphMarkFormatting?: boolean;
314
315
  indent?: ParagraphIndent;
315
316
  keepNext?: boolean;
316
317
  keepLines?: boolean;
@@ -337,7 +338,8 @@ type ParagraphAttrs = {
337
338
  listIsBullet?: boolean;
338
339
  listMarkerHidden?: boolean;
339
340
  listMarkerFontFamily?: string;
340
- listMarkerFontSize?: number;
341
+ listMarkerFontSize?: number; /** Horizontal alignment of the marker around the paragraph's list anchor. */
342
+ listMarkerAlignment?: "left" | "center" | "right";
341
343
  /**
342
344
  * `w:suff` (§17.9.25) — what follows the marker before body text.
343
345
  * `tab` (default) grows the marker to the next tab stop; `space` adds
@@ -443,6 +445,8 @@ type TableCell = {
443
445
  type TableRow = {
444
446
  id: BlockId;
445
447
  cells: TableCell[];
448
+ gridBefore?: number;
449
+ gridAfter?: number;
446
450
  height?: number;
447
451
  heightRule?: "auto" | "atLeast" | "exact";
448
452
  isHeader?: boolean;
@@ -582,7 +586,8 @@ type TextBoxBlock = {
582
586
  kind: "textBox";
583
587
  id: BlockId; /** Width in pixels */
584
588
  width: number; /** Height in pixels (may be auto-calculated) */
585
- height?: number; /** Fill/background color */
589
+ height?: number; /** Text fitting behavior */
590
+ autoFit?: ShapeTextBody["autoFit"]; /** Fill/background color */
586
591
  fillColor?: string; /** Border width in pixels */
587
592
  outlineWidth?: number; /** Border color */
588
593
  outlineColor?: string; /** Outline dash style, or `"none"` for an explicit no-outline. */
@@ -913,7 +918,8 @@ type LayoutOptions = {
913
918
  * overflowing first-page header without forcing pages 2+ to inherit the
914
919
  * same extension.
915
920
  */
916
- firstPageMargins?: PageMargins; /** Body-level final section page size. */
921
+ firstPageMargins?: PageMargins; /** Per-section body margins used on even section pages. */
922
+ sectionEvenPageMargins?: (PageMargins | undefined)[]; /** Body-level final section page size. */
917
923
  finalPageSize?: {
918
924
  w: number;
919
925
  h: number;
@@ -1016,7 +1022,8 @@ type HeaderFooterContent = {
1016
1022
  /**
1017
1023
  * `true` when the image run is anchored at page-level coordinates instead of
1018
1024
  * participating in inline flow. Covers the OOXML floating wrap types
1019
- * (`square`, `tight`, `through`, `behind`, `inFront`) and the legacy
1025
+ * (`square`, `tight`, `through`, `behind`, `inFront`), explicitly positioned
1026
+ * `topAndBottom` artwork, and the legacy
1020
1027
  * `displayMode === "float"` escape hatch used by ProseMirror nodes that
1021
1028
  * pre-date the wrap-type round-trip.
1022
1029
  *
@@ -1067,5 +1074,7 @@ declare function floatingTextBoxReservesBand(block: TextBoxFlowAttrs): boolean;
1067
1074
  * and the painter both consult this so their line count and `white-space` agree.
1068
1075
  */
1069
1076
  declare function tableColumnsArePinned(table: TableBlock): boolean;
1077
+ /** Return the leading visual offset for a row with omitted grid columns. */
1078
+ declare const getTableRowLeadingWidth: (row: TableRow, columnWidths: readonly number[]) => number;
1070
1079
  //#endregion
1071
- 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 };
1080
+ 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, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_d_exports };
@@ -9,6 +9,7 @@ var types_exports = /* @__PURE__ */ __exportAll({
9
9
  FOOTNOTE_SEPARATOR_HEIGHT: () => 12,
10
10
  floatingTextBoxReservesBand: () => floatingTextBoxReservesBand,
11
11
  floatingTextBoxWrapsText: () => floatingTextBoxWrapsText,
12
+ getTableRowLeadingWidth: () => getTableRowLeadingWidth,
12
13
  isFloatingImageRun: () => isFloatingImageRun,
13
14
  isFloatingTextBoxBlock: () => isFloatingTextBoxBlock,
14
15
  isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun,
@@ -46,7 +47,8 @@ const FOOTNOTE_FALLBACK_LINE_HEIGHT = 13;
46
47
  /**
47
48
  * `true` when the image run is anchored at page-level coordinates instead of
48
49
  * participating in inline flow. Covers the OOXML floating wrap types
49
- * (`square`, `tight`, `through`, `behind`, `inFront`) and the legacy
50
+ * (`square`, `tight`, `through`, `behind`, `inFront`), explicitly positioned
51
+ * `topAndBottom` artwork, and the legacy
50
52
  * `displayMode === "float"` escape hatch used by ProseMirror nodes that
51
53
  * pre-date the wrap-type round-trip.
52
54
  *
@@ -55,6 +57,7 @@ const FOOTNOTE_FALLBACK_LINE_HEIGHT = 13;
55
57
  */
56
58
  const isFloatingImageRun = (run) => {
57
59
  const wrapType = run.wrapType;
60
+ if (wrapType === "topAndBottom" && run.position !== void 0) return true;
58
61
  if (wrapType === "square" || wrapType === "tight" || wrapType === "through" || wrapType === "behind" || wrapType === "inFront") return true;
59
62
  return run.displayMode === "float";
60
63
  };
@@ -114,5 +117,15 @@ function tableColumnsArePinned(table) {
114
117
  if (table.layout === "fixed") return true;
115
118
  return table.widthType === "dxa" || table.widthType === "pct";
116
119
  }
120
+ const tableRowOwnsNestedGrid = (row) => {
121
+ const cell = row.cells.length === 1 ? row.cells.at(0) : void 0;
122
+ if (!cell?.blocks.some((block) => block.kind === "table")) return false;
123
+ return cell.blocks.every((block) => block.kind === "table" || block.kind === "paragraph" && block.runs.length === 0);
124
+ };
125
+ /** Return the leading visual offset for a row with omitted grid columns. */
126
+ const getTableRowLeadingWidth = (row, columnWidths) => {
127
+ if (tableRowOwnsNestedGrid(row)) return 0;
128
+ return columnWidths.slice(0, row.gridBefore ?? 0).reduce((sum, columnWidth) => sum + columnWidth, 0);
129
+ };
117
130
  //#endregion
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 };
131
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_exports };
@@ -1188,12 +1188,12 @@ function applySectionHeaderFooterOptions(page, pageOptions, options) {
1188
1188
  const useEven = refs.evenAndOddHeaders === true && sectionPageNumber % 2 === 0;
1189
1189
  const headerRId = (() => {
1190
1190
  if (useFirst) return refs.headerFirst;
1191
- if (useEven && refs.headerEven) return refs.headerEven;
1191
+ if (useEven) return refs.headerEven;
1192
1192
  return refs.headerDefault;
1193
1193
  })();
1194
1194
  const footerRId = (() => {
1195
1195
  if (useFirst) return refs.footerFirst;
1196
- if (useEven && refs.footerEven) return refs.footerEven;
1196
+ if (useEven) return refs.footerEven;
1197
1197
  return refs.footerDefault;
1198
1198
  })();
1199
1199
  if (headerRId) {
@@ -5,7 +5,7 @@ import { calculateTabWidth } from "../prosemirror/utils/tabCalculator.js";
5
5
  import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
6
6
  import { hasCjk, segmentByScript } from "../utils/scriptSegments.js";
7
7
  import { isFloatingImageRun } from "../layout-engine/types.js";
8
- import { getListMarkerInlineWidth } from "../layout-engine/measure/listMarkerWidth.js";
8
+ import { getListMarkerInlineWidth, getListMarkerVisualOffset } from "../layout-engine/measure/listMarkerWidth.js";
9
9
  import { applySdtDataAttrs } from "./sdtBoundary.js";
10
10
  import { applyImageVisualAttrs, hasImageVisualAttrs, wrapImageWithCrop } from "./renderImage.js";
11
11
  import { ommlToMathml } from "../docx/mathToMathml.js";
@@ -1351,7 +1351,7 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1351
1351
  }
1352
1352
  const markerFontFamily = block.attrs.listMarkerFontFamily ?? firstTextRun?.fontFamily ?? block.attrs.defaultFontFamily;
1353
1353
  const markerFontSize = block.attrs.listMarkerFontSize ?? firstTextRun?.fontSize ?? block.attrs.defaultFontSize;
1354
- const marker = renderListMarker(block.attrs.listMarker, getListMarkerInlineWidth(block), doc, markerFontFamily, markerFontSize, block.attrs.listMarkerRevision, block.attrs.listMarkerSecondSlotOffsetTwips);
1354
+ const marker = renderListMarker(block.attrs.listMarker, getListMarkerInlineWidth(block), getListMarkerVisualOffset(block), doc, markerFontFamily, markerFontSize, block.attrs.listMarkerRevision, block.attrs.listMarkerSecondSlotOffsetTwips);
1355
1355
  const markerMarginLeft = markerStart - Math.min(indentLeft, 0);
1356
1356
  if (markerMarginLeft < 0) marker.style.marginLeft = `${markerMarginLeft}px`;
1357
1357
  lineEl.prepend(marker);
@@ -1362,13 +1362,13 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1362
1362
  }
1363
1363
  /**
1364
1364
  * Render a list marker element as an inline-block at the start of the first
1365
- * body line. `minWidth` (from `getListMarkerInlineWidth`) sizes the marker
1365
+ * body line. `inlineWidth` (from `getListMarkerInlineWidth`) sizes the marker
1366
1366
  * so the body text aligns at the next tab stop per ECMA-376 §17.9.25 —
1367
1367
  * this honours `w:suff` (`tab` / `space` / `nothing`) and the document's
1368
1368
  * tab grid. Long markers like "1.1.1." therefore grow to the next stop
1369
1369
  * instead of butting against the body text.
1370
1370
  */
1371
- function renderListMarker(marker, minWidth, doc, fontFamily, fontSize, revision, secondSlotOffsetTwips) {
1371
+ function renderListMarker(marker, inlineWidth, visualOffset, doc, fontFamily, fontSize, revision, secondSlotOffsetTwips) {
1372
1372
  const span = doc.createElement("span");
1373
1373
  span.className = "layout-list-marker";
1374
1374
  span.style.display = "inline-block";
@@ -1377,7 +1377,8 @@ function renderListMarker(marker, minWidth, doc, fontFamily, fontSize, revision,
1377
1377
  span.style.textAlign = "left";
1378
1378
  span.style.textAlignLast = "left";
1379
1379
  span.style.boxSizing = "border-box";
1380
- if (minWidth > 0) span.style.minWidth = `${minWidth}px`;
1380
+ span.style.width = `${inlineWidth}px`;
1381
+ if (visualOffset !== 0) span.style.transform = `translateX(${visualOffset}px)`;
1381
1382
  if (revision) {
1382
1383
  const authorIdx = getAuthorColorIdx(revision.author ?? "");
1383
1384
  const authorColor = AUTHOR_COLORS[authorIdx];