@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,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing the pagination decision back as CSS, so the browser breaks where the
|
|
3
|
+
* host decided.
|
|
4
|
+
*
|
|
5
|
+
* ── The gap this closes ─────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The host has been able to say where pages should break for a while:
|
|
8
|
+
* `collectBlocks` measures the document's rows, `paginate` assigns them to
|
|
9
|
+
* pages, and the result reconciles exactly against the measured height. None of
|
|
10
|
+
* it reached the paper. Layout mode *drew* the breaks; Chrome went on choosing
|
|
11
|
+
* its own from `break-inside` alone.
|
|
12
|
+
*
|
|
13
|
+
* The owner, three times, in their own words: the layout button should switch to
|
|
14
|
+
* a mode that **adjusts the breaks, not just shows them**, and the result should
|
|
15
|
+
* be what prints. Each time I answered with a better instrument. The step I had
|
|
16
|
+
* not taken is this one, and it is small: emit `break-before: page` on the first
|
|
17
|
+
* row of every page, and the browser's choices become the host's choices.
|
|
18
|
+
*
|
|
19
|
+
* ── Why a stylesheet and not inline styles ─────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* The element that must carry the break is the cell's grid item, and
|
|
22
|
+
* `CellRenderer` writes that element's `style` attribute itself
|
|
23
|
+
* (`grid-column`, `break-inside`, `position`, `container`). Mutating it from an
|
|
24
|
+
* effect means racing React for the same attribute: it wins on the next render
|
|
25
|
+
* and the breaks vanish with no error. It is also the exact defect that defeated
|
|
26
|
+
* the bundle's cover fix — an inline style no author rule could beat.
|
|
27
|
+
*
|
|
28
|
+
* So the decision becomes a `<style>` element. React does not manage it, a
|
|
29
|
+
* re-render cannot clobber it, and it is part of the document that gets printed.
|
|
30
|
+
*
|
|
31
|
+
* `:has(> [data-cell-id="x"])` selects that grid item without needing a class on
|
|
32
|
+
* it, because the item is exactly *the parent of the cell* and CSS has no parent
|
|
33
|
+
* combinator otherwise. Chrome has supported `:has()` since 105; this route is
|
|
34
|
+
* Chrome-only in practice (it is what prints and what a render service drives).
|
|
35
|
+
*
|
|
36
|
+
* `!important` throughout, and not as a reflex: the grid item's own inline
|
|
37
|
+
* `break-inside: avoid` would otherwise outrank a `break-before` rule in the
|
|
38
|
+
* cascade, and an inline `height` on a scaled row would outrank the height that
|
|
39
|
+
* makes the scale add up.
|
|
40
|
+
*
|
|
41
|
+
* ── Resizing, which is the owner's other requirement ───────────────────────
|
|
42
|
+
*
|
|
43
|
+
* *"If it's not a text component or something that can be split across pages
|
|
44
|
+
* and still look good then we need to figure out how to resize the component to
|
|
45
|
+
* fit a page."*
|
|
46
|
+
*
|
|
47
|
+
* A row taller than a whole page is scaled by `k = pageHeight / rowHeight`. The
|
|
48
|
+
* row's box is shortened to `k × height` and its contents scaled from the top
|
|
49
|
+
* left, so the visual geometry is preserved exactly and the layout box shrinks
|
|
50
|
+
* with it. This is what print engines have always done with an oversized figure.
|
|
51
|
+
*
|
|
52
|
+
* **There is a floor.** Below it, type stops being readable and a shrunk
|
|
53
|
+
* component is a worse outcome than a reported one — the honest answer there is
|
|
54
|
+
* that the component needs a print variant from its owner. A silently
|
|
55
|
+
* illegible page is the failure this whole thread keeps producing in other
|
|
56
|
+
* forms: something that looks finished and misleads.
|
|
57
|
+
*
|
|
58
|
+
* ── What this still does NOT do ────────────────────────────────────────────
|
|
59
|
+
*
|
|
60
|
+
* It does not verify that Chrome honours the breaks. Nothing available here can:
|
|
61
|
+
* `@media print` is inert outside printing, there is no print emulation, and no
|
|
62
|
+
* PDF has been opened by anyone but the owner. The rules are asserted to be
|
|
63
|
+
* *emitted and correct*; whether the paper agrees is one look away and has not
|
|
64
|
+
* been taken.
|
|
65
|
+
*/
|
|
66
|
+
import type { Band } from './page-map.js';
|
|
67
|
+
import type { PageMap } from './paginate.js';
|
|
68
|
+
import { type SectionMarker } from './running-header.js';
|
|
69
|
+
export interface PrintLayoutOptions {
|
|
70
|
+
/** Usable page height in CSS px — the sheet minus its top and bottom margins. */
|
|
71
|
+
textBlockPx: number;
|
|
72
|
+
/**
|
|
73
|
+
* Also emit a `@media screen` block that separates the pages visually.
|
|
74
|
+
*
|
|
75
|
+
* On by default. Without it the print view is a single long column on screen
|
|
76
|
+
* that breaks correctly on paper — which is precisely the disconnect this
|
|
77
|
+
* whole feature exists to remove: *"the layout button would switch to a
|
|
78
|
+
* layout mode that adjusts the breaks not just shows them"*. If the screen
|
|
79
|
+
* cannot show the pages, the mode is still only telling you about them.
|
|
80
|
+
*
|
|
81
|
+
* Screen-only, and that scoping is load-bearing: the separation is margin,
|
|
82
|
+
* and margin on paper would shift every subsequent break — the layout would
|
|
83
|
+
* be wrong in exactly the way it is being fixed.
|
|
84
|
+
*/
|
|
85
|
+
screenSeparators?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Smallest scale a row may be shrunk to before it is refused instead.
|
|
88
|
+
*
|
|
89
|
+
* 0.6 by default: a 12px label becomes 7.2px, which is about the floor of
|
|
90
|
+
* comfortable print. Below that, shrinking produces a page nobody can read
|
|
91
|
+
* and hides the problem from the person who could fix it.
|
|
92
|
+
*/
|
|
93
|
+
scaleFloor?: number;
|
|
94
|
+
/**
|
|
95
|
+
* What a deliberate blank page says. Default is the printer's convention.
|
|
96
|
+
*
|
|
97
|
+
* A blank sheet with nothing on it reads as a printing fault, and the reader
|
|
98
|
+
* concluding that is the failure this text exists to prevent.
|
|
99
|
+
*/
|
|
100
|
+
blankPageNotice?: string;
|
|
101
|
+
/**
|
|
102
|
+
* The `[data-cell-id]` element IS the fragmentation box (the host's cell frame, which is the grid
|
|
103
|
+
* item), rather than the box's only child (the PWA's `.cell-renderer`). Rules then target the
|
|
104
|
+
* frame, and a scale shrinks the frame's children.
|
|
105
|
+
*/
|
|
106
|
+
cellIsBox?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Emit per-page running headers and footers.
|
|
109
|
+
*
|
|
110
|
+
* `slots` places a running slot name at a margin-box position, in the same
|
|
111
|
+
* `region -> side` shape a document profile uses. `markers` are the section
|
|
112
|
+
* headings, resolved from the rendered document by the caller.
|
|
113
|
+
*
|
|
114
|
+
* Absent means no running content, which is the state of every document that
|
|
115
|
+
* has not asked for one.
|
|
116
|
+
*/
|
|
117
|
+
running?: {
|
|
118
|
+
slots: RunningSlotPlacement[];
|
|
119
|
+
markers: readonly SectionMarker[];
|
|
120
|
+
/** `' (continued)'` by default. */
|
|
121
|
+
continuedSuffix?: string;
|
|
122
|
+
/** Font and colour for the margin boxes, matching `buildPageStylesheet`. */
|
|
123
|
+
marginFont?: string;
|
|
124
|
+
marginColor?: string;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Where a running slot goes: which margin box, and which slot fills it. */
|
|
128
|
+
export interface RunningSlotPlacement {
|
|
129
|
+
edge: 'top' | 'bottom';
|
|
130
|
+
side: 'left' | 'center' | 'right';
|
|
131
|
+
/** `section-title` or `section-title-continued`. */
|
|
132
|
+
slot: string;
|
|
133
|
+
}
|
|
134
|
+
export interface ScaledRow {
|
|
135
|
+
/** The band id — member cell ids joined by `+`. */
|
|
136
|
+
id: string;
|
|
137
|
+
heightPx: number;
|
|
138
|
+
/** The factor applied. Always < 1. */
|
|
139
|
+
scale: number;
|
|
140
|
+
scaledHeightPx: number;
|
|
141
|
+
}
|
|
142
|
+
export interface RefusedRow {
|
|
143
|
+
id: string;
|
|
144
|
+
heightPx: number;
|
|
145
|
+
/** The scale it would have needed. Below the floor. */
|
|
146
|
+
requiredScale: number;
|
|
147
|
+
}
|
|
148
|
+
export interface PrintLayout {
|
|
149
|
+
/**
|
|
150
|
+
* The stylesheet to put in the document. Empty string when there is nothing
|
|
151
|
+
* to say.
|
|
152
|
+
*
|
|
153
|
+
* Contains both the print typesetting and a `@media screen` block that makes
|
|
154
|
+
* the same page boundaries VISIBLE on screen — see `screenSeparators`.
|
|
155
|
+
*/
|
|
156
|
+
css: string;
|
|
157
|
+
/** Rows that will be broken to a fresh page, in page order. Excludes page 1. */
|
|
158
|
+
pageStartCellIds: string[];
|
|
159
|
+
scaled: ScaledRow[];
|
|
160
|
+
/** Too tall to scale within the floor — these need a print variant. */
|
|
161
|
+
refused: RefusedRow[];
|
|
162
|
+
/** Per-page running content, for a consumer that wants to show it. */
|
|
163
|
+
runningByPage: Array<{
|
|
164
|
+
page: number;
|
|
165
|
+
sectionTitle?: string;
|
|
166
|
+
continued: boolean;
|
|
167
|
+
}>;
|
|
168
|
+
/** Rows given the whole sheet as their box, in page order. */
|
|
169
|
+
fullPageCellIds: string[];
|
|
170
|
+
/** Rows that carry a deliberate blank page before them, in page order. */
|
|
171
|
+
blankPageBeforeCellIds: string[];
|
|
172
|
+
/**
|
|
173
|
+
* How far below such a row's box top its own content begins, on screen.
|
|
174
|
+
*
|
|
175
|
+
* Published rather than re-derived: the hover affordances are positioned
|
|
176
|
+
* against the box and would otherwise be drawn on top of the blank page —
|
|
177
|
+
* the owner's *"there are no options that show up on hover"*. They were
|
|
178
|
+
* there, a page above where anyone would look.
|
|
179
|
+
*/
|
|
180
|
+
blankPageScreenOffsetPx: number;
|
|
181
|
+
/**
|
|
182
|
+
* How much taller `css` makes the document on SCREEN.
|
|
183
|
+
*
|
|
184
|
+
* Two contributions: the page separators, which are margin, border and
|
|
185
|
+
* padding and therefore real height; and the floor put under a full-page row,
|
|
186
|
+
* which grows its box to the sheet.
|
|
187
|
+
*
|
|
188
|
+
* A caller that re-measures the document after applying this stylesheet must
|
|
189
|
+
* add this back, or it will conclude the document changed when only the
|
|
190
|
+
* stylesheet did — see `map-staleness.ts` for what that cost.
|
|
191
|
+
*/
|
|
192
|
+
screenExtraPx: number;
|
|
193
|
+
}
|
|
194
|
+
/** What one separator adds to the document's screen height. */
|
|
195
|
+
/**
|
|
196
|
+
* What the line BELOW a blank page adds on screen — margin plus border.
|
|
197
|
+
*
|
|
198
|
+
* No padding: `box-sizing: border-box` would absorb it into the blank page's
|
|
199
|
+
* own height instead of adding to it, so counting it would overstate the
|
|
200
|
+
* document by 16px per blank page and re-open the staleness oscillation.
|
|
201
|
+
*/
|
|
202
|
+
export declare const BLANK_PAGE_SEPARATOR_PX: number;
|
|
203
|
+
export declare const SEPARATOR_TOTAL_PX: number;
|
|
204
|
+
/**
|
|
205
|
+
* Turn a page map into the CSS that makes it real.
|
|
206
|
+
*
|
|
207
|
+
* @param map From `paginate`, at the same geometry as `options.textBlockPx`.
|
|
208
|
+
* @param bands From `collectBlocks`, so a band id can be resolved to the cell
|
|
209
|
+
* whose box carries the rule. The map alone cannot do this: its
|
|
210
|
+
* block ids are joined band ids, and the selector needs one member.
|
|
211
|
+
*/
|
|
212
|
+
export declare function buildPrintLayout(map: PageMap, bands: readonly Band[], { textBlockPx, scaleFloor, screenSeparators, running, blankPageNotice, cellIsBox }: PrintLayoutOptions): PrintLayout;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination rule 08, as the check the specification asks it to be.
|
|
3
|
+
*
|
|
4
|
+
* `Document System.pdf` p11, rule 08: *"No element is positioned fixed, and no
|
|
5
|
+
* length is in viewport units"* — annotated `lint rule`, the only one of the
|
|
6
|
+
* eight the spec describes as a check rather than as a CSS property. Its
|
|
7
|
+
* reason: *"Fixed elements stamp every sheet and viewport units mis-size at
|
|
8
|
+
* print."*
|
|
9
|
+
*
|
|
10
|
+
* ── The two halves need two different sources, and that is the whole point ──
|
|
11
|
+
*
|
|
12
|
+
* `position: fixed` is visible in computed style. `fixed` is never what an
|
|
13
|
+
* element resolves to for free, so reading it back is sound.
|
|
14
|
+
*
|
|
15
|
+
* **Viewport units are NOT visible in computed style, and this is the same
|
|
16
|
+
* defect that hid `break-inside` for a fortnight**. `max-height:
|
|
17
|
+
* 100vh` computes to `806px` — a resolved value with no trace of how it was
|
|
18
|
+
* written. A check that read computed style would find zero violations on a
|
|
19
|
+
* document full of them and report a clean bill.
|
|
20
|
+
*
|
|
21
|
+
* It is not hypothetical here either. `max-height: 100vh` on the bundle's cover plate
|
|
22
|
+
* is exactly why the cover printed as a small card in the top third of an empty
|
|
23
|
+
* sheet, and `806` — the number I chased for an afternoon — was my
|
|
24
|
+
* own `innerHeight` resolved into the cascade.
|
|
25
|
+
*
|
|
26
|
+
* So viewport units are found by reading the DECLARED text of the stylesheets
|
|
27
|
+
* and the inline `style` attributes. Nothing else can see them.
|
|
28
|
+
*
|
|
29
|
+
* ── Why this reports rather than fixes ─────────────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* Most violations will be in a component repo's components, which are not this repo's to
|
|
32
|
+
* edit. The deliverable for a cross-repo defect is a precise list — selector,
|
|
33
|
+
* property, declared value — that the owner can act on. A host that silently
|
|
34
|
+
* rewrote another repo's CSS would be worse than the bug.
|
|
35
|
+
*/
|
|
36
|
+
/** One rule-08 violation, named precisely enough for its owner to find it. */
|
|
37
|
+
export interface PrintLintViolation {
|
|
38
|
+
rule: 'position-fixed' | 'viewport-unit';
|
|
39
|
+
/** A CSS selector, or a `[data-cell-id]` for an element found in the DOM. */
|
|
40
|
+
where: string;
|
|
41
|
+
/** `position` for the first rule; the offending property for the second. */
|
|
42
|
+
property: string;
|
|
43
|
+
/** The DECLARED value — `100vh`, not the `806px` it resolved to. */
|
|
44
|
+
value: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Scan declared CSS text for viewport units.
|
|
48
|
+
*
|
|
49
|
+
* Exported for its own tests, and because the stylesheet walk that feeds it is
|
|
50
|
+
* inherently untestable without a browser — the parsing is where the mistakes
|
|
51
|
+
* live, so the parsing is what gets tested directly.
|
|
52
|
+
*/
|
|
53
|
+
export declare function viewportUnitsIn(cssText: string, where: string): PrintLintViolation[];
|
|
54
|
+
/**
|
|
55
|
+
* Can a rule under this media condition reach PAPER?
|
|
56
|
+
*
|
|
57
|
+
* ── Why the question is about print, not about "the current media" ─────────
|
|
58
|
+
*
|
|
59
|
+
* A review found this check media-blind: it walked `CSSMediaRule` children
|
|
60
|
+
* happily, so a rule that cannot apply reported identically to one that does.
|
|
61
|
+
* That finding is right and this is the fix — but the fix is NOT to filter to
|
|
62
|
+
* the media the browser is currently in, which is what a first reading suggests.
|
|
63
|
+
*
|
|
64
|
+
* The print route renders in **screen** media. Filtering to the current media
|
|
65
|
+
* would discard everything inside `@media print` — and a viewport unit inside
|
|
66
|
+
* `@media print` is not the harmless case, it is **the dangerous one**. It is
|
|
67
|
+
* the declaration that is guaranteed to be live at exactly the moment rule 08
|
|
68
|
+
* is about it: *"viewport units mis-size at print."*
|
|
69
|
+
*
|
|
70
|
+
* On the specific case that prompted this: the bundle's
|
|
71
|
+
* `.cover-solo .cover-plate { max-height: 100vh }` was print-only. Under
|
|
72
|
+
* "current media" it is a false positive. Under "reaches paper" it is the single
|
|
73
|
+
* most reportable declaration in the sheet. FEC removed it for a better reason
|
|
74
|
+
* than either — the plate's 17/22 ratio resolves to 851px against a 946px page,
|
|
75
|
+
* so the backstop could never fire — but the lint rule matters beyond that one
|
|
76
|
+
* declaration.
|
|
77
|
+
*
|
|
78
|
+
* So: **screen-only is skipped; everything else is kept.** Conservative in the
|
|
79
|
+
* direction that matters, since a false positive is reviewable and a silent
|
|
80
|
+
* drop is not.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately textual rather than `window.matchMedia`, which evaluates against
|
|
83
|
+
* the browser's CURRENT media and would answer the wrong question confidently.
|
|
84
|
+
*/
|
|
85
|
+
export declare function mediaReachesPrint(condition: string | null | undefined): boolean;
|
|
86
|
+
export interface PrintLintOptions {
|
|
87
|
+
/** Resolve an element's computed style. Injected — jsdom has no layout. */
|
|
88
|
+
computedStyle: (el: Element) => {
|
|
89
|
+
position?: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* The CSS rules that APPLY to this document, as `[label, declarations]`.
|
|
93
|
+
*
|
|
94
|
+
* Text rather than `CSSStyleSheet` objects because a cross-origin sheet
|
|
95
|
+
* throws on `.cssRules` with no way to ask first, and because flattening a
|
|
96
|
+
* whole sheet to text destroys the at-rule nesting a caller has to walk
|
|
97
|
+
* anyway.
|
|
98
|
+
*
|
|
99
|
+
* ── "APPLY" is load-bearing, and it cost a wrong number ────────────────────
|
|
100
|
+
*
|
|
101
|
+
* The first version took whole stylesheets. Run against the real document it
|
|
102
|
+
* reported **42** violations; **40** of them were Tailwind utility classes
|
|
103
|
+
* (`h-[85vh]`, `h-screen`, `100dvh` under an `@supports`) sitting unused in
|
|
104
|
+
* the host's global chunk. The document's actual count was **two**.
|
|
105
|
+
*
|
|
106
|
+
* Forty false positives do not read as noise — they read as a document in
|
|
107
|
+
* serious trouble, and they would have been handed to FEC as a list to fix.
|
|
108
|
+
* A wrongly-scoped measurement reads as PRECISE rather than as wrong, which
|
|
109
|
+
* is the fourth time that has happened on this project.
|
|
110
|
+
*
|
|
111
|
+
* So the caller filters to rules that match something under `root` — it has
|
|
112
|
+
* the DOM and can ask `root.querySelector(selectorText)`, which this pure
|
|
113
|
+
* function cannot. A rule that matches nothing cannot mis-size anything.
|
|
114
|
+
*/
|
|
115
|
+
rules: ReadonlyArray<readonly [string, string]>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Audit a rendered document against rule 08.
|
|
119
|
+
*
|
|
120
|
+
* Returns violations in the order found, most-specific label first. An empty
|
|
121
|
+
* array means the document conforms — and unlike a computed-style check, that
|
|
122
|
+
* emptiness is load-bearing rather than structural.
|
|
123
|
+
*/
|
|
124
|
+
export declare function auditPrintLint(root: Element | null, { computedStyle, rules }: PrintLintOptions): PrintLintViolation[];
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@printPolicy` — parsing, composition, and the CSS it becomes.
|
|
3
|
+
*
|
|
4
|
+
* FEC declares a component's paper behaviour as a JSDoc tag; their
|
|
5
|
+
* `scripts/check-print-policy.mts` proves each key against the CSS the
|
|
6
|
+
* component actually emits, and the values reach us on
|
|
7
|
+
* `ComponentDeclaration.print_policy`. This is the host half: read them,
|
|
8
|
+
* compose them down a nesting chain, and emit the styles.
|
|
9
|
+
*
|
|
10
|
+
* Grammar and composition rules from the document-components contract (v1).
|
|
11
|
+
*
|
|
12
|
+
* ── Two classes of key, and only one of them composes ───────────────────────
|
|
13
|
+
*
|
|
14
|
+
* **Constraints** answer *"how may this break"*. They are monotonic: a child may
|
|
15
|
+
* tighten what its parent permits and never loosen it. `orphans`/`widows`
|
|
16
|
+
* compose as `max`, which is that same rule on a threshold instead of an enum.
|
|
17
|
+
*
|
|
18
|
+
* **With one exception, added 2026-09-03: a FORCED break.**
|
|
19
|
+
* `breakAfter: 'page'` is an instruction at a point, not a property of a
|
|
20
|
+
* subtree, and it is not monotonic — it does not tighten an ancestor's `avoid`,
|
|
21
|
+
* it contradicts it. CSS Fragmentation §3.2 settles the conflict in favour of
|
|
22
|
+
* the forced break, and `composeConstraints` follows CSS rather than inventing
|
|
23
|
+
* a rule, because CSS is what actually decides it on paper.
|
|
24
|
+
*
|
|
25
|
+
* It lives on the constraint keys anyway, because CSS models prohibition and
|
|
26
|
+
* instruction as one property with both values in its domain. A third class
|
|
27
|
+
* here would read cleaner and disagree with the enforcer.
|
|
28
|
+
*
|
|
29
|
+
* **Placements** answer *"where does this render"*. They do NOT compose, and
|
|
30
|
+
* applying the constraint rule to them returns a confident wrong answer —
|
|
31
|
+
* FEC's words. `region: page-footer` lifts an element out of its parent
|
|
32
|
+
* entirely, so the parent's atomicity has nothing to say about it.
|
|
33
|
+
* `display: none` is monotonic in the OPPOSITE direction: a hidden parent
|
|
34
|
+
* removes the subtree and a visible child cannot un-hide itself.
|
|
35
|
+
*
|
|
36
|
+
* That distinction is the whole reason this file has two functions instead of
|
|
37
|
+
* one, and it is a known failure in miniature — a sound rule applied to a set it was
|
|
38
|
+
* not written for.
|
|
39
|
+
*
|
|
40
|
+
* ── Values are strings on the wire, numerics included ───────────────────────
|
|
41
|
+
*
|
|
42
|
+
* `"3"`, not `3`. FEC offered to emit typed values; coercing here is one line,
|
|
43
|
+
* where changing their emitter is a shipped-contract change that would also
|
|
44
|
+
* strand every already-built bundle. So the coercion lives at this parser and
|
|
45
|
+
* `print_policy` stays `Record<string, string>` on the wire.
|
|
46
|
+
*/
|
|
47
|
+
export type BreakInside = 'avoid' | 'auto';
|
|
48
|
+
/**
|
|
49
|
+
* A break edge is bivalent: a prohibition or an instruction.
|
|
50
|
+
*
|
|
51
|
+
* A review found that the grammar could only ever FORBID a break — every
|
|
52
|
+
* constraint key was `avoid`, `auto`, or a threshold — so `PageBreak`, the one
|
|
53
|
+
* component in the document bundle that exists solely to force a break, could not be
|
|
54
|
+
* described by the contract at all. Its whole implementation is
|
|
55
|
+
* `style={{ breakAfter: 'page' }}`, and this parser rejected that value as
|
|
56
|
+
* unrecognised.
|
|
57
|
+
*
|
|
58
|
+
* `page` is added here rather than deferred to the paginator because **CSS
|
|
59
|
+
* enforces it today**. `break-after: page` is honoured by Chromium in print
|
|
60
|
+
* with no host pagination whatsoever, so this reaches paper by the same path as
|
|
61
|
+
* `avoid` — `printPolicyToStyle` onto the cell wrapper. A key that only a
|
|
62
|
+
* future paginator could honour would be the declared-and-unread shape this
|
|
63
|
+
* whole effort exists to end.
|
|
64
|
+
*/
|
|
65
|
+
export type BreakEdge = 'avoid' | 'page';
|
|
66
|
+
/** "How may this break." Monotonic — a child may only tighten. */
|
|
67
|
+
export interface PrintConstraints {
|
|
68
|
+
breakInside?: BreakInside;
|
|
69
|
+
/**
|
|
70
|
+
* `avoid` prohibits a break; `page` FORCES one. Both live on this one key
|
|
71
|
+
* because CSS models them as one property (`break-after`) with both values in
|
|
72
|
+
* its domain, and splitting them would fight the enforcer. See
|
|
73
|
+
* `composeConstraints` for the precedence rule when they conflict.
|
|
74
|
+
*/
|
|
75
|
+
breakAfter?: BreakEdge;
|
|
76
|
+
breakBefore?: BreakEdge;
|
|
77
|
+
orphans?: number;
|
|
78
|
+
widows?: number;
|
|
79
|
+
/** Never split, at any depth. Implemented in FEC's checker, declared by nothing yet. */
|
|
80
|
+
atomic?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/** "Where does this render." Does NOT compose like a constraint. */
|
|
83
|
+
export interface PrintPlacements {
|
|
84
|
+
/** Lift out of flow into the page's footer region, repeated per page. */
|
|
85
|
+
region?: 'page-footer';
|
|
86
|
+
/** Omit from paper entirely. */
|
|
87
|
+
display?: 'none';
|
|
88
|
+
}
|
|
89
|
+
export interface ParsedPrintPolicy {
|
|
90
|
+
constraints: PrintConstraints;
|
|
91
|
+
placements: PrintPlacements;
|
|
92
|
+
/**
|
|
93
|
+
* Keys or values this parser did not recognise, as `key=value` strings.
|
|
94
|
+
*
|
|
95
|
+
* **Reported, never silently dropped.** A tag FEC ships that the host quietly
|
|
96
|
+
* ignores is the exact failure `print_policy` spent a month in — declared on
|
|
97
|
+
* ten components and read by nothing. An unknown key means the vocabularies
|
|
98
|
+
* have diverged, and the only way that surfaces is if someone says so.
|
|
99
|
+
*/
|
|
100
|
+
unrecognised: string[];
|
|
101
|
+
}
|
|
102
|
+
export declare function parsePrintPolicy(raw: Record<string, string> | null | undefined): ParsedPrintPolicy;
|
|
103
|
+
/**
|
|
104
|
+
* Compose a chain of CONSTRAINTS, outermost first.
|
|
105
|
+
*
|
|
106
|
+
* A child may tighten and never loosen:
|
|
107
|
+
* - `breakInside`: any `avoid` in the chain wins over every `auto`
|
|
108
|
+
* - `breakAfter` / `breakBefore`: set by anyone, cleared by no one — and a
|
|
109
|
+
* `page` (forced) beats an `avoid` in either order, per CSS Fragmentation
|
|
110
|
+
* §3.2, which is what decides it on paper
|
|
111
|
+
* - `orphans` / `widows`: `max`
|
|
112
|
+
* - `atomic`: any `true` wins, and it also forces `breakInside: avoid`,
|
|
113
|
+
* because "never split at any depth" is strictly stronger
|
|
114
|
+
*
|
|
115
|
+
* **Placements are deliberately absent from this function.** See the header —
|
|
116
|
+
* they answer a different question and composing them here would be wrong in
|
|
117
|
+
* two different directions at once.
|
|
118
|
+
*/
|
|
119
|
+
export declare function composeConstraints(chain: ReadonlyArray<PrintConstraints | undefined | null>): PrintConstraints;
|
|
120
|
+
/**
|
|
121
|
+
* Resolve PLACEMENT down a chain, outermost first.
|
|
122
|
+
*
|
|
123
|
+
* Not a compose — a resolution, and the two keys resolve oppositely:
|
|
124
|
+
* - `display: none` on ANY ancestor hides the subtree; a child cannot
|
|
125
|
+
* un-hide itself, so the outermost `none` is absolute
|
|
126
|
+
* - `region` belongs to the element that declares it. An ancestor's region
|
|
127
|
+
* does not push a descendant into the footer, so the INNERMOST declaration
|
|
128
|
+
* is the one that applies
|
|
129
|
+
*
|
|
130
|
+
* **Innermost-wins for `region` is inferred, not specified.** FEC said region
|
|
131
|
+
* *"belongs to the element that declares it"*, which settles the ancestor case
|
|
132
|
+
* but not two competing declarations — and with `page-footer` as the only legal
|
|
133
|
+
* value that case cannot arise, so no real data can distinguish it. Pinned by a
|
|
134
|
+
* cast in the tests so the rule survives a refactor; flag to FEC if a second
|
|
135
|
+
* region value is ever added.
|
|
136
|
+
*/
|
|
137
|
+
export declare function resolvePlacement(chain: ReadonlyArray<PrintPlacements | undefined | null>): PrintPlacements;
|
|
138
|
+
/**
|
|
139
|
+
* The effective policy as print CSS.
|
|
140
|
+
*
|
|
141
|
+
* Emitted only where it says something: `breakInside: auto` is the CSS default,
|
|
142
|
+
* so writing it changes nothing and would make an unspecified element
|
|
143
|
+
* indistinguishable from a deliberately-splittable one in the DOM. FEC checks
|
|
144
|
+
* `auto` as a positive assertion for that same reason.
|
|
145
|
+
*
|
|
146
|
+
* `region: page-footer` is NOT handled here. Lifting an element into a `@page`
|
|
147
|
+
* margin box is a paginator operation, not a style — and Chromium does not
|
|
148
|
+
* implement GCPM `string()`, so the host resolves footer content to literals at
|
|
149
|
+
* build time (`print-stylesheet.ts`). Returning a style for it would look like
|
|
150
|
+
* support for something that does not work.
|
|
151
|
+
*/
|
|
152
|
+
export declare function printPolicyToStyle(constraints: PrintConstraints, placements?: PrintPlacements): Record<string, string | number>;
|
|
153
|
+
/**
|
|
154
|
+
* The attribute the renderers stamp the DECLARED policy onto.
|
|
155
|
+
*
|
|
156
|
+
* ── Why the DOM needs this when it already carries the CSS ─────────────────
|
|
157
|
+
*
|
|
158
|
+
* Because CSS is lossy in exactly the place the packer reads it. Three
|
|
159
|
+
* declarations that mean different things arrive at `getComputedStyle` as one
|
|
160
|
+
* value:
|
|
161
|
+
*
|
|
162
|
+
* a bundle declared `breakInside: 'auto'` -> computed `auto`
|
|
163
|
+
* a bundle declared nothing at all -> computed `auto` (initial value)
|
|
164
|
+
* nobody has ever considered the component -> computed `auto`
|
|
165
|
+
*
|
|
166
|
+
* `paginate`'s `mayNotSplit` treats an explicit `auto` as PERMISSION to split
|
|
167
|
+
* and absence as a prohibition — *"absence is not permission"*, its own header
|
|
168
|
+
* says — and the DOM had made that rule unachievable. Every band on a real
|
|
169
|
+
* document read back as splittable, including the 44 untagged components the
|
|
170
|
+
* conservative default exists for.
|
|
171
|
+
*
|
|
172
|
+
* `orphans`/`widows` are worse: their computed value is `2` in Chrome whether
|
|
173
|
+
* or not anyone said so, so `constraintsFromComputedStyle` had to drop them
|
|
174
|
+
* entirely — which is why the spec's pagination rule 05 (`orphans: 3;
|
|
175
|
+
* widows: 3`) could not be enforced no matter what a bundle declared.
|
|
176
|
+
*
|
|
177
|
+
* So the renderer, which HAS the declaration, writes it down. This is not a
|
|
178
|
+
* second resolution path: the same `parsePrintPolicy` output that becomes the
|
|
179
|
+
* style becomes the attribute, one line apart. The paginator reads the
|
|
180
|
+
* declaration where there is one and falls back to computed style where there
|
|
181
|
+
* is not, so a page rendered by anything else still paginates as before.
|
|
182
|
+
*/
|
|
183
|
+
export declare const PRINT_POLICY_ATTR = "data-oa-print-policy";
|
|
184
|
+
/**
|
|
185
|
+
* Constraints as an attribute value: `breakAfter=avoid;orphans=3`.
|
|
186
|
+
*
|
|
187
|
+
* Key=value pairs rather than JSON because this is read in devtools far more
|
|
188
|
+
* often than by code, and a quoted-and-escaped object in an attribute is
|
|
189
|
+
* unreadable at exactly the moment someone is asking why a row broke.
|
|
190
|
+
*
|
|
191
|
+
* Deterministic key order, so a DOM diff shows a real change and not a
|
|
192
|
+
* reordering. Empty in, empty out — the caller omits the attribute rather than
|
|
193
|
+
* writing `""`, because an empty attribute is a declaration of nothing and
|
|
194
|
+
* would be indistinguishable from a declaration this parser failed to render.
|
|
195
|
+
*/
|
|
196
|
+
export declare function serializeConstraints(c: PrintConstraints | undefined | null): string;
|
|
197
|
+
/**
|
|
198
|
+
* Read an attribute written by `serializeConstraints` back to constraints.
|
|
199
|
+
*
|
|
200
|
+
* **Routed through `parsePrintPolicy` on purpose.** A private parser here would
|
|
201
|
+
* be a second implementation of the grammar, free to drift from the one that
|
|
202
|
+
* validates FEC's tags — and the whole reason this attribute exists is that two
|
|
203
|
+
* representations of one fact with no shared reader diverge silently. Anything
|
|
204
|
+
* this function accepts, `parsePrintPolicy` accepted first.
|
|
205
|
+
*
|
|
206
|
+
* Returns `undefined` for absent, empty, or entirely-unrecognised input, which
|
|
207
|
+
* is what the packer needs: `undefined` means "no declaration", and that is a
|
|
208
|
+
* different state from "declared, and it permits nothing".
|
|
209
|
+
*/
|
|
210
|
+
export declare function deserializeConstraints(value: string | null | undefined): PrintConstraints | undefined;
|