postext 0.3.28 → 0.3.30

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 (119) hide show
  1. package/dist/__tests__/columnBalancing.test.js +78 -1
  2. package/dist/__tests__/columnBalancing.test.js.map +1 -1
  3. package/dist/__tests__/exports.test.js +2 -0
  4. package/dist/__tests__/exports.test.js.map +1 -1
  5. package/dist/__tests__/pipeline/calloutOverflow.test.d.ts +2 -0
  6. package/dist/__tests__/pipeline/calloutOverflow.test.d.ts.map +1 -0
  7. package/dist/__tests__/pipeline/calloutOverflow.test.js +189 -0
  8. package/dist/__tests__/pipeline/calloutOverflow.test.js.map +1 -0
  9. package/dist/__tests__/pipeline/colonBalancing.test.d.ts +2 -0
  10. package/dist/__tests__/pipeline/colonBalancing.test.d.ts.map +1 -0
  11. package/dist/__tests__/pipeline/colonBalancing.test.js +124 -0
  12. package/dist/__tests__/pipeline/colonBalancing.test.js.map +1 -0
  13. package/dist/__tests__/pipeline/floatFirstSlot.test.js +33 -0
  14. package/dist/__tests__/pipeline/floatFirstSlot.test.js.map +1 -1
  15. package/dist/__tests__/pipeline/headingShortBand.test.d.ts +2 -0
  16. package/dist/__tests__/pipeline/headingShortBand.test.d.ts.map +1 -0
  17. package/dist/__tests__/pipeline/headingShortBand.test.js +89 -0
  18. package/dist/__tests__/pipeline/headingShortBand.test.js.map +1 -0
  19. package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.d.ts +2 -0
  20. package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.d.ts.map +1 -0
  21. package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.js +96 -0
  22. package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.js.map +1 -0
  23. package/dist/__tests__/pipeline/spanFloatAtCut.test.d.ts +2 -0
  24. package/dist/__tests__/pipeline/spanFloatAtCut.test.d.ts.map +1 -0
  25. package/dist/__tests__/pipeline/spanFloatAtCut.test.js +155 -0
  26. package/dist/__tests__/pipeline/spanFloatAtCut.test.js.map +1 -0
  27. package/dist/__tests__/pipeline/tableCellImages.test.d.ts +2 -0
  28. package/dist/__tests__/pipeline/tableCellImages.test.d.ts.map +1 -0
  29. package/dist/__tests__/pipeline/tableCellImages.test.js +166 -0
  30. package/dist/__tests__/pipeline/tableCellImages.test.js.map +1 -0
  31. package/dist/__tests__/pipeline/tableCellLists.test.d.ts +2 -0
  32. package/dist/__tests__/pipeline/tableCellLists.test.d.ts.map +1 -0
  33. package/dist/__tests__/pipeline/tableCellLists.test.js +91 -0
  34. package/dist/__tests__/pipeline/tableCellLists.test.js.map +1 -0
  35. package/dist/__tests__/pipeline/tableSlice.test.d.ts +2 -0
  36. package/dist/__tests__/pipeline/tableSlice.test.d.ts.map +1 -0
  37. package/dist/__tests__/pipeline/tableSlice.test.js +226 -0
  38. package/dist/__tests__/pipeline/tableSlice.test.js.map +1 -0
  39. package/dist/__tests__/pipeline/tableSplit.test.d.ts +2 -0
  40. package/dist/__tests__/pipeline/tableSplit.test.d.ts.map +1 -0
  41. package/dist/__tests__/pipeline/tableSplit.test.js +154 -0
  42. package/dist/__tests__/pipeline/tableSplit.test.js.map +1 -0
  43. package/dist/__tests__/pipeline/trailingCalloutBalance.test.d.ts +2 -0
  44. package/dist/__tests__/pipeline/trailingCalloutBalance.test.d.ts.map +1 -0
  45. package/dist/__tests__/pipeline/trailingCalloutBalance.test.js +60 -0
  46. package/dist/__tests__/pipeline/trailingCalloutBalance.test.js.map +1 -0
  47. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  48. package/dist/canvas-backend/headerFooter.js +2 -6
  49. package/dist/canvas-backend/headerFooter.js.map +1 -1
  50. package/dist/canvas-backend/index.d.ts +1 -1
  51. package/dist/canvas-backend/index.d.ts.map +1 -1
  52. package/dist/canvas-backend/renderResourceBlock.d.ts +15 -1
  53. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  54. package/dist/canvas-backend/renderResourceBlock.js +117 -9
  55. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  56. package/dist/defaults/calloutStyles.d.ts +1 -0
  57. package/dist/defaults/calloutStyles.d.ts.map +1 -1
  58. package/dist/defaults/calloutStyles.js +6 -0
  59. package/dist/defaults/calloutStyles.js.map +1 -1
  60. package/dist/defaults/headings.d.ts +2 -0
  61. package/dist/defaults/headings.d.ts.map +1 -1
  62. package/dist/defaults/headings.js +14 -0
  63. package/dist/defaults/headings.js.map +1 -1
  64. package/dist/defaults/index.d.ts +1 -1
  65. package/dist/defaults/index.d.ts.map +1 -1
  66. package/dist/defaults/index.js +1 -1
  67. package/dist/defaults/index.js.map +1 -1
  68. package/dist/defaults/tableStyle.d.ts +7 -1
  69. package/dist/defaults/tableStyle.d.ts.map +1 -1
  70. package/dist/defaults/tableStyle.js +35 -1
  71. package/dist/defaults/tableStyle.js.map +1 -1
  72. package/dist/html-backend.d.ts.map +1 -1
  73. package/dist/html-backend.js +32 -19
  74. package/dist/html-backend.js.map +1 -1
  75. package/dist/index.d.ts +5 -5
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +2 -2
  78. package/dist/index.js.map +1 -1
  79. package/dist/knuthPlass/breakpoints.d.ts.map +1 -1
  80. package/dist/knuthPlass/breakpoints.js +6 -2
  81. package/dist/knuthPlass/breakpoints.js.map +1 -1
  82. package/dist/knuthPlass/constants.d.ts +3 -1
  83. package/dist/knuthPlass/constants.d.ts.map +1 -1
  84. package/dist/knuthPlass/constants.js +3 -1
  85. package/dist/knuthPlass/constants.js.map +1 -1
  86. package/dist/pipeline/bandCaps.d.ts +4 -2
  87. package/dist/pipeline/bandCaps.d.ts.map +1 -1
  88. package/dist/pipeline/bandCaps.js +5 -3
  89. package/dist/pipeline/bandCaps.js.map +1 -1
  90. package/dist/pipeline/build.d.ts.map +1 -1
  91. package/dist/pipeline/build.js +710 -107
  92. package/dist/pipeline/build.js.map +1 -1
  93. package/dist/pipeline/calloutLayout.d.ts +7 -0
  94. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  95. package/dist/pipeline/calloutLayout.js +36 -6
  96. package/dist/pipeline/calloutLayout.js.map +1 -1
  97. package/dist/pipeline/columnBalancing.d.ts +46 -5
  98. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  99. package/dist/pipeline/columnBalancing.js +142 -6
  100. package/dist/pipeline/columnBalancing.js.map +1 -1
  101. package/dist/pipeline/config.js +1 -1
  102. package/dist/pipeline/config.js.map +1 -1
  103. package/dist/pipeline/floatPlacement.d.ts +4 -0
  104. package/dist/pipeline/floatPlacement.d.ts.map +1 -1
  105. package/dist/pipeline/floatPlacement.js.map +1 -1
  106. package/dist/pipeline/resourceLayout.d.ts +55 -1
  107. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  108. package/dist/pipeline/resourceLayout.js +394 -30
  109. package/dist/pipeline/resourceLayout.js.map +1 -1
  110. package/dist/table/model.d.ts +7 -1
  111. package/dist/table/model.d.ts.map +1 -1
  112. package/dist/table/model.js +22 -0
  113. package/dist/table/model.js.map +1 -1
  114. package/dist/types.d.ts +56 -5
  115. package/dist/types.d.ts.map +1 -1
  116. package/dist/vdt.d.ts +55 -0
  117. package/dist/vdt.d.ts.map +1 -1
  118. package/dist/vdt.js.map +1 -1
  119. package/package.json +1 -1
