@peripheries/renderer 0.22.0 → 0.23.1
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 +125 -23
- package/dist/catalog.js +2 -6
- package/dist/chunks/forms-DxA9WzCt.js +336 -0
- package/dist/chunks/{load-CjPvISzH.js → load-b38vUiZM.js} +0 -4
- package/dist/chunks/{page-bundles-DLRIBP8g.js → page-bundles-CPj_vx-J.js} +1 -5
- package/dist/chunks/{persistence-DCMakM9H.js → persistence-hurkUUQ0.js} +0 -6
- package/dist/chunks/{projection-yvYKzMOS.js → projection-zhu32_KJ.js} +1 -15
- package/dist/chunks/{query-BPuCJC1C.js → query-D6xQbTFK.js} +166 -12
- package/dist/chunks/{refs-BLUokra5.js → refs-Csgh6rXq.js} +0 -4
- package/dist/chunks/{structure-o9pYoDjD.js → structure-CKBtQHJq.js} +1 -7
- package/dist/chunks/{validate-4LgH8QMJ.js → validate-BcB_PA1k.js} +20 -13
- package/dist/core.js +138 -47
- package/dist/forms.js +2 -0
- package/dist/print.js +0 -24
- package/dist/react.js +352 -22
- package/dist/runtime.js +2 -12
- package/dist/styles.css +9 -15
- package/dist/testing.js +214 -19
- package/dist/theme.js +0 -10
- package/dist/types/catalog/check.d.ts +0 -13
- package/dist/types/core/actions.d.ts +0 -24
- package/dist/types/core/bundles/load.d.ts +0 -23
- package/dist/types/core/bundles/page-bundles.d.ts +0 -9
- package/dist/types/core/bundles/refs.d.ts +1 -28
- package/dist/types/core/bundles/resolve.d.ts +0 -10
- package/dist/types/core/cell-props.d.ts +0 -20
- package/dist/types/core/cells.d.ts +0 -21
- package/dist/types/core/data-context.d.ts +4 -21
- package/dist/types/core/diagnostics.d.ts +20 -6
- package/dist/types/core/graph.d.ts +6 -44
- package/dist/types/core/index.d.ts +4 -3
- package/dist/types/core/layout.d.ts +0 -9
- package/dist/types/core/load-page.d.ts +30 -88
- package/dist/types/core/query.d.ts +6 -52
- package/dist/types/core/references.d.ts +0 -21
- package/dist/types/core/release.d.ts +11 -0
- package/dist/types/core/rules.d.ts +0 -1
- package/dist/types/core/selected.d.ts +0 -5
- package/dist/types/core/sources.d.ts +10 -63
- package/dist/types/core/store/create-store.d.ts +0 -6
- package/dist/types/core/store/persistence.d.ts +0 -1
- package/dist/types/core/store/projection.d.ts +0 -5
- package/dist/types/core/store/scope-keys.d.ts +0 -1
- package/dist/types/core/store/types.d.ts +0 -6
- package/dist/types/core/structure.d.ts +0 -33
- package/dist/types/core/types.d.ts +2 -13
- package/dist/types/core/validate.d.ts +0 -12
- package/dist/types/core/values.d.ts +0 -6
- package/dist/types/forms/conditions.d.ts +18 -0
- package/dist/types/forms/index.d.ts +3 -0
- package/dist/types/forms/model.d.ts +90 -0
- package/dist/types/forms/port.d.ts +47 -0
- package/dist/types/print/document-bindings.d.ts +0 -61
- package/dist/types/print/document-profile.d.ts +0 -137
- package/dist/types/print/map-staleness.d.ts +0 -66
- package/dist/types/print/page-map.d.ts +1 -334
- package/dist/types/print/paginate.d.ts +0 -190
- package/dist/types/print/print-layout.d.ts +0 -161
- package/dist/types/print/print-lint.d.ts +0 -109
- package/dist/types/print/print-policy.d.ts +0 -184
- package/dist/types/print/print-stylesheet.d.ts +0 -135
- package/dist/types/print/running-header.d.ts +0 -86
- package/dist/types/print/structure-print.d.ts +1 -243
- package/dist/types/react/cell-states.d.ts +0 -2
- package/dist/types/react/cell-view.d.ts +1 -15
- package/dist/types/react/index.d.ts +1 -0
- package/dist/types/react/page-renderer.d.ts +1 -33
- package/dist/types/react/structure-view.d.ts +0 -12
- package/dist/types/react/use-cell-store.d.ts +0 -5
- package/dist/types/react/use-container-size.d.ts +0 -5
- package/dist/types/react/use-forms-runtime.d.ts +147 -0
- package/dist/types/react/use-paging.d.ts +0 -9
- package/dist/types/runtime/boot.d.ts +0 -11
- package/dist/types/runtime/install.d.ts +0 -15
- package/dist/types/runtime/shims.d.ts +0 -8
- package/dist/types/runtime/store-export.d.ts +0 -7
- package/dist/types/testing/base-props.d.ts +0 -1
- package/dist/types/testing/components.d.ts +0 -8
- package/dist/types/testing/form-workbook.d.ts +103 -0
- package/dist/types/testing/golden.d.ts +0 -16
- package/dist/types/testing/ids.d.ts +0 -2
- package/dist/types/testing/index.d.ts +1 -0
- package/dist/types/testing/sources.d.ts +0 -7
- package/dist/types/theme/check.d.ts +0 -2
- package/dist/types/theme/color.d.ts +0 -12
- package/dist/types/theme/derive.d.ts +0 -12
- package/dist/types/theme/parse.d.ts +0 -7
- package/package.json +6 -2
- package/dist/catalog.js.map +0 -1
- package/dist/chunks/load-CjPvISzH.js.map +0 -1
- package/dist/chunks/page-bundles-DLRIBP8g.js.map +0 -1
- package/dist/chunks/persistence-DCMakM9H.js.map +0 -1
- package/dist/chunks/projection-yvYKzMOS.js.map +0 -1
- package/dist/chunks/query-BPuCJC1C.js.map +0 -1
- package/dist/chunks/refs-BLUokra5.js.map +0 -1
- package/dist/chunks/structure-o9pYoDjD.js.map +0 -1
- package/dist/chunks/validate-4LgH8QMJ.js.map +0 -1
- package/dist/core.js.map +0 -1
- package/dist/print.js.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/runtime.js.map +0 -1
- package/dist/testing.js.map +0 -1
- package/dist/theme.js.map +0 -1
|
@@ -1,104 +1,5 @@
|
|
|
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
1
|
import { type PrintConstraints } from './print-policy.js';
|
|
100
2
|
import type { PaginationBlock } from './paginate.js';
|
|
101
|
-
/** The part of a DOMRect this module needs, in a common coordinate space. */
|
|
102
3
|
export interface ElementRect {
|
|
103
4
|
top: number;
|
|
104
5
|
bottom: number;
|
|
@@ -106,175 +7,43 @@ export interface ElementRect {
|
|
|
106
7
|
width: number;
|
|
107
8
|
}
|
|
108
9
|
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
10
|
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
11
|
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
12
|
boxFor?: (cell: Element) => Element;
|
|
129
|
-
/** Selector for a cell. Defaults to the host's cell marker. */
|
|
130
13
|
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
14
|
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
15
|
blankBefore?: (cellId: string) => boolean;
|
|
164
16
|
}
|
|
165
|
-
/** A row of the document: the cells that share it, and its geometry. */
|
|
166
17
|
export interface Band {
|
|
167
|
-
/** Member cell ids joined by `+`. Stable, and readable in a page listing. */
|
|
168
18
|
id: string;
|
|
169
19
|
members: string[];
|
|
170
|
-
/** Offset of the band's top from the container's top, in CSS px. */
|
|
171
20
|
topPx: number;
|
|
172
|
-
/** Band height. The row gap is NOT included — see `blocks`. */
|
|
173
21
|
heightPx: number;
|
|
174
22
|
constraints?: PrintConstraints;
|
|
175
23
|
}
|
|
176
24
|
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
25
|
blocks: PaginationBlock[];
|
|
182
26
|
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
27
|
sections: Array<{
|
|
192
28
|
cellId: string;
|
|
193
29
|
title: string;
|
|
194
30
|
}>;
|
|
195
|
-
/** Width the measurement ran at, in CSS px. The map is only true here. */
|
|
196
31
|
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
32
|
approximate?: boolean;
|
|
202
|
-
/** Boxes whose measured height was 0 — usually "not yet rendered". */
|
|
203
33
|
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
34
|
reconciles: boolean;
|
|
212
35
|
reconciliation: {
|
|
213
|
-
/** Anything above the first band, plus every band and the gaps between. */
|
|
214
36
|
modelPx: number;
|
|
215
|
-
/** The container's own measured height. */
|
|
216
37
|
containerPx: number;
|
|
217
|
-
/** Space above the first band — unmeasured, and it displaces every edge. */
|
|
218
38
|
leadingPx: number;
|
|
219
39
|
};
|
|
220
40
|
}
|
|
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
41
|
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
42
|
export declare function constraintsFromComputedStyle(cs: {
|
|
260
43
|
breakInside?: string;
|
|
261
44
|
breakAfter?: string;
|
|
262
45
|
breakBefore?: string;
|
|
263
46
|
}): 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
47
|
export declare function constraintsFromBox(el: {
|
|
279
48
|
getAttribute(name: string): string | null;
|
|
280
49
|
}, computed: () => {
|
|
@@ -282,18 +51,9 @@ export declare function constraintsFromBox(el: {
|
|
|
282
51
|
breakAfter?: string;
|
|
283
52
|
breakBefore?: string;
|
|
284
53
|
}): PrintConstraints | undefined;
|
|
285
|
-
/** What `forcedBreaksWithin` needs of an element: its element children, in order. */
|
|
286
54
|
type BoxNode = {
|
|
287
55
|
children: ArrayLike<BoxNode>;
|
|
288
56
|
};
|
|
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
57
|
export declare function forcedBreaksWithin<T extends BoxNode>(box: T, styleOf: (el: T) => {
|
|
298
58
|
breakAfter?: string;
|
|
299
59
|
breakBefore?: string;
|
|
@@ -301,107 +61,20 @@ export declare function forcedBreaksWithin<T extends BoxNode>(box: T, styleOf: (
|
|
|
301
61
|
breakBefore?: 'page';
|
|
302
62
|
breakAfter?: 'page';
|
|
303
63
|
};
|
|
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
64
|
export declare function withForcedBreaks(constraints: PrintConstraints | undefined, forced: {
|
|
309
65
|
breakBefore?: 'page';
|
|
310
66
|
breakAfter?: 'page';
|
|
311
67
|
}): 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
68
|
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
69
|
export interface PaperGeometryPx {
|
|
340
|
-
/** Usable height per page — sheet height minus top and bottom margins. */
|
|
341
70
|
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
71
|
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
72
|
pageBoxWidthPx: number;
|
|
373
|
-
/** The marginalia column reserved beside the content. 0 when there is none. */
|
|
374
73
|
railPx: number;
|
|
375
|
-
/** Space between the content column and the marginalia column. 0 when none. */
|
|
376
74
|
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
75
|
columnsReconcile: boolean;
|
|
390
76
|
}
|
|
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
77
|
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
78
|
export declare function paperGeometryPx(sheet: {
|
|
406
79
|
width?: string;
|
|
407
80
|
height?: string;
|
|
@@ -410,13 +83,7 @@ export declare function paperGeometryPx(sheet: {
|
|
|
410
83
|
right?: string;
|
|
411
84
|
bottom?: string;
|
|
412
85
|
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?: {
|
|
86
|
+
} | undefined, columns?: {
|
|
420
87
|
measure?: string;
|
|
421
88
|
rail?: {
|
|
422
89
|
placement?: string;
|
|
@@ -1,231 +1,41 @@
|
|
|
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
1
|
import type { PrintConstraints } from './print-policy.js';
|
|
56
|
-
/** One measured, packable thing. Usually a cell; the packer does not care. */
|
|
57
2
|
export interface PaginationBlock {
|
|
58
3
|
id: string;
|
|
59
|
-
/** Rendered height at the PRINT text-block width, not the screen width. */
|
|
60
4
|
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
5
|
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
6
|
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
7
|
sectionId?: string;
|
|
87
8
|
}
|
|
88
9
|
export interface PaginateOptions {
|
|
89
|
-
/** Usable height per page: sheet height minus top and bottom margins. */
|
|
90
10
|
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
11
|
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
12
|
sectionFillRatio?: number;
|
|
126
13
|
}
|
|
127
14
|
export interface PlacedBlock {
|
|
128
15
|
id: string;
|
|
129
16
|
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
17
|
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
18
|
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
19
|
spansPages?: number;
|
|
155
20
|
}
|
|
156
21
|
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
22
|
blank?: boolean;
|
|
165
|
-
/** 1-based, because it is a page number shown to a person. */
|
|
166
23
|
index: number;
|
|
167
24
|
blocks: PlacedBlock[];
|
|
168
25
|
usedPx: number;
|
|
169
|
-
/** Never negative: a page that overflows reports the overflow separately. */
|
|
170
26
|
wastePx: number;
|
|
171
27
|
}
|
|
172
28
|
export interface PaginationOverflow {
|
|
173
29
|
id: string;
|
|
174
30
|
heightPx: number;
|
|
175
31
|
textBlockPx: number;
|
|
176
|
-
/** How much taller than a whole empty page this block is. */
|
|
177
32
|
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
33
|
availablePx: number;
|
|
188
34
|
}
|
|
189
35
|
export interface PageMap {
|
|
190
36
|
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
37
|
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
38
|
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
39
|
degenerateGeometry?: boolean;
|
|
230
40
|
}
|
|
231
41
|
export declare function paginate(blocks: readonly PaginationBlock[], { textBlockPx, keepWithNextScaleFloor, sectionFillRatio }: PaginateOptions): PageMap;
|