@peripheries/renderer 0.17.0 → 0.20.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.
- package/README.md +1 -0
- package/dist/catalog.js +2 -2
- package/dist/chunks/{projection-BTt_LUza.js → projection-yvYKzMOS.js} +4 -3
- package/dist/chunks/projection-yvYKzMOS.js.map +1 -0
- package/dist/chunks/{query-DRdMhtcX.js → query-BPuCJC1C.js} +109 -21
- package/dist/chunks/query-BPuCJC1C.js.map +1 -0
- package/dist/chunks/{structure-BPBwFPO0.js → structure-o9pYoDjD.js} +29 -3
- package/dist/chunks/{structure-BPBwFPO0.js.map → structure-o9pYoDjD.js.map} +1 -1
- package/dist/chunks/{validate-XuJViQas.js → validate-4LgH8QMJ.js} +107 -11
- package/dist/chunks/{validate-XuJViQas.js.map → validate-4LgH8QMJ.js.map} +1 -1
- package/dist/core.js +147 -17
- package/dist/core.js.map +1 -1
- package/dist/print.js +1391 -0
- package/dist/print.js.map +1 -0
- package/dist/react.js +62 -13
- package/dist/react.js.map +1 -1
- package/dist/runtime.js +5 -4
- package/dist/runtime.js.map +1 -1
- package/dist/styles.css +4 -0
- package/dist/testing.js +1 -1
- package/dist/types/core/cell-props.d.ts +3 -1
- package/dist/types/core/data-context.d.ts +4 -0
- package/dist/types/core/diagnostics.d.ts +16 -0
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/load-page.d.ts +15 -1
- package/dist/types/core/references.d.ts +8 -2
- package/dist/types/core/sources.d.ts +97 -0
- package/dist/types/core/structure.d.ts +16 -0
- package/dist/types/core/types.d.ts +34 -0
- package/dist/types/core/validate.d.ts +10 -0
- package/dist/types/print/document-bindings.d.ts +76 -0
- package/dist/types/print/document-profile.d.ts +203 -0
- package/dist/types/print/index.d.ts +11 -0
- package/dist/types/print/map-staleness.d.ts +75 -0
- package/dist/types/print/page-map.d.ts +427 -0
- package/dist/types/print/paginate.d.ts +231 -0
- package/dist/types/print/print-layout.d.ts +212 -0
- package/dist/types/print/print-lint.d.ts +124 -0
- package/dist/types/print/print-policy.d.ts +210 -0
- package/dist/types/print/print-stylesheet.d.ts +168 -0
- package/dist/types/print/running-header.d.ts +105 -0
- package/dist/types/print/structure-print.d.ts +302 -0
- package/dist/types/react/cell-view.d.ts +5 -0
- package/dist/types/react/page-renderer.d.ts +6 -0
- package/dist/types/runtime/install.d.ts +9 -3
- package/dist/types/testing/components.d.ts +1 -1
- package/package.json +6 -2
- package/dist/chunks/projection-BTt_LUza.js.map +0 -1
- package/dist/chunks/query-DRdMhtcX.js.map +0 -1
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading measured blocks out of the rendered DOM, for `paginate()`.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this is separate from the packer AND from the hook ──────────────────
|
|
5
|
+
*
|
|
6
|
+
* `paginate()` is arithmetic and pure. The React hook owns re-measurement and
|
|
7
|
+
* lifecycle. This middle layer is the part that touches the DOM, and it is a
|
|
8
|
+
* plain function taking injected measurers so it can be tested without a
|
|
9
|
+
* browser — jsdom reports every height as 0, so a collector that called
|
|
10
|
+
* `getBoundingClientRect` directly would be untestable in exactly the way that
|
|
11
|
+
* matters.
|
|
12
|
+
*
|
|
13
|
+
* ── The unit of pagination is a BAND, not a cell ───────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* This module's first version treated a document as a vertical stack of cells,
|
|
16
|
+
* one `[data-cell-id]` per block. That is wrong, and it was wrong in a way that
|
|
17
|
+
* produced credible numbers for a whole day.
|
|
18
|
+
*
|
|
19
|
+
* A `/doc` page is a **12-column grid**. Cells sit side by side: two `span 6`
|
|
20
|
+
* profile cards share one row, a `span 8` chart shares a row with a `span 4`
|
|
21
|
+
* stat strip. You cannot put a page break between two cells that are beside
|
|
22
|
+
* each other — the break goes between grid ROWS. So the unit is a *band*: a
|
|
23
|
+
* maximal set of cells whose vertical extents connect.
|
|
24
|
+
*
|
|
25
|
+
* **How many bands the proof document has depends on HOW the width is narrowed**,
|
|
26
|
+
* and getting that wrong is the third measurement error of this same family:
|
|
27
|
+
*
|
|
28
|
+
* - Narrow the grid element alone and it stays a 12-column grid, because the
|
|
29
|
+
* `container-type: inline-size` ancestor is still 976px wide and the container
|
|
30
|
+
* query has not fired. Six pairs share rows → **27 bands**.
|
|
31
|
+
* - Narrow that ANCESTOR — which is what layout mode does, and what printing
|
|
32
|
+
* does to the whole page — and the query fires, the template collapses to a
|
|
33
|
+
* single 672px column, every `span` becomes full width, and nothing shares a
|
|
34
|
+
* row → **33 bands**.
|
|
35
|
+
*
|
|
36
|
+
* The second is the truth about paper. A hand-measurement that sets a width on
|
|
37
|
+
* the grid and reads the result is measuring a layout that will never print.
|
|
38
|
+
* The banding code stays necessary regardless: a wider sheet, a landscape
|
|
39
|
+
* profile, or a template whose query threshold sits below 672px all keep the
|
|
40
|
+
* multi-column layout, and then a break between two side-by-side cells is
|
|
41
|
+
* exactly the error this prevents.
|
|
42
|
+
*
|
|
43
|
+
* Two further corrections came out of the same measurement, both from picking
|
|
44
|
+
* the wrong element:
|
|
45
|
+
*
|
|
46
|
+
* 1. **The fragmentation box is the cell's PARENT.** `[data-cell-id]` is the
|
|
47
|
+
* `.cell-renderer` inside the grid item; the grid item is the element
|
|
48
|
+
* `CellsRenderer` puts `break-inside`/`break-after` on, and the element the
|
|
49
|
+
* browser would actually split. Measuring the child loses the constraint on
|
|
50
|
+
* **28 of 33 cells** and gets a different height on **10 of 33** (a
|
|
51
|
+
* `SectionHeader` cell is 104px, its grid item 152px).
|
|
52
|
+
* 2. **Constraints are read from computed style, not resolved by name.** The
|
|
53
|
+
* renderer has already resolved `@printPolicy` through the bundle cascade
|
|
54
|
+
* and written it onto that grid item. Re-resolving it here would be a second
|
|
55
|
+
* implementation of a lookup that can disagree with the first; reading the
|
|
56
|
+
* computed value cannot.
|
|
57
|
+
*
|
|
58
|
+
* ── `reconciles` is the check that would have caught all of this ────────────
|
|
59
|
+
*
|
|
60
|
+
* Everything above the first band, plus every band, plus the gaps between them,
|
|
61
|
+
* must equal the measured height of the container. The old per-cell model
|
|
62
|
+
* summed to 9,973px against a 9,629px container and nothing in the code
|
|
63
|
+
* noticed; the first version of THIS model read `row-gap` off the wrong
|
|
64
|
+
* element, lost all 32 gaps, and came 512px short — caught by the check on its
|
|
65
|
+
* first run against a real document rather than by a third day of measuring.
|
|
66
|
+
*
|
|
67
|
+
* So the map now carries that arithmetic as a field. A map that does not
|
|
68
|
+
* reconcile is not slightly wrong — it is a different document — and a consumer
|
|
69
|
+
* must be able to refuse it rather than draw page edges from it.
|
|
70
|
+
*
|
|
71
|
+
* ── The width problem, and why the map admits it ────────────────────────────
|
|
72
|
+
*
|
|
73
|
+
* A page map is only true at the width it was measured at. Measured on the
|
|
74
|
+
* proof document: at the screen's 976px the document has **no** unpageable blocks;
|
|
75
|
+
* with the grid constrained to the true 672px, `ReportCoverHero` becomes
|
|
76
|
+
* unpageable. A screen-width map is not a slightly-wrong print map — it is
|
|
77
|
+
* missing the overflows.
|
|
78
|
+
*
|
|
79
|
+
* Forcing print width on a document someone is reading reflows it under them,
|
|
80
|
+
* so this module does **not** do it: it measures where the DOM already is and
|
|
81
|
+
* reports `approximate` when that is not print width. Exact maps come from a
|
|
82
|
+
* caller already laying out at print width — layout mode.
|
|
83
|
+
*
|
|
84
|
+
* ── What this still cannot see ─────────────────────────────────────────────
|
|
85
|
+
*
|
|
86
|
+
* `@media print` rules are inert unless the browser is actually printing.
|
|
87
|
+
* Constraining a container to 672px does not activate them. So a
|
|
88
|
+
* map taken this way reflects screen-media geometry at paper width, which is
|
|
89
|
+
* close but not identical — on the proof cover the difference was 261px until the
|
|
90
|
+
* print rule stopped being defeated by an inline style. A consumer that needs
|
|
91
|
+
* the print-media truth needs real print emulation, which this module has no
|
|
92
|
+
* way to request.
|
|
93
|
+
*
|
|
94
|
+
* Charts also settle asynchronously, and measuring before they draw yields
|
|
95
|
+
* heights no assertion here can catch. What this offers against that is
|
|
96
|
+
* `zeroHeightIds` — the boxes that measured 0, the observable signature of "not
|
|
97
|
+
* yet rendered". Re-measurement on mutation belongs to the hook.
|
|
98
|
+
*/
|
|
99
|
+
import { type PrintConstraints } from './print-policy.js';
|
|
100
|
+
import type { PaginationBlock } from './paginate.js';
|
|
101
|
+
/** The part of a DOMRect this module needs, in a common coordinate space. */
|
|
102
|
+
export interface ElementRect {
|
|
103
|
+
top: number;
|
|
104
|
+
bottom: number;
|
|
105
|
+
height: number;
|
|
106
|
+
width: number;
|
|
107
|
+
}
|
|
108
|
+
export interface CollectOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Rect of an element. Injected so this is testable: pass
|
|
111
|
+
* `(el) => el.getBoundingClientRect()` in a browser.
|
|
112
|
+
*
|
|
113
|
+
* `top` and `bottom` must be in one coordinate space across all calls —
|
|
114
|
+
* banding compares them against each other. Viewport coordinates are fine.
|
|
115
|
+
*/
|
|
116
|
+
measureRect: (el: Element) => ElementRect;
|
|
117
|
+
/**
|
|
118
|
+
* The fragmentation properties the browser has resolved for this element.
|
|
119
|
+
* Injected for the same reason as `measureRect`, and because a real
|
|
120
|
+
* implementation needs `getComputedStyle`. See `constraintsFromComputedStyle`.
|
|
121
|
+
*/
|
|
122
|
+
readConstraints?: (el: Element) => PrintConstraints | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* The fragmentation box for a matched cell. Defaults to the cell's parent,
|
|
125
|
+
* which is the grid item — see the header. Overridable because a caller with
|
|
126
|
+
* a different cell wrapper should not have to reimplement banding.
|
|
127
|
+
*/
|
|
128
|
+
boxFor?: (cell: Element) => Element;
|
|
129
|
+
/** Selector for a cell. Defaults to the host's cell marker. */
|
|
130
|
+
selector?: string;
|
|
131
|
+
/**
|
|
132
|
+
* Does this cell OPEN a section, and what is the section called?
|
|
133
|
+
*
|
|
134
|
+
* Returns the title, or `undefined` for a cell that opens nothing. The
|
|
135
|
+
* section's id is the cell's id, so a caller never invents one.
|
|
136
|
+
*
|
|
137
|
+
* ── Why this is injected, and why it lives HERE rather than beside the ────
|
|
138
|
+
* ── running header, which also needs it ──────────────────────────────────
|
|
139
|
+
*
|
|
140
|
+
* Because it needs to be inferred exactly once. Two places already wanted
|
|
141
|
+
* "which rows are section headings": the running header, and now the
|
|
142
|
+
* specification's pagination rule 01. Two inferences of one fact drift, and
|
|
143
|
+
* this project has spent a week on exactly that failure in three other
|
|
144
|
+
* shapes. So the walk that already visits every cell answers it, and both
|
|
145
|
+
* consumers read the answer.
|
|
146
|
+
*
|
|
147
|
+
* Injected rather than hard-coded because the signal is not settled.
|
|
148
|
+
* Today it is FEC's own `break-after: avoid` plus a heading element, which
|
|
149
|
+
* selects exactly the seven `*-header` rows on the proof document. If the bundle
|
|
150
|
+
* adopts the specification's stable section ids it becomes a
|
|
151
|
+
* config read, and only the caller changes.
|
|
152
|
+
*/
|
|
153
|
+
sectionStart?: (cell: Element, constraints: PrintConstraints | undefined) => string | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* Does a DELIBERATE blank page precede this cell?
|
|
156
|
+
*
|
|
157
|
+
* Injected like `sectionStart`, and for the same reason: it comes from the
|
|
158
|
+
* page config, which this module has no business reading. `paginate` needs it
|
|
159
|
+
* because a blank page changes the page count and therefore where every
|
|
160
|
+
* subsequent break falls — it cannot be added after the map the way the
|
|
161
|
+
* hover controls are.
|
|
162
|
+
*/
|
|
163
|
+
blankBefore?: (cellId: string) => boolean;
|
|
164
|
+
}
|
|
165
|
+
/** A row of the document: the cells that share it, and its geometry. */
|
|
166
|
+
export interface Band {
|
|
167
|
+
/** Member cell ids joined by `+`. Stable, and readable in a page listing. */
|
|
168
|
+
id: string;
|
|
169
|
+
members: string[];
|
|
170
|
+
/** Offset of the band's top from the container's top, in CSS px. */
|
|
171
|
+
topPx: number;
|
|
172
|
+
/** Band height. The row gap is NOT included — see `blocks`. */
|
|
173
|
+
heightPx: number;
|
|
174
|
+
constraints?: PrintConstraints;
|
|
175
|
+
}
|
|
176
|
+
export interface CollectedBlocks {
|
|
177
|
+
/**
|
|
178
|
+
* One block per band, in reading order, with the row gap folded into every
|
|
179
|
+
* band but the last — a gap sits below its band in the flow.
|
|
180
|
+
*/
|
|
181
|
+
blocks: PaginationBlock[];
|
|
182
|
+
bands: Band[];
|
|
183
|
+
/**
|
|
184
|
+
* The sections found, in document order — `cellId` is the row that opens the
|
|
185
|
+
* section and doubles as its id.
|
|
186
|
+
*
|
|
187
|
+
* Structurally a `SectionMarker` (`running-header.ts`), and deliberately not
|
|
188
|
+
* imported as one: that module imports `Band` from here, and a type import
|
|
189
|
+
* back would make the pair circular for the sake of a two-field object.
|
|
190
|
+
*/
|
|
191
|
+
sections: Array<{
|
|
192
|
+
cellId: string;
|
|
193
|
+
title: string;
|
|
194
|
+
}>;
|
|
195
|
+
/** Width the measurement ran at, in CSS px. The map is only true here. */
|
|
196
|
+
measuredWidthPx: number;
|
|
197
|
+
/**
|
|
198
|
+
* Set when `measuredWidthPx` is not the print text-block width, so the map is
|
|
199
|
+
* indicative rather than exact.
|
|
200
|
+
*/
|
|
201
|
+
approximate?: boolean;
|
|
202
|
+
/** Boxes whose measured height was 0 — usually "not yet rendered". */
|
|
203
|
+
zeroHeightIds: string[];
|
|
204
|
+
/**
|
|
205
|
+
* Does the band model account for the container's height?
|
|
206
|
+
*
|
|
207
|
+
* **False means the map is describing a different document** and a consumer
|
|
208
|
+
* must refuse to present it as precise. This is the assertion whose absence
|
|
209
|
+
* let a per-cell model over-count the proof document by 344px for a day.
|
|
210
|
+
*/
|
|
211
|
+
reconciles: boolean;
|
|
212
|
+
reconciliation: {
|
|
213
|
+
/** Anything above the first band, plus every band and the gaps between. */
|
|
214
|
+
modelPx: number;
|
|
215
|
+
/** The container's own measured height. */
|
|
216
|
+
containerPx: number;
|
|
217
|
+
/** Space above the first band — unmeasured, and it displaces every edge. */
|
|
218
|
+
leadingPx: number;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Read the bands of a container.
|
|
223
|
+
*
|
|
224
|
+
* @param root The element that lays the cells out.
|
|
225
|
+
* @param printWidthPx The print text-block width, for the accuracy comparison.
|
|
226
|
+
*/
|
|
227
|
+
export declare function collectBlocks(root: Element | null, printWidthPx: number, { measureRect, readConstraints, boxFor, selector, sectionStart, blankBefore }: CollectOptions): CollectedBlocks;
|
|
228
|
+
/**
|
|
229
|
+
* `readConstraints` from a `CSSStyleDeclaration` — what the browser resolved.
|
|
230
|
+
*
|
|
231
|
+
* **Only PROHIBITIONS and FORCED breaks are read. Never a permission.**
|
|
232
|
+
*
|
|
233
|
+
* `orphans` and `widows` were already excluded, and the reason given was right:
|
|
234
|
+
* their computed value is always present (`2` in Chrome, declared or not), so a
|
|
235
|
+
* computed style cannot distinguish "this component declared a threshold" from
|
|
236
|
+
* "nobody has ever mentioned this".
|
|
237
|
+
*
|
|
238
|
+
* What this function's own header then got wrong, for a fortnight: it claimed
|
|
239
|
+
* `break-*` do not have that problem, because their initial value `auto` is
|
|
240
|
+
* *"both the absence of a declaration and a true statement about what the
|
|
241
|
+
* browser will do"*. The second half is true and the first half is the bug.
|
|
242
|
+
* `mayNotSplit` does not ask what the browser will do — it asks **what was
|
|
243
|
+
* declared**, and reads an explicit `auto` as PERMISSION to split. Its own
|
|
244
|
+
* header states the rule: *"absence is not permission"*.
|
|
245
|
+
*
|
|
246
|
+
* So every element on every document reported `breakInside: 'auto'`, because
|
|
247
|
+
* that is what an unstyled element computes to, and the packer read every one
|
|
248
|
+
* of them as a component that had asked to be splittable. The conservative
|
|
249
|
+
* default for the 44 untagged components — *"untagged implies unbreakable"* —
|
|
250
|
+
* was unreachable through this path from the day it was written.
|
|
251
|
+
*
|
|
252
|
+
* The declaration now travels as an attribute the renderer stamps
|
|
253
|
+
* (`PRINT_POLICY_ATTR`), and this function is the fallback for a DOM nobody
|
|
254
|
+
* stamped. In that role it must be conservative in the same direction as
|
|
255
|
+
* absence: `avoid` is a prohibition and `page` an instruction, and both are
|
|
256
|
+
* safe to believe from a computed value because neither is what an element gets
|
|
257
|
+
* for free.
|
|
258
|
+
*/
|
|
259
|
+
export declare function constraintsFromComputedStyle(cs: {
|
|
260
|
+
breakInside?: string;
|
|
261
|
+
breakAfter?: string;
|
|
262
|
+
breakBefore?: string;
|
|
263
|
+
}): PrintConstraints | undefined;
|
|
264
|
+
/**
|
|
265
|
+
* The constraints on a fragmentation box: the DECLARATION where there is one,
|
|
266
|
+
* the computed style where there is not.
|
|
267
|
+
*
|
|
268
|
+
* Not a compose of the two. The attribute already carries the composed
|
|
269
|
+
* intrinsic-plus-extrinsic answer — the renderer composed it before writing it
|
|
270
|
+
* — and folding the computed style back in would re-add the `auto` this exists
|
|
271
|
+
* to keep out.
|
|
272
|
+
*
|
|
273
|
+
* The fallback matters and is not defensive padding: `/portal/page/[pageId]`,
|
|
274
|
+
* a print harness and any future headless render reach `collectBlocks` through
|
|
275
|
+
* elements these renderers did not write, and those keep the behaviour they
|
|
276
|
+
* have always had.
|
|
277
|
+
*/
|
|
278
|
+
export declare function constraintsFromBox(el: {
|
|
279
|
+
getAttribute(name: string): string | null;
|
|
280
|
+
}, computed: () => {
|
|
281
|
+
breakInside?: string;
|
|
282
|
+
breakAfter?: string;
|
|
283
|
+
breakBefore?: string;
|
|
284
|
+
}): PrintConstraints | undefined;
|
|
285
|
+
/** What `forcedBreaksWithin` needs of an element: its element children, in order. */
|
|
286
|
+
type BoxNode = {
|
|
287
|
+
children: ArrayLike<BoxNode>;
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* A forced break a box's content asks for at its edges (CSS Fragmentation §3.1: a `break-before`
|
|
291
|
+
* on a box's first in-flow child, and a `break-after` on its last, propagate to the box). A
|
|
292
|
+
* component's own page break (core's `PageBreak` is `break-after: page` on its root) sits
|
|
293
|
+
* inside the cell's frame, so the frame's computed style doesn't show it, but Chrome breaks
|
|
294
|
+
* there. Walks the first-child chain for `before` and the last-child chain for `after`; only
|
|
295
|
+
* forced breaks (`page`) propagate.
|
|
296
|
+
*/
|
|
297
|
+
export declare function forcedBreaksWithin<T extends BoxNode>(box: T, styleOf: (el: T) => {
|
|
298
|
+
breakAfter?: string;
|
|
299
|
+
breakBefore?: string;
|
|
300
|
+
}): {
|
|
301
|
+
breakBefore?: 'page';
|
|
302
|
+
breakAfter?: 'page';
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* The constraints on a box with the forced breaks its content propagates (forcedBreaksWithin)
|
|
306
|
+
* folded in: a forced break wins over `avoid` on the same edge, as it does in CSS.
|
|
307
|
+
*/
|
|
308
|
+
export declare function withForcedBreaks(constraints: PrintConstraints | undefined, forced: {
|
|
309
|
+
breakBefore?: 'page';
|
|
310
|
+
breakAfter?: 'page';
|
|
311
|
+
}): PrintConstraints | undefined;
|
|
312
|
+
/**
|
|
313
|
+
* Build a `readConstraints` from a component-name → policy lookup.
|
|
314
|
+
*
|
|
315
|
+
* The **contract-derived** path, kept for a caller that has no rendered DOM to
|
|
316
|
+
* read (a headless render service resolving policy before layout). Prefer
|
|
317
|
+
* `constraintsFromComputedStyle` where the document is on screen: the renderer
|
|
318
|
+
* has already done this resolution and the DOM cannot disagree with itself.
|
|
319
|
+
*
|
|
320
|
+
* `composeConstraints` is called even for a single link, deliberately: it
|
|
321
|
+
* promotes `atomic` to `breakInside: 'avoid'`, and the render path's failure to
|
|
322
|
+
* call it is exactly how `atomic` once emitted no CSS at all.
|
|
323
|
+
*/
|
|
324
|
+
export declare function constraintsResolver(policyFor: (componentName: string) => PrintConstraints | undefined): (componentName: string | null) => PrintConstraints | undefined;
|
|
325
|
+
/**
|
|
326
|
+
* The paper geometry a page map needs, in CSS pixels.
|
|
327
|
+
*
|
|
328
|
+
* The document profile stores lengths as CSS strings (`'8.5in'`, `'0.75in'`) —
|
|
329
|
+
* the right shape for a stylesheet and useless to a packer that works in
|
|
330
|
+
* numbers. This converts once, at the boundary.
|
|
331
|
+
*
|
|
332
|
+
* **Only absolute units are accepted.** `em`, `%` and `vh` are all legal CSS
|
|
333
|
+
* and none of them has a fixed pixel value on paper, so accepting them would
|
|
334
|
+
* mean converting against a font size or a viewport that has nothing to do with
|
|
335
|
+
* the sheet. An unparseable length yields `null` rather than a plausible
|
|
336
|
+
* default: the geometry falling back silently to US Letter is how a document
|
|
337
|
+
* configured for something else would paginate correctly and print wrong.
|
|
338
|
+
*/
|
|
339
|
+
export interface PaperGeometryPx {
|
|
340
|
+
/** Usable height per page — sheet height minus top and bottom margins. */
|
|
341
|
+
textBlockPx: number;
|
|
342
|
+
/**
|
|
343
|
+
* The width the CONTENT column lays out at — the profile's `measure` where it
|
|
344
|
+
* declares one, otherwise the full page box.
|
|
345
|
+
*
|
|
346
|
+
* ── Why this is the measure and not sheet-minus-margins ──────────────────
|
|
347
|
+
*
|
|
348
|
+
* It was sheet-minus-margins, and the component repo measured what that costs on a
|
|
349
|
+
* profile whose measure is narrower than its page box. The bundle's `instrument`
|
|
350
|
+
* states its page as a single sum:
|
|
351
|
+
*
|
|
352
|
+
* 1.25in margin · 4.50in measure · 0.25in gutter · 1.50in marginalia · 1.00in margin
|
|
353
|
+
*
|
|
354
|
+
* The page box is `8.5 − 1.25 − 1.00 = 6.25in`, and the text column is
|
|
355
|
+
* specified at **4.50in**. Laying content out across the whole box set it
|
|
356
|
+
* **39% over measure** and never established the marginalia column at all.
|
|
357
|
+
*
|
|
358
|
+
* **`workbook` and `report` are unaffected, and that is arithmetic rather
|
|
359
|
+
* than a special case:** workbook's measure is `6.85in` and its page box is
|
|
360
|
+
* `8.5 − 0.95 − 0.70 = 6.85in`. Identical. `report` extends workbook. So
|
|
361
|
+
* reading the measure changes nothing for either, which is what the
|
|
362
|
+
* component repo asked for.
|
|
363
|
+
*/
|
|
364
|
+
printWidthPx: number;
|
|
365
|
+
/**
|
|
366
|
+
* The full width between the left and right margins.
|
|
367
|
+
*
|
|
368
|
+
* Equal to `printWidthPx` unless the profile declares a narrower measure. The
|
|
369
|
+
* content column occupies `printWidthPx` of it and the remainder is the
|
|
370
|
+
* gutter plus the marginalia column.
|
|
371
|
+
*/
|
|
372
|
+
pageBoxWidthPx: number;
|
|
373
|
+
/** The marginalia column reserved beside the content. 0 when there is none. */
|
|
374
|
+
railPx: number;
|
|
375
|
+
/** Space between the content column and the marginalia column. 0 when none. */
|
|
376
|
+
gutterPx: number;
|
|
377
|
+
/**
|
|
378
|
+
* Does the profile's own column arithmetic add up?
|
|
379
|
+
*
|
|
380
|
+
* `printWidthPx + gutterPx + railPx` against `pageBoxWidthPx`. False means the
|
|
381
|
+
* profile's stated sum disagrees with its own sheet and margins, and the
|
|
382
|
+
* caller is reserving a column from a page that has no room for it.
|
|
383
|
+
*
|
|
384
|
+
* Reported rather than clamped, for `reconciles`' reason: a geometry that
|
|
385
|
+
* does not add up is not slightly wrong, it is describing a different page,
|
|
386
|
+
* and silently absorbing the difference is how a wrong number survives.
|
|
387
|
+
* `true` when no rail column is declared — there is nothing to add up.
|
|
388
|
+
*/
|
|
389
|
+
columnsReconcile: boolean;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Parse one CSS absolute length to px, or `null`.
|
|
393
|
+
*
|
|
394
|
+
* Returns `null` for a relative unit, a unitless number that is not zero, or
|
|
395
|
+
* anything unparseable. `0` alone is legal CSS with no unit and means zero.
|
|
396
|
+
*/
|
|
397
|
+
export declare function cssLengthToPx(value: string | undefined | null): number | null;
|
|
398
|
+
/**
|
|
399
|
+
* Compute the usable text block from a profile's sheet and margins.
|
|
400
|
+
*
|
|
401
|
+
* Returns `null` when any length is unparseable or the result is not a usable
|
|
402
|
+
* box — the caller then has no geometry, which `paginate` reports as
|
|
403
|
+
* `degenerateGeometry` rather than guessing.
|
|
404
|
+
*/
|
|
405
|
+
export declare function paperGeometryPx(sheet: {
|
|
406
|
+
width?: string;
|
|
407
|
+
height?: string;
|
|
408
|
+
} | undefined, margins: {
|
|
409
|
+
top?: string;
|
|
410
|
+
right?: string;
|
|
411
|
+
bottom?: string;
|
|
412
|
+
left?: string;
|
|
413
|
+
} | undefined,
|
|
414
|
+
/**
|
|
415
|
+
* The profile's column declarations. Optional, and absent means "the content
|
|
416
|
+
* fills the page box", which is what every profile did before columns existed and
|
|
417
|
+
* what the host default still does.
|
|
418
|
+
*/
|
|
419
|
+
columns?: {
|
|
420
|
+
measure?: string;
|
|
421
|
+
rail?: {
|
|
422
|
+
placement?: string;
|
|
423
|
+
width?: string;
|
|
424
|
+
gutter?: string;
|
|
425
|
+
};
|
|
426
|
+
}): PaperGeometryPx | null;
|
|
427
|
+
export {};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page boundaries, computed by the host.
|
|
3
|
+
*
|
|
4
|
+
* ── Why the host paginates at all ───────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Layers 1–3 of print mode hand rules to the browser and let it paginate:
|
|
7
|
+
* `@page` geometry from the document profile, `@printPolicy` from each
|
|
8
|
+
* component onto the cell wrapper, `node.print` from the page author. That is
|
|
9
|
+
* enough to *print* well and useless for anything else, because **the browser
|
|
10
|
+
* never reports where the pages fell.** Approved by the repo owner 2026-09-03
|
|
11
|
+
* (*"2. Yes to pagination"*).
|
|
12
|
+
*
|
|
13
|
+
* Everything the owner asked for beyond printing needs page boundaries as data:
|
|
14
|
+
* showing page edges on screen, a layout mode that moves a block between pages,
|
|
15
|
+
* knowing which page the cursor is on, and reflowing when a chart no longer
|
|
16
|
+
* fits. None of it is expressible against a browser that paginates privately.
|
|
17
|
+
*
|
|
18
|
+
* ── The split this file exists to make ─────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* **Measuring is DOM work; packing is arithmetic.** They are separated because
|
|
21
|
+
* only one of them can be tested without a browser, and the browser run is
|
|
22
|
+
* exactly where a regression goes unnoticed — the spike that produced the
|
|
23
|
+
* numbers below was a one-off `evaluate_script` whose result no CI could
|
|
24
|
+
* reproduce. So this module takes measured heights as *input* and is pure.
|
|
25
|
+
*
|
|
26
|
+
* ── Measured on the proof document, 2026-09-03 ──────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* 33 cells, US Letter, 0.75in margins, 96 CSS px/in → text block **912 × 672**.
|
|
29
|
+
* Heights measured with the `display: grid` container constrained to 672px:
|
|
30
|
+
*
|
|
31
|
+
* total content 9,973 px
|
|
32
|
+
* perfect packing 10.94 pages (the floor)
|
|
33
|
+
* every block atomic 14 pages (the conservative worst case)
|
|
34
|
+
* blocks that cannot fit 2 — `ReportCoverHero` 1,150 px,
|
|
35
|
+
* `DecumulationSimulator` 952 px
|
|
36
|
+
*
|
|
37
|
+
* **The first version of these numbers was measured wrong** and was corrected:
|
|
38
|
+
* the width was applied to `firstCell.parentElement`, which is the
|
|
39
|
+
* first cell's own wrapper, so one of 33 cells reflowed. That produced 9,215 px,
|
|
40
|
+
* 10.1 pages and a single overflow — and a conclusion that content barely grows
|
|
41
|
+
* at print width, drawn from the one element that had actually been narrowed.
|
|
42
|
+
* Twelve cells change; the document grows 13.4%.
|
|
43
|
+
*
|
|
44
|
+
* Two consequences that shape the rules below:
|
|
45
|
+
*
|
|
46
|
+
* 1. **The 3-page spread between 10.94 and 14 is what accurate `@printPolicy`
|
|
47
|
+
* tagging buys.** So `breakInside: 'auto'` has to be honoured as a positive
|
|
48
|
+
* assertion, not folded into the default — otherwise the packer cannot
|
|
49
|
+
* realise the tagging it is being given.
|
|
50
|
+
* 2. **A block can exceed the page.** `ReportCoverHero` at 1,150px does not fit
|
|
51
|
+
* on its own page, so "give an unplaceable atomic block its own page" is not
|
|
52
|
+
* a complete answer and this module must be able to say *"this does not fit"*
|
|
53
|
+
* rather than silently clipping it.
|
|
54
|
+
*/
|
|
55
|
+
import type { PrintConstraints } from './print-policy.js';
|
|
56
|
+
/** One measured, packable thing. Usually a cell; the packer does not care. */
|
|
57
|
+
export interface PaginationBlock {
|
|
58
|
+
id: string;
|
|
59
|
+
/** Rendered height at the PRINT text-block width, not the screen width. */
|
|
60
|
+
heightPx: number;
|
|
61
|
+
/**
|
|
62
|
+
* The block's effective constraints — already composed down its nesting
|
|
63
|
+
* chain by `composeConstraints`. This module does not compose: composition is
|
|
64
|
+
* a property of the tree and packing is a property of the sequence, and
|
|
65
|
+
* conflating them is how `atomic` came to mean two things.
|
|
66
|
+
*/
|
|
67
|
+
constraints?: PrintConstraints;
|
|
68
|
+
/**
|
|
69
|
+
* A DELIBERATE blank page immediately before this block.
|
|
70
|
+
*
|
|
71
|
+
* Distinct from the empty page `breakPage` refuses to emit: that guard exists
|
|
72
|
+
* because an ACCIDENTAL blank sheet is a defect. This one is asked for, so it
|
|
73
|
+
* is carried as data and emitted as its own page with no blocks.
|
|
74
|
+
*/
|
|
75
|
+
blankPageBefore?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The section this block belongs to, for the specification's pagination
|
|
78
|
+
* rule 01. Blocks sharing an id must be CONTIGUOUS — a section is a run of
|
|
79
|
+
* the document, not a tag scattered through it.
|
|
80
|
+
*
|
|
81
|
+
* Optional, and absent means "no section": rule 01 simply does not fire, and
|
|
82
|
+
* a document with no sections paginates exactly as it did before. That is the
|
|
83
|
+
* right default while the host still INFERS sections from the DOM rather than
|
|
84
|
+
* reading declared ids.
|
|
85
|
+
*/
|
|
86
|
+
sectionId?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface PaginateOptions {
|
|
89
|
+
/** Usable height per page: sheet height minus top and bottom margins. */
|
|
90
|
+
textBlockPx: number;
|
|
91
|
+
/**
|
|
92
|
+
* How far an over-tall block may be squeezed to keep its heading with it.
|
|
93
|
+
*
|
|
94
|
+
* When a block too tall for a page is preceded by a `break-after: avoid`
|
|
95
|
+
* heading, the heading is brought onto its page and the block gets what is
|
|
96
|
+
* left. That only helps if what is left is still enough — below this ratio
|
|
97
|
+
* the heading is left behind instead, because a stranded heading is a
|
|
98
|
+
* typographic fault and an illegible chart is a useless one.
|
|
99
|
+
*
|
|
100
|
+
* 0.6 by default, matching `buildPrintLayout`'s `scaleFloor`: this decides
|
|
101
|
+
* whether the row CAN be scaled and that decides how far. Two numbers that
|
|
102
|
+
* must agree, and the default is the same literal in both — if you change one,
|
|
103
|
+
* pass the other.
|
|
104
|
+
*/
|
|
105
|
+
keepWithNextScaleFloor?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Rule 01: *"A section that fills half a page or more starts a new page."*
|
|
108
|
+
*
|
|
109
|
+
* The specification's own words (`Document System.pdf` p11), and its own
|
|
110
|
+
* reason: *"Members flip and file by section, and a section that begins
|
|
111
|
+
* mid-page is hard to find again."* It is the one pagination rule that is
|
|
112
|
+
* unambiguously the host's — a component cannot know what half a page is, and
|
|
113
|
+
* neither can a page config; only the packer holds the geometry and the
|
|
114
|
+
* measured height of the run.
|
|
115
|
+
*
|
|
116
|
+
* Expressed as a RATIO rather than the literal `0.5`, because the threshold
|
|
117
|
+
* is a judgement about findability and not a fact about paper. `0` disables
|
|
118
|
+
* the rule; `1` would mean "only a section that fills a whole page".
|
|
119
|
+
*
|
|
120
|
+
* **This is the one number in this file that came from the specification
|
|
121
|
+
* rather than from a measurement**, which is worth saying out loud: if it
|
|
122
|
+
* turns out to be wrong, it is wrong upstream and the fix is a conversation,
|
|
123
|
+
* not a tuning pass.
|
|
124
|
+
*/
|
|
125
|
+
sectionFillRatio?: number;
|
|
126
|
+
}
|
|
127
|
+
export interface PlacedBlock {
|
|
128
|
+
id: string;
|
|
129
|
+
heightPx: number;
|
|
130
|
+
/**
|
|
131
|
+
* This entry is the CONTINUATION of a block that began on an earlier page,
|
|
132
|
+
* not a second block with the same id. `heightPx` here is the portion on
|
|
133
|
+
* this page, so summing a page's `heightPx` gives its real occupancy.
|
|
134
|
+
*/
|
|
135
|
+
continues?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* The block's FULL measured height, present only when it spans pages.
|
|
138
|
+
*
|
|
139
|
+
* `heightPx` is always this page's share, so the invariant *"a page's block
|
|
140
|
+
* heights sum to its `usedPx`"* holds everywhere. Without this the whole size
|
|
141
|
+
* would be unrecoverable from the map.
|
|
142
|
+
*/
|
|
143
|
+
totalHeightPx?: number;
|
|
144
|
+
/**
|
|
145
|
+
* True when the block is taller than the space remaining and is *permitted*
|
|
146
|
+
* to split, so it continues onto the following page.
|
|
147
|
+
*
|
|
148
|
+
* **The host does not know WHERE inside the block the break falls** — that is
|
|
149
|
+
* the browser's line-breaking, and this packer sees an opaque height. So a
|
|
150
|
+
* spanning block reports which pages it touches and no more. A page map that
|
|
151
|
+
* claimed a split position it cannot compute would be worse than one that
|
|
152
|
+
* admits the gap.
|
|
153
|
+
*/
|
|
154
|
+
spansPages?: number;
|
|
155
|
+
}
|
|
156
|
+
export interface PaginatedPage {
|
|
157
|
+
/**
|
|
158
|
+
* This page is a deliberate blank — no blocks, and it prints the notice.
|
|
159
|
+
*
|
|
160
|
+
* The ONLY page in a map that may have an empty `blocks` array. Every other
|
|
161
|
+
* empty page is a bug, which is why this is a positive marker rather than a
|
|
162
|
+
* count of the blocks.
|
|
163
|
+
*/
|
|
164
|
+
blank?: boolean;
|
|
165
|
+
/** 1-based, because it is a page number shown to a person. */
|
|
166
|
+
index: number;
|
|
167
|
+
blocks: PlacedBlock[];
|
|
168
|
+
usedPx: number;
|
|
169
|
+
/** Never negative: a page that overflows reports the overflow separately. */
|
|
170
|
+
wastePx: number;
|
|
171
|
+
}
|
|
172
|
+
export interface PaginationOverflow {
|
|
173
|
+
id: string;
|
|
174
|
+
heightPx: number;
|
|
175
|
+
textBlockPx: number;
|
|
176
|
+
/** How much taller than a whole empty page this block is. */
|
|
177
|
+
excessPx: number;
|
|
178
|
+
/**
|
|
179
|
+
* The height this block actually gets, which is NOT always a whole page.
|
|
180
|
+
*
|
|
181
|
+
* A block too tall for a page is given its own page — unless it was preceded
|
|
182
|
+
* by a heading that declares `break-after: avoid`, in which case the heading
|
|
183
|
+
* comes with it and the block gets what is left. The consumer that scales the
|
|
184
|
+
* row has to scale it to THIS, not to the sheet, or it will scale to a height
|
|
185
|
+
* that its heading is already occupying part of.
|
|
186
|
+
*/
|
|
187
|
+
availablePx: number;
|
|
188
|
+
}
|
|
189
|
+
export interface PageMap {
|
|
190
|
+
pages: PaginatedPage[];
|
|
191
|
+
/**
|
|
192
|
+
* Blocks that cannot fit a page even alone AND are not permitted to split.
|
|
193
|
+
*
|
|
194
|
+
* Reported rather than thrown, and rather than clipped. `ReportCoverHero` is
|
|
195
|
+
* a real instance (1,150px against 912px), and the only useful thing to do
|
|
196
|
+
* with it is tell somebody — the fix is a tagging or component decision, not
|
|
197
|
+
* a packing one.
|
|
198
|
+
*/
|
|
199
|
+
overflows: PaginationOverflow[];
|
|
200
|
+
/**
|
|
201
|
+
* Total content height ÷ page height. A lower bound, and **unreachable in
|
|
202
|
+
* principle** — do not read it as a target.
|
|
203
|
+
*
|
|
204
|
+
* It assumes blocks may be packed in any order. A document may not: **page
|
|
205
|
+
* order is reading order**, so a chart cannot be moved ahead of its heading
|
|
206
|
+
* to fill a gap. For a fixed sequence of unsplittable blocks, placing each
|
|
207
|
+
* one on the current page if it fits and starting a new page otherwise
|
|
208
|
+
* already minimises the page count — so the greedy pass in `paginate` is
|
|
209
|
+
* optimal, and the gap between it and `idealPages` is not slack a better
|
|
210
|
+
* algorithm can recover.
|
|
211
|
+
*
|
|
212
|
+
* On the proof document that gap is **14 vs 10.94**, and I twice described it as
|
|
213
|
+
* something tagging or better packing would close.
|
|
214
|
+
* Neither can. The only levers are fewer atomic blocks (a tagging decision,
|
|
215
|
+
* now made and exhausted — it bought zero pages), scaling a block to fit (a
|
|
216
|
+
* component decision), or less content.
|
|
217
|
+
*
|
|
218
|
+
* Kept because it is a useful *diagnostic*: a large gap says the document is
|
|
219
|
+
* made of big indivisible blocks, which is a content fact worth surfacing in
|
|
220
|
+
* layout mode. It is not a performance figure.
|
|
221
|
+
*/
|
|
222
|
+
idealPages: number;
|
|
223
|
+
/**
|
|
224
|
+
* Set when `textBlockPx` was not a usable page height, so nothing was
|
|
225
|
+
* paginated and every block sits on one page. Present rather than silent
|
|
226
|
+
* because a one-page map is otherwise indistinguishable from a short
|
|
227
|
+
* document.
|
|
228
|
+
*/
|
|
229
|
+
degenerateGeometry?: boolean;
|
|
230
|
+
}
|
|
231
|
+
export declare function paginate(blocks: readonly PaginationBlock[], { textBlockPx, keepWithNextScaleFloor, sectionFillRatio }: PaginateOptions): PageMap;
|