@@ -23,7 +23,12 @@
23
23
  * the caption is above, otherwise under the caption.
24
24
  *
25
25
  * Resource + caption + note are measured as one group; `totalHeight` is what
26
- * goes to placement (resources paginate atomically — no mid-split for v1).
26
+ * goes to placement. Figures paginate atomically; a table taller than the
27
+ * page can be laid out as a *slice* of its rows (`ResourceLayoutInput.slice`)
28
+ * — a continuation repeats the header rows and suffixes the caption, and a
29
+ * slice that goes on sets a marker under it and holds the note back for the
30
+ * last one. {@link planTableSlice} picks the rows a slice can carry from the
31
+ * row metrics the full-table layout reports.
27
32
  */
28
33
  import type { InlineSpan } from '../parse';
29
34
  import type { Resource, ResourceType, TableModel } from '../types';
@@ -40,6 +45,35 @@ export interface ResourceLayoutInput {
40
45
  resourceNumbering: ResourceNumberingMap;
41
46
  resourceTypes: ResourceType[];
42
47
  resources: Resource[];
48
+ /** Lay out only these rows of a table (a slice of a table split across
49
+ * pages). Ignored for figures. */
50
+ slice?: TableSliceSpec;
51
+ }
52
+ /** The rows a table slice carries. `startRow > 0` makes it a continuation:
53
+ * the header rows are repeated above `startRow` and the caption gets the
54
+ * continued suffix. `continues` sets the marker under the slice (and holds
55
+ * the note back). */
56
+ export interface TableSliceSpec {
57
+ startRow: number;
58
+ /** Exclusive. */
59
+ endRow: number;
60
+ continues: boolean;
61
+ }
62
+ /** Per-row metrics of a table laid out in full at some width, from which
63
+ * {@link planTableSlice} decides where a slice can end. Row heights are
64
+ * exact for any slice at the same width: every row is measured at the same
65
+ * column edges, and rowspans never straddle a break. */
66
+ export interface TableRowMetrics {
67
+ /** Height of each model row in px (rowspan overflow already distributed). */
68
+ rowHeights: number[];
69
+ /** Leading header rows, repeated at the top of every continuation. */
70
+ headerRowCount: number;
71
+ /** Whether a slice may end after row `i` — no rowspan crosses the edge
72
+ * between `i` and `i + 1`. */
73
+ breakableAfter: boolean[];
74
+ /** Rows that head the rows below them (a single cell across every column,
75
+ * or all header cells): a slice never ends on one when it can help it. */
76
+ groupHeaderRow: boolean[];
43
77
  }
44
78
  /** Build a label for an inline `:ref` to a resource, honouring its `style` and
45
79
  * optional override `text`. Mirrors the caption-prefix conventions. */
@@ -58,6 +92,23 @@ export declare function resolveRefSpans(spans: InlineSpan[], resourceNumbering:
58
92
  * non-positive / non-finite weight falls back to an equal split.
59
93
  */
60
94
  export declare function computeColumnEdges(model: TableModel, columnWidth: number): number[];
95
+ /** Number of leading header rows of a table: `headerRowCount` when the
96
+ * model declares it, else the leading run of rows whose visible cells are
97
+ * all header cells. Never the whole table — a continuation needs at least
98
+ * one body row to carry. */
99
+ export declare function tableHeaderRowCount(model: TableModel): number;
100
+ /** The model rows a slice lays out, in order: the header rows (repeated on a
101
+ * continuation) followed by the slice's own rows. */
102
+ export declare function tableSliceRows(model: TableModel, slice: TableSliceSpec): number[];
103
+ /**
104
+ * Where a slice starting at `startRow` can end so that its rows (plus the
105
+ * repeated header rows of a continuation) stay within `bodyBudget` px: the
106
+ * furthest row edge no rowspan straddles, backed off a group-header row so
107
+ * the heading opens the rows it heads on the next page. Returns the
108
+ * exclusive end row — `startRow` when not even one row fits, the row count
109
+ * when the whole rest fits.
110
+ */
111
+ export declare function planTableSlice(metrics: TableRowMetrics, startRow: number, bodyBudget: number): number;
61
112
  /**
62
113
  * Measure a resource block (image / svg / table) plus its caption into a
63
114
  * placement-ready {@link ResolvedResourceBlock}. Returns the block and its
@@ -67,5 +118,8 @@ export declare function computeColumnEdges(model: TableModel, columnWidth: numbe
67
118
  export declare function layoutResourceBlock(input: ResourceLayoutInput): {
68
119
  block: ResolvedResourceBlock;
69
120
  totalHeight: number;
121
+ /** Row metrics of a table laid out in full (no `slice`), for
122
+ * {@link planTableSlice}. */
123
+ tableRows?: TableRowMetrics;
70
124
  };
