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.
- package/dist/__tests__/columnBalancing.test.js +78 -1
- package/dist/__tests__/columnBalancing.test.js.map +1 -1
- package/dist/__tests__/exports.test.js +2 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutOverflow.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutOverflow.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutOverflow.test.js +189 -0
- package/dist/__tests__/pipeline/calloutOverflow.test.js.map +1 -0
- package/dist/__tests__/pipeline/colonBalancing.test.d.ts +2 -0
- package/dist/__tests__/pipeline/colonBalancing.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/colonBalancing.test.js +124 -0
- package/dist/__tests__/pipeline/colonBalancing.test.js.map +1 -0
- package/dist/__tests__/pipeline/floatFirstSlot.test.js +33 -0
- package/dist/__tests__/pipeline/floatFirstSlot.test.js.map +1 -1
- package/dist/__tests__/pipeline/headingShortBand.test.d.ts +2 -0
- package/dist/__tests__/pipeline/headingShortBand.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/headingShortBand.test.js +89 -0
- package/dist/__tests__/pipeline/headingShortBand.test.js.map +1 -0
- package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.d.ts +2 -0
- package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.js +96 -0
- package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.js.map +1 -0
- package/dist/__tests__/pipeline/spanFloatAtCut.test.d.ts +2 -0
- package/dist/__tests__/pipeline/spanFloatAtCut.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/spanFloatAtCut.test.js +155 -0
- package/dist/__tests__/pipeline/spanFloatAtCut.test.js.map +1 -0
- package/dist/__tests__/pipeline/tableCellImages.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableCellImages.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableCellImages.test.js +166 -0
- package/dist/__tests__/pipeline/tableCellImages.test.js.map +1 -0
- package/dist/__tests__/pipeline/tableCellLists.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableCellLists.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableCellLists.test.js +91 -0
- package/dist/__tests__/pipeline/tableCellLists.test.js.map +1 -0
- package/dist/__tests__/pipeline/tableSlice.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableSlice.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableSlice.test.js +226 -0
- package/dist/__tests__/pipeline/tableSlice.test.js.map +1 -0
- package/dist/__tests__/pipeline/tableSplit.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableSplit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableSplit.test.js +154 -0
- package/dist/__tests__/pipeline/tableSplit.test.js.map +1 -0
- package/dist/__tests__/pipeline/trailingCalloutBalance.test.d.ts +2 -0
- package/dist/__tests__/pipeline/trailingCalloutBalance.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/trailingCalloutBalance.test.js +60 -0
- package/dist/__tests__/pipeline/trailingCalloutBalance.test.js.map +1 -0
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +2 -6
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/canvas-backend/index.d.ts +1 -1
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts +15 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +117 -9
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/defaults/calloutStyles.d.ts +1 -0
- package/dist/defaults/calloutStyles.d.ts.map +1 -1
- package/dist/defaults/calloutStyles.js +6 -0
- package/dist/defaults/calloutStyles.js.map +1 -1
- package/dist/defaults/headings.d.ts +2 -0
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +14 -0
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/index.d.ts +1 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +1 -1
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts +7 -1
- package/dist/defaults/tableStyle.d.ts.map +1 -1
- package/dist/defaults/tableStyle.js +35 -1
- package/dist/defaults/tableStyle.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +32 -19
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/breakpoints.d.ts.map +1 -1
- package/dist/knuthPlass/breakpoints.js +6 -2
- package/dist/knuthPlass/breakpoints.js.map +1 -1
- package/dist/knuthPlass/constants.d.ts +3 -1
- package/dist/knuthPlass/constants.d.ts.map +1 -1
- package/dist/knuthPlass/constants.js +3 -1
- package/dist/knuthPlass/constants.js.map +1 -1
- package/dist/pipeline/bandCaps.d.ts +4 -2
- package/dist/pipeline/bandCaps.d.ts.map +1 -1
- package/dist/pipeline/bandCaps.js +5 -3
- package/dist/pipeline/bandCaps.js.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +710 -107
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +7 -0
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +36 -6
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/columnBalancing.d.ts +46 -5
- package/dist/pipeline/columnBalancing.d.ts.map +1 -1
- package/dist/pipeline/columnBalancing.js +142 -6
- package/dist/pipeline/columnBalancing.js.map +1 -1
- package/dist/pipeline/config.js +1 -1
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +4 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +55 -1
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +394 -30
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/table/model.d.ts +7 -1
- package/dist/table/model.d.ts.map +1 -1
- package/dist/table/model.js +22 -0
- package/dist/table/model.js.map +1 -1
- package/dist/types.d.ts +56 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +55 -0
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
157
|
-
const
|
|
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
|
|
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
|
-
|
|
166
|
-
if (cell.hiddenBy)
|
|
167
|
-
|
|
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
|
|
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 =
|
|
175
|
-
|
|
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
|
|
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[
|
|
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
|
|
199
|
-
spannedHeight += rowMinHeight[
|
|
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.
|
|
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
|
|
208
|
-
rowEdges.push(rowEdges[
|
|
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.
|
|
214
|
-
const y1 = rowEdges[Math.min(m.
|
|
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
|
-
|
|
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
|
-
|
|
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
|