71
125
  //# sourceMappingURL=resourceLayout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resourceLayout.d.ts","sourceRoot":"","sources":["../../src/pipeline/resourceLayout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAW,MAAM,UAAU,CAAC;AACpD,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EACZ,UAAU,EAIX,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EAItB,MAAM,QAAQ,CAAC;AAUhB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAMhE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,cAAc,CAAC;IACzB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,iBAAiB,EAAE,oBAAoB,CAAC;IACxC,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB;AAED;wEACwE;AACxE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EACnC,iBAAiB,EAAE,oBAAoB,EACvC,aAAa,EAAE,YAAY,EAAE,EAC7B,SAAS,EAAE,QAAQ,EAAE,GACpB,MAAM,CAcR;AAmBD;;6EAE6E;AAC7E,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EAAE,EACnB,iBAAiB,EAAE,oBAAoB,EACvC,aAAa,EAAE,YAAY,EAAE,EAC7B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,CAAC,EAAE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC5C,UAAU,EAAE,CAcd;AAuDD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBnF;AAkJD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,mBAAmB,GAAG;IAC/D,KAAK,EAAE,qBAAqB,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;CACrB,CAiQA"}
1
+ {"version":3,"file":"resourceLayout.d.ts","sourceRoot":"","sources":["../../src/pipeline/resourceLayout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAW,MAAM,UAAU,CAAC;AACpD,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EAEZ,UAAU,EAIX,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EAOtB,MAAM,QAAQ,CAAC;AAUhB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAMhE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,cAAc,CAAC;IACzB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,iBAAiB,EAAE,oBAAoB,CAAC;IACxC,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB;uCACmC;IACnC,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED;;;sBAGsB;AACtB,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;yDAGyD;AACzD,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB;mCAC+B;IAC/B,cAAc,EAAE,OAAO,EAAE,CAAC;IAC1B;+EAC2E;IAC3E,cAAc,EAAE,OAAO,EAAE,CAAC;CAC3B;AAED;wEACwE;AACxE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EACnC,iBAAiB,EAAE,oBAAoB,EACvC,aAAa,EAAE,YAAY,EAAE,EAC7B,SAAS,EAAE,QAAQ,EAAE,GACpB,MAAM,CAcR;AAmBD;;6EAE6E;AAC7E,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EAAE,EACnB,iBAAiB,EAAE,oBAAoB,EACvC,aAAa,EAAE,YAAY,EAAE,EAC7B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,CAAC,EAAE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC5C,UAAU,EAAE,CAcd;AAgPD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBnF;AAED;;;6BAG6B;AAC7B,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAe7D;AAMD;sDACsD;AACtD,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,EAAE,CAajF;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAuBrG;AA4ND;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,mBAAmB,GAAG;IAC/D,KAAK,EAAE,qBAAqB,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB;kCAC8B;IAC9B,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B,CA4TA"}
@@ -23,10 +23,15 @@
23
23
  * the caption is above, otherwise under the caption.
24
24
  *
25
25
  * Resource + caption + note are measured as one group; `totalHeight` is what
26
- * goes to placement (resources paginate atomically — no mid-split for v1).
26
+ * goes to placement. Figures paginate atomically; a table taller than the
27
+ * page can be laid out as a *slice* of its rows (`ResourceLayoutInput.slice`)
28
+ * — a continuation repeats the header rows and suffixes the caption, and a
29
+ * slice that goes on sets a marker under it and holds the note back for the
30
+ * last one. {@link planTableSlice} picks the rows a slice can carry from the
31
+ * row metrics the full-table layout reports.
27
32
  */
28
33
  import { createBoundingBox } from '../vdt';
29
- import { measureRichBlock, buildFontString } from '../measure';
34
+ import { measureRichBlock, measureTextWidth, buildFontString } from '../measure';
30
35
  import { dimensionToPx } from '../units';
31
36
  // Caption / table-cell / note content is parsed with the shared snippet
32
37
  // parser so measurement and the sandbox's glyph→snippet mapping agree on
@@ -112,6 +117,157 @@ function fitWidth(intrinsicW, intrinsicH, columnWidth) {
112
117
  const height = (intrinsicH / intrinsicW) * width;
113
118
  return { width, height };
114
119
  }
120
+ /** Markers a cell paragraph may open with: bullets, dashes, a number with
121
+ * its dot or bracket — followed by whitespace and some text. */
122
+ const CELL_ITEM_MARKER = /^(\s*)([•·◦○▪‣\-*–—]|\d{1,3}[.)])(\s+)(?=\S)/;
123
+ /** Split a cell's spans into paragraphs at hard line breaks (`\n`). The
124
+ * break characters are dropped; an empty paragraph (a blank line) is kept
125
+ * out — the measurer would skip it as whitespace anyway. */
126
+ function splitCellParagraphs(spans) {
127
+ const out = [];
128
+ let current = [];
129
+ const flush = () => {
130
+ if (current.some((sp) => sp.text.trim().length > 0))
131
+ out.push(current);
132
+ current = [];
133
+ };
134
+ for (const span of spans) {
135
+ if (span.ref || span.math || !span.text.includes('\n')) {
136
+ current.push(span);
137
+ continue;
138
+ }
139
+ const pieces = span.text.split('\n');
140
+ pieces.forEach((piece, i) => {
141
+ if (i > 0)
142
+ flush();
143
+ if (piece.length > 0)
144
+ current.push({ ...span, text: piece });
145
+ });
146
+ }
147
+ flush();
148
+ return out;
149
+ }
150
+ /** Detach a list marker from the head of a paragraph, when it opens with
151
+ * one. Returns the marker and the spans with it stripped. */
152
+ function takeCellItemMarker(spans) {
153
+ const first = spans[0];
154
+ if (!first || first.ref || first.math)
155
+ return null;
156
+ const m = CELL_ITEM_MARKER.exec(first.text);
157
+ if (!m)
158
+ return null;
159
+ const [whole, indent, text, ws] = m;
160
+ const rest = first.text.slice(whole.length);
161
+ const stripped = rest.length > 0 ? [{ ...first, text: rest }, ...spans.slice(1)] : spans.slice(1);
162
+ return { marker: { text, ws, level: Math.min(5, 1 + Math.floor(indent.length / 2)) }, spans: stripped };
163
+ }
164
+ /**
165
+ * Measure a cell's content: paragraphs separated by hard line breaks, each
166
+ * either plain text or a list item. An item hangs its text off its marker —
167
+ * the marker is painted as authored ("•", "–", "1."), the text starts after
168
+ * `listGapPx`, and wrapped lines align with the text — nested two spaces per
169
+ * level. Every plain character of the content survives in the produced
170
+ * segments (the marker and its whitespace included), so the sandbox's
171
+ * glyph → snippet mapping stays exact.
172
+ */
173
+ function measureCellContent(spans, set, width, textAlign, listGapPx) {
174
+ const paragraphs = splitCellParagraphs(spans);
175
+ const lines = [];
176
+ let y = 0;
177
+ const measure = (ps, w) => measureRichBlock(ps, set.fontString, set.boldFontString, set.italicFontString, set.boldItalicFontString, Math.max(1, w), set.lineHeightPx, { textAlign: 'left' });
178
+ // Plain paragraphs follow the cell's horizontal alignment: the measurer
179
+ // sets every line flush left at its natural width, so a centred or
180
+ // right-aligned line is pushed over by the slack. List items stay flush
181
+ // left (their markers align).
182
+ const slack = (line) => textAlign === 'center' ? Math.max(0, (width - line.bbox.width) / 2)
183
+ : textAlign === 'right' ? Math.max(0, width - line.bbox.width)
184
+ : 0;
185
+ for (const paragraph of paragraphs) {
186
+ const item = takeCellItemMarker(paragraph);
187
+ if (!item) {
188
+ const m = measure(paragraph, width);
189
+ lines.push(...m.lines.map((line) => shiftLines([line], slack(line), y)[0]));
190
+ y += m.lines.length * set.lineHeightPx;
191
+ continue;
192
+ }
193
+ const markerWidth = measureTextWidth(item.marker.text, set.fontString);
194
+ const indentPx = markerWidth + listGapPx;
195
+ const levelOffset = (item.marker.level - 1) * indentPx;
196
+ const textX = levelOffset + indentPx;
197
+ const m = measure(item.spans, width - textX);
198
+ m.lines.forEach((line, i) => {
199
+ if (i === 0) {
200
+ const head = [
201
+ { kind: 'text', text: item.marker.text, width: markerWidth },
202
+ { kind: 'space', text: item.marker.ws, width: indentPx - markerWidth },
203
+ ];
204
+ lines.push({
205
+ ...line,
206
+ text: `${item.marker.text}${item.marker.ws}${line.text}`,
207
+ bbox: createBoundingBox(levelOffset, line.bbox.y + y, indentPx + line.bbox.width, line.bbox.height),
208
+ baseline: line.baseline + y,
209
+ segments: [...head, ...(line.segments ?? [])],
210
+ });
211
+ }
212
+ else {
213
+ lines.push({
214
+ ...line,
215
+ bbox: createBoundingBox(textX + line.bbox.x, line.bbox.y + y, line.bbox.width, line.bbox.height),
216
+ baseline: line.baseline + y,
217
+ });
218
+ }
219
+ });
220
+ y += m.lines.length * set.lineHeightPx;
221
+ }
222
+ return { lines, totalHeight: y };
223
+ }
224
+ /**
225
+ * Resolve `TableCell.image` against the document's resources and size it:
226
+ * the image takes `image.width` of the cell's inner width (all of it by
227
+ * default) with its aspect ratio kept — a bitmap narrower than that keeps
228
+ * its intrinsic size, like a floated figure — and sits at the cell's
229
+ * horizontal alignment. An id that matches no bitmap / SVG resource, or a
230
+ * payload without a file, yields null and the cell lays out text-only.
231
+ */
232
+ function fitCellImage(image, align, innerWidth, resources) {
233
+ if (!image)
234
+ return null;
235
+ const resource = resources.find((r) => r.id === image.resourceId);
236
+ if (!resource)
237
+ return null;
238
+ const fraction = image.width !== undefined && Number.isFinite(image.width) && image.width > 0
239
+ ? Math.min(1, image.width)
240
+ : 1;
241
+ const target = Math.max(1, innerWidth * fraction);
242
+ let fileId;
243
+ let format;
244
+ let kind;
245
+ let width;
246
+ let height;
247
+ if (resource.kind === 'bitmap' && resource.bitmap) {
248
+ kind = 'bitmap';
249
+ fileId = resource.bitmap.fileId;
250
+ format = resource.bitmap.format;
251
+ const fit = fitWidth(resource.bitmap.width, resource.bitmap.height, target);
252
+ width = fit.width;
253
+ height = fit.height;
254
+ }
255
+ else if (resource.kind === 'svg' && resource.svg) {
256
+ kind = 'svg';
257
+ fileId = resource.svg.fileId;
258
+ const iw = resource.svg.width ?? 0;
259
+ const ih = resource.svg.height ?? 0;
260
+ width = target;
261
+ height = iw > 0 && ih > 0 ? target * (ih / iw) : target * 0.75;
262
+ }
263
+ else {
264
+ return null;
265
+ }
266
+ if (!fileId)
267
+ return null;
268
+ const x = align === 'center' ? (innerWidth - width) / 2 : align === 'right' ? innerWidth - width : 0;
269
+ return { resourceId: resource.id, kind, fileId, format, x: Math.max(0, x), width, height };
270
+ }
115
271
  /** Smallest border thickness (px) we let through: thinner rules would vanish
116
272
  * on screen, but 0.5pt (≈0.67px at 96dpi) hairlines must survive intact —
117
273
  * the previous `max(1, round(px))` rounded them up to a full pixel. */
@@ -147,46 +303,168 @@ export function computeColumnEdges(model, columnWidth) {
147
303
  edges[colCount] = columnWidth;
148
304
  return edges;
149
305
  }
306
+ /** Number of leading header rows of a table: `headerRowCount` when the
307
+ * model declares it, else the leading run of rows whose visible cells are
308
+ * all header cells. Never the whole table — a continuation needs at least
309
+ * one body row to carry. */
310
+ export function tableHeaderRowCount(model) {
311
+ const rowCount = model.rows.length;
312
+ if (rowCount === 0)
313
+ return 0;
314
+ const declared = model.headerRowCount ?? 0;
315
+ let count = 0;
316
+ if (declared > 0) {
317
+ count = declared;
318
+ }
319
+ else {
320
+ for (const row of model.rows) {
321
+ const visible = row.filter((c) => !c.hiddenBy);
322
+ if (visible.length === 0 || !visible.every((c) => c.isHeader))
323
+ break;
324
+ count++;
325
+ }
326
+ }
327
+ return Math.max(0, Math.min(count, rowCount - 1));
328
+ }
329
+ /** Whether a table cell is painted as a header cell. */
330
+ const cellIsHeader = (cell, row, model) => cell.isHeader ?? row < (model.headerRowCount ?? 0);
331
+ /** The model rows a slice lays out, in order: the header rows (repeated on a
332
+ * continuation) followed by the slice's own rows. */
333
+ export function tableSliceRows(model, slice) {
334
+ const headerRows = tableHeaderRowCount(model);
335
+ const rowCount = model.rows.length;
336
+ const start = Math.max(0, Math.min(slice.startRow, rowCount));
337
+ const end = Math.max(start, Math.min(slice.endRow, rowCount));
338
+ const rows = [];
339
+ if (start > 0) {
340
+ for (let r = 0; r < headerRows; r++)
341
+ rows.push(r);
342
+ for (let r = Math.max(start, headerRows); r < end; r++)
343
+ rows.push(r);
344
+ }
345
+ else {
346
+ for (let r = start; r < end; r++)
347
+ rows.push(r);
348
+ }
349
+ return rows;
350
+ }
351
+ /**
352
+ * Where a slice starting at `startRow` can end so that its rows (plus the
353
+ * repeated header rows of a continuation) stay within `bodyBudget` px: the
354
+ * furthest row edge no rowspan straddles, backed off a group-header row so
355
+ * the heading opens the rows it heads on the next page. Returns the
356
+ * exclusive end row — `startRow` when not even one row fits, the row count
357
+ * when the whole rest fits.
358
+ */
359
+ export function planTableSlice(metrics, startRow, bodyBudget) {
360
+ const { rowHeights, headerRowCount, breakableAfter, groupHeaderRow } = metrics;
361
+ const rowCount = rowHeights.length;
362
+ const start = Math.max(0, Math.min(startRow, rowCount));
363
+ // A continuation carries the header rows again; the first slice includes
364
+ // them as ordinary leading rows.
365
+ const firstBody = start > 0 ? Math.max(start, headerRowCount) : start;
366
+ let acc = 0;
367
+ if (start > 0)
368
+ for (let r = 0; r < headerRowCount; r++)
369
+ acc += rowHeights[r] ?? 0;
370
+ let best = start;
371
+ for (let r = firstBody; r < rowCount; r++) {
372
+ acc += rowHeights[r] ?? 0;
373
+ if (acc > bodyBudget + 0.01)
374
+ break;
375
+ if (breakableAfter[r] ?? true)
376
+ best = r + 1;
377
+ }
378
+ // The first slice must carry a body row past the header; a continuation
379
+ // at least its first row. Below that floor the caller decides.
380
+ const floor = (start > 0 ? firstBody : headerRowCount) + 1;
381
+ // Back off group heads left at the cut, but only when a row that is not
382
+ // one remains: a run of nothing but heads is not a heading at all.
383
+ let cut = best;
384
+ while (cut > floor && groupHeaderRow[cut - 1])
385
+ cut--;
386
+ return cut > floor || best <= floor ? cut : best;
387
+ }
150
388
  /** Lay out an HTML-table resource: weighted column split (see
151
389
  * {@link computeColumnEdges}), per-cell rich-text measurement, row height =
152
390
  * max measured cell height. Rowspans reserve their primary cell's full
153
- * vertical extent. */
154
- function layoutTable(model, columnWidth, style, resourceNumbering, resourceTypes, resources, refStyle) {
391
+ * vertical extent. `selection` restricts the layout to those model rows
392
+ * (in order) for a slice; cells keep their model row index. The row metrics
393
+ * are reported for a full layout only. */
394
+ function layoutTable(model, columnWidth, style, resourceNumbering, resourceTypes, resources, refStyle, selection) {
155
395
  const { body, header, borderColor, borderWidthPx, cellPaddingPx } = style;
156
- const rowCount = model.rows.length;
157
- const colCount = rowCount > 0 ? Math.max(...model.rows.map((r) => r.length)) : 0;
396
+ const modelRowCount = model.rows.length;
397
+ const rows = selection ?? model.rows.map((_r, i) => i);
398
+ const rowCount = rows.length;
399
+ /** Slice index of each laid-out model row. */
400
+ const sliceIndexOf = new Map();
401
+ rows.forEach((r, i) => sliceIndexOf.set(r, i));
402
+ const colCount = modelRowCount > 0 ? Math.max(...model.rows.map((r) => r.length)) : 0;
158
403
  const columnEdges = computeColumnEdges(model, columnWidth);
159
404
  const spanWidth = (col, colSpan) => (columnEdges[Math.min(col + colSpan, colCount)] ?? columnWidth) - (columnEdges[col] ?? 0);
405
+ /** Rows a primary cell at model row `r` spanning `rowSpan` rows covers
406
+ * within this layout: the run of its spanned rows that sit consecutively
407
+ * after it in the selection. */
408
+ const spanInSlice = (si, r, rowSpan) => {
409
+ let n = 1;
410
+ while (n < rowSpan && rows[si + n] === r + n)
411
+ n++;
412
+ return n;
413
+ };
160
414
  const measured = [];
161
415
  const rowMinHeight = new Array(rowCount).fill(body.lineHeightPx);
162
- for (let r = 0; r < rowCount; r++) {
416
+ for (let si = 0; si < rowCount; si++) {
417
+ const r = rows[si];
163
418
  const row = model.rows[r];
419
+ if (!row)
420
+ continue;
164
421
  for (let c = 0; c < row.length; c++) {
165
- const cell = row[c];
166
- if (cell.hiddenBy)
167
- continue;
422
+ let cell = row[c];
423
+ if (cell.hiddenBy) {
424
+ // Covered by a merge whose primary is laid out here and reaches this
425
+ // row: skip. A primary left out of the slice (a rowspan running from
426
+ // the header into the body) leaves an empty cell so the grid closes.
427
+ const p = cell.hiddenBy;
428
+ const primary = model.rows[p.row]?.[p.col];
429
+ const psi = sliceIndexOf.get(p.row);
430
+ const covered = primary !== undefined && psi !== undefined && psi <= si
431
+ && spanInSlice(psi, p.row, Math.max(1, primary.rowSpan ?? 1)) > si - psi
432
+ && c >= p.col && c < p.col + Math.max(1, primary.colSpan ?? 1);
433
+ if (covered)
434
+ continue;
435
+ cell = { content: '', isHeader: cellIsHeader(primary ?? cell, p.row, model) };
436
+ }
168
437
  const colSpan = Math.max(1, cell.colSpan ?? 1);
169
- const rowSpan = Math.max(1, cell.rowSpan ?? 1);
170
- const isHeader = cell.isHeader ?? r < (model.headerRowCount ?? 0);
438
+ const rowSpan = spanInSlice(si, r, Math.max(1, cell.rowSpan ?? 1));
439
+ const isHeader = cellIsHeader(cell, r, model);
171
440
  const set = isHeader ? header : body;
172
441
  const cellWidth = spanWidth(c, colSpan) - cellPaddingPx * 2;
173
442
  const spans = resolveRefSpans(parseRefAwareSpans(cell.content), resourceNumbering, resourceTypes, resources, refStyle);
174
- const m = measureRichBlock(spans, set.fontString, set.boldFontString, set.italicFontString, set.boldItalicFontString, Math.max(1, cellWidth), set.lineHeightPx, { textAlign: cell.align === 'center' ? 'center' : cell.align === 'right' ? 'left' : 'left' });
175
- const contentHeight = Math.max(set.lineHeightPx, m.totalHeight) + cellPaddingPx * 2;
443
+ const m = measureCellContent(spans, set, Math.max(1, cellWidth), cell.align ?? 'left', style.listGapPx);
444
+ // An embedded image sits at the top of the cell; the text (when
445
+ // there is any) runs under it, a padding's worth below.
446
+ const image = cell.hiddenBy ? null : fitCellImage(cell.image, cell.align ?? 'left', Math.max(1, cellWidth), resources);
447
+ const textHeight = m.totalHeight;
448
+ const textY = image ? image.height + (textHeight > 0 ? cellPaddingPx : 0) : 0;
449
+ const lines = image && textHeight > 0 ? shiftLines(m.lines, 0, textY) : m.lines;
450
+ const stackHeight = image ? textY + textHeight : textHeight;
451
+ const contentHeight = Math.max(set.lineHeightPx, stackHeight) + cellPaddingPx * 2;
176
452
  measured.push({
177
453
  row: r,
454
+ sliceRow: si,
178
455
  col: c,
179
456
  colSpan,
180
457
  rowSpan,
181
458
  isHeader,
182
459
  align: cell.align ?? 'left',
183
460
  verticalAlign: cell.verticalAlign ?? 'top',
184
- lines: m.lines,
461
+ lines,
185
462
  contentHeight,
463
+ image,
186
464
  });
187
465
  // Single-row cells drive their row's minimum height directly.
188
466
  if (rowSpan === 1) {
189
- rowMinHeight[r] = Math.max(rowMinHeight[r], contentHeight);
467
+ rowMinHeight[si] = Math.max(rowMinHeight[si], contentHeight);
190
468
  }
191
469
  }
192
470
  }
@@ -195,27 +473,64 @@ function layoutTable(model, columnWidth, style, resourceNumbering, resourceTypes
195
473
  if (m.rowSpan <= 1)
196
474
  continue;
197
475
  let spannedHeight = 0;
198
- for (let r = m.row; r < m.row + m.rowSpan && r < rowCount; r++) {
199
- spannedHeight += rowMinHeight[r];
476
+ for (let si = m.sliceRow; si < m.sliceRow + m.rowSpan && si < rowCount; si++) {
477
+ spannedHeight += rowMinHeight[si];
200
478
  }
201
479
  if (m.contentHeight > spannedHeight) {
202
- const lastRow = Math.min(m.row + m.rowSpan - 1, rowCount - 1);
480
+ const lastRow = Math.min(m.sliceRow + m.rowSpan - 1, rowCount - 1);
203
481
  rowMinHeight[lastRow] = rowMinHeight[lastRow] + (m.contentHeight - spannedHeight);
204
482
  }
205
483
  }
206
484
  const rowEdges = [0];
207
- for (let r = 0; r < rowCount; r++)
208
- rowEdges.push(rowEdges[r] + rowMinHeight[r]);
485
+ for (let si = 0; si < rowCount; si++)
486
+ rowEdges.push(rowEdges[si] + rowMinHeight[si]);
209
487
  const tableHeight = rowEdges[rowCount] ?? 0;
488
+ // Row metrics for slice planning (full layouts only — a slice's rows are
489
+ // a subset, so its heights say nothing about the rest).
490
+ let metrics;
491
+ if (!selection) {
492
+ const breakableAfter = new Array(rowCount).fill(true);
493
+ const groupHeaderRow = new Array(rowCount).fill(false);
494
+ const primaries = new Array(rowCount).fill(0);
495
+ const allHeader = new Array(rowCount).fill(true);
496
+ const fullSpan = new Array(rowCount).fill(false);
497
+ for (const m of measured) {
498
+ for (let r = m.row; r < m.row + m.rowSpan - 1; r++)
499
+ breakableAfter[r] = false;
500
+ primaries[m.row] = (primaries[m.row] ?? 0) + 1;
501
+ if (!m.isHeader)
502
+ allHeader[m.row] = false;
503
+ if (m.colSpan >= colCount)
504
+ fullSpan[m.row] = true;
505
+ }
506
+ const headerRowCount = tableHeaderRowCount(model);
507
+ for (let r = headerRowCount; r < rowCount; r++) {
508
+ const p = primaries[r] ?? 0;
509
+ // A single cell across every column heads the rows below it — in a
510
+ // multi-column table. Every row of a one-column (boxed) table spans
511
+ // it, and none of them is a heading.
512
+ groupHeaderRow[r] = p > 0 && ((p === 1 && fullSpan[r] === true && colCount > 1) || allHeader[r] === true);
513
+ }
514
+ metrics = { rowHeights: [...rowMinHeight], headerRowCount, breakableAfter, groupHeaderRow };
515
+ }
210
516
  const cells = measured.map((m) => {
211
517
  const x0 = columnEdges[m.col] ?? 0;
212
518
  const x1 = columnEdges[Math.min(m.col + m.colSpan, colCount)] ?? columnWidth;
213
- const y0 = rowEdges[m.row] ?? 0;
214
- const y1 = rowEdges[Math.min(m.row + m.rowSpan, rowCount)] ?? tableHeight;
519
+ const y0 = rowEdges[m.sliceRow] ?? 0;
520
+ const y1 = rowEdges[Math.min(m.sliceRow + m.rowSpan, rowCount)] ?? tableHeight;
215
521
  const rect = createBoundingBox(x0, y0, x1 - x0, y1 - y0);
216
522
  // Place lines inside the cell with padding; horizontal alignment is applied
217
523
  // by the renderer via the cell rect + align flag.
218
524
  const placed = shiftLines(m.lines, x0 + cellPaddingPx, y0 + cellPaddingPx);
525
+ const image = m.image
526
+ ? {
527
+ resourceId: m.image.resourceId,
528
+ kind: m.image.kind,
529
+ fileId: m.image.fileId,
530
+ ...(m.image.format !== undefined ? { format: m.image.format } : {}),
531
+ rect: createBoundingBox(x0 + cellPaddingPx + m.image.x, y0 + cellPaddingPx, m.image.width, m.image.height),
532
+ }
533
+ : undefined;
219
534
  return {
220
535
  row: m.row,
221
536
  col: m.col,
@@ -226,6 +541,7 @@ function layoutTable(model, columnWidth, style, resourceNumbering, resourceTypes
226
541
  verticalAlign: m.verticalAlign,
227
542
  rect,
228
543
  lines: placed,
544
+ ...(image ? { image } : {}),
229
545
  };
230
546
  });
231
547
  const layout = {
@@ -248,7 +564,7 @@ function layoutTable(model, columnWidth, style, resourceNumbering, resourceTypes
248
564
  rowEdges,
249
565
  rules: style.rules,
250
566
  };
251
- return { layout, height: tableHeight };
567
+ return metrics ? { layout, height: tableHeight, metrics } : { layout, height: tableHeight };
252
568
  }
253
569
  /**
254
570
  * Measure a resource block (image / svg / table) plus its caption into a
@@ -258,6 +574,18 @@ function layoutTable(model, columnWidth, style, resourceNumbering, resourceTypes
258
574
  */
259
575
  export function layoutResourceBlock(input) {
260
576
  const { resource, resourceType, number, resolved, columnWidth, resourceNumbering, resourceTypes, resources, } = input;
577
+ // A slice only applies to tables; a slice covering the whole table from
578
+ // row 0 is the full table (no continuation marks).
579
+ const model = resource.kind === 'table' ? resource.table?.model : undefined;
580
+ const slice = model && input.slice
581
+ && (input.slice.startRow > 0 || input.slice.endRow < model.rows.length || input.slice.continues)
582
+ ? {
583
+ startRow: Math.max(0, Math.min(input.slice.startRow, model.rows.length)),
584
+ endRow: Math.max(0, Math.min(input.slice.endRow, model.rows.length)),
585
+ continued: input.slice.startRow > 0,
586
+ continues: input.slice.continues,
587
+ }
588
+ : undefined;
261
589
  const bodyStyle = resolveBodyStyle(resolved);
262
590
  const dpi = resolved.page.dpi;
263
591
  // Normal / bold weights reused for table + caption font sets.
@@ -273,6 +601,7 @@ export function layoutResourceBlock(input) {
273
601
  let bodyWidth = columnWidth;
274
602
  let bodyHeight = 0;
275
603
  let table;
604
+ let tableRows;
276
605
  let fileId;
277
606
  let format;
278
607
  if (resource.kind === 'bitmap' && resource.bitmap) {
@@ -327,9 +656,11 @@ export function layoutResourceBlock(input) {
327
656
  headerBackground: ts.headerBackgroundEnabled ? ts.headerBackground.hex : undefined,
328
657
  bodyBackground: ts.bodyBackgroundEnabled ? ts.bodyBackground.hex : undefined,
329
658
  rules: ts.rules,
659
+ listGapPx: dimensionToPx(resolved.unorderedLists.gap, dpi, bodyFontPx),
330
660
  };
331
- const { layout, height } = layoutTable(resource.table.model, columnWidth, style, resourceNumbering, resourceTypes, resources, refStyle);
661
+ const { layout, height, metrics } = layoutTable(resource.table.model, columnWidth, style, resourceNumbering, resourceTypes, resources, refStyle, slice ? tableSliceRows(resource.table.model, slice) : undefined);
332
662
  table = layout;
663
+ tableRows = metrics;
333
664
  bodyWidth = columnWidth;
334
665
  bodyHeight = height;
335
666
  }
@@ -363,9 +694,15 @@ export function layoutResourceBlock(input) {
363
694
  const descSpans = cs.descriptionItalic
364
695
  ? resolvedSpans.map((s) => ({ ...s, italic: s.italic || true }))
365
696
  : resolvedSpans;
697
+ // A continued table slice: "Table 6-4. Title (cont.)" — the suffix is
698
+ // set in italics after the description, glued to it by a plain space.
699
+ const suffix = slice?.continued ? resolved.tableStyle.continuedSuffix.trim() : '';
700
+ const suffixSpans = suffix.length > 0
701
+ ? [{ text: `${descSpans.length > 0 ? ' ' : ''}${suffix}`, bold: false, italic: true }]
702
+ : [];
366
703
  const allSpans = prefixText.length > 0
367
- ? [{ text: prefixText, bold: cs.labelBold, italic: cs.labelItalic, captionLabel: true }, ...descSpans]
368
- : descSpans;
704
+ ? [{ text: prefixText, bold: cs.labelBold, italic: cs.labelItalic, captionLabel: true }, ...descSpans, ...suffixSpans]
705
+ : [...descSpans, ...suffixSpans];
369
706
  const measured = measureRichBlock(allSpans, captionFontString, captionBoldFontString, captionItalicFontString, captionBoldItalicFontString, Math.max(1, columnWidth - captionPaddingPx * 2), captionLineHeightPx, { textAlign: cs.align });
370
707
  measuredCaption = measured.lines;
371
708
  }
@@ -385,7 +722,9 @@ export function layoutResourceBlock(input) {
385
722
  const noteBoldItalicFontString = buildFontString(cs.fontFamily, noteFontPx, boldWeight, 'italic');
386
723
  let measuredNote = [];
387
724
  const noteText = resource.note ?? '';
388
- if (noteText.trim().length > 0) {
725
+ // The note closes the table: a slice that continues holds it back for
726
+ // the last slice.
727
+ if (noteText.trim().length > 0 && !slice?.continues) {
389
728
  const noteSpans = resolveRefSpans(parseRefAwareSpans(noteText), resourceNumbering, resourceTypes, resources, refStyle);
390
729
  const slanted = cs.note.italic
391
730
  ? noteSpans.map((s) => ({ ...s, italic: s.italic || true }))
@@ -396,6 +735,24 @@ export function layoutResourceBlock(input) {
396
735
  const noteHeight = measuredNote.length > 0
397
736
  ? measuredNote.length * noteLineHeightPx + noteGapPx
398
737
  : 0;
738
+ // --- Continues marker --------------------------------------------------
739
+ // "Continued" under a slice that goes on: the note's typeface and size,
740
+ // italic, flush right — where the note would sit.
741
+ let measuredContinues = [];
742
+ const ts = resolved.tableStyle;
743
+ const markerText = slice?.continues && ts.continuesMarkerEnabled ? ts.continuesMarker.trim() : '';
744
+ if (markerText.length > 0) {
745
+ const measured = measureRichBlock([{ text: markerText, bold: false, italic: true }], noteFontString, noteBoldFontString, noteItalicFontString, noteBoldItalicFontString, Math.max(1, columnWidth), noteLineHeightPx, { textAlign: 'left' });
746
+ // Flush right: the measurer has no right alignment, so each line is
747
+ // pushed to the block's right edge by its own width.
748
+ measuredContinues = measured.lines.map((line) => ({
749
+ ...line,
750
+ bbox: createBoundingBox(columnWidth - line.bbox.width, line.bbox.y, line.bbox.width, line.bbox.height),
751
+ }));
752
+ }
753
+ const continuesHeight = measuredContinues.length > 0
754
+ ? measuredContinues.length * noteLineHeightPx + noteGapPx
755
+ : 0;
399
756
  // --- Vertical stacking -------------------------------------------------
400
757
  // above: [caption band] gap [body] noteGap [note]
401
758
  // below: [body] gap [caption band] noteGap [note]
@@ -412,6 +769,9 @@ export function layoutResourceBlock(input) {
412
769
  ...cell,
413
770
  rect: createBoundingBox(cell.rect.x, cell.rect.y + bodyY, cell.rect.width, cell.rect.height),
414
771
  lines: shiftLines(cell.lines, 0, bodyY),
772
+ ...(cell.image
773
+ ? { image: { ...cell.image, rect: createBoundingBox(cell.image.rect.x, cell.image.rect.y + bodyY, cell.image.rect.width, cell.image.rect.height) } }
774
+ : {}),
415
775
  })),
416
776
  };
417
777
  }
@@ -428,10 +788,13 @@ export function layoutResourceBlock(input) {
428
788
  : undefined;
429
789
  const noteY = (captionAbove ? bodyY + bodyHeight : bodyHeight + captionHeight) + noteGapPx;
430
790
  const noteLines = shiftLines(measuredNote, 0, noteY);
431
- const totalHeight = bodyHeight + captionHeight + noteHeight;
791
+ // The marker takes the note's slot (a continuing slice has no note).
792
+ const continuesLines = shiftLines(measuredContinues, 0, noteY);
793
+ const totalHeight = bodyHeight + captionHeight + noteHeight + continuesHeight;
432
794
  const block = {
433
795
  resource,
434
796
  kind: resource.kind,
797
+ ...(slice ? { slice } : {}),
435
798
  number,
436
799
  captionPrefix,
437
800
  bodyRect,
@@ -453,7 +816,8 @@ export function layoutResourceBlock(input) {
453
816
  noteItalicFontString,
454
817
  noteBoldItalicFontString,
455
818
  noteColor: cs.note.color.hex,
819
+ continuesLines,
456
820
  };
457
- return { block, totalHeight };
821
+ return tableRows ? { block, totalHeight, tableRows } : { block, totalHeight };
458
822
  }
459
823
  //# sourceMappingURL=resourceLayout.js.map