@beltype/cicero 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/seam/client.d.ts +10 -0
- package/dist/seam/client.js +59 -17
- package/dist/seam/facsimile.d.ts +2 -0
- package/dist/seam/facsimile.js +2 -0
- package/dist/seam/index.js +3 -1
- package/dist/src/capture.d.ts +17 -1
- package/dist/src/capture.js +127 -68
- package/dist/src/chase.js +2 -0
- package/dist/src/document-state.d.ts +27 -0
- package/dist/src/document-state.js +47 -0
- package/dist/src/driver.d.ts +10 -3
- package/dist/src/driver.js +98 -39
- package/dist/src/fragmentation.d.ts +1 -1
- package/dist/src/fragmentation.js +1 -1
- package/dist/src/fragments.d.ts +2 -2
- package/dist/src/fragments.js +49 -10
- package/dist/src/gauge.d.ts +4 -0
- package/dist/src/gauge.js +14 -2
- package/dist/src/hyphen-patterns/en-us.d.ts +1 -0
- package/dist/src/hyphen-patterns/en-us.js +2 -0
- package/dist/src/hyphen-patterns/it.d.ts +21 -0
- package/dist/src/hyphen-patterns/it.js +52 -0
- package/dist/src/hyphenate.d.ts +5 -2
- package/dist/src/hyphenate.js +14 -6
- package/dist/src/measure.d.ts +5 -3
- package/dist/src/measure.js +15 -10
- package/dist/src/reads.js +1 -1
- package/dist/src/root.d.ts +2 -0
- package/dist/src/root.js +18 -3
- package/dist/src/stonehand.js +13 -8
- package/dist/src/word-space.d.ts +8 -0
- package/dist/src/word-space.js +15 -0
- package/package.json +3 -1
package/dist/seam/client.d.ts
CHANGED
|
@@ -110,6 +110,15 @@ export declare function run(opts?: {
|
|
|
110
110
|
laid: number;
|
|
111
111
|
} | null;
|
|
112
112
|
}>;
|
|
113
|
+
/**
|
|
114
|
+
* Let go of everything this client registered in the page, so a document that replaces it (`document.open()` keeps the `Window`, its `FontFaceSet` and its listeners) is not answered by this one's.
|
|
115
|
+
*
|
|
116
|
+
* It removes the `loadingdone` listener, stops the rim, drops the batch of late truth waiting to be answered and deletes the handle from the window, all before the first `await`. A run stopped at a page is let go and its pages stay on screen. A run under way with a pace (`until`) rejects with the let-go error at its next page, and a first run that has shown nothing puts the document back as it found it; one without a pace finishes and presents. When nothing is under way, the promise resolves, after the root's idle build, observer and window listeners and the facsimile's observer have been let go. A `run()` made afterwards is rejected by name. Calling it again is a no-op.
|
|
117
|
+
*
|
|
118
|
+
* @remarks **Host obligations.** The custom-element registry belongs to the window and is not released: the next document's `<beltype-root>`, `<beltype-page>` and PDF element are built from the classes this module defined, which keep no state about the document they were defined for. The handle is gone, and the next client module runs only under a URL the window has not imported (measured 2026-11-03: the same URL after `document.open()` does not run again, a keyed one does, and a second `customElements.define` of a name throws, which the three definitions guard against). The host keys the URL.
|
|
119
|
+
* @see SPEC.md Article XII.7
|
|
120
|
+
*/
|
|
121
|
+
export declare function release(): Promise<void>;
|
|
113
122
|
/** THE HANDLE — `window.__beltypeCicero`, the host's one door onto the engine in the page. The calls are set at load; the doors onto the presentation are seated by every presentation ({@link seatDoors}), so they are absent before the first; `last` and `ledger` are the last run's. */
|
|
114
123
|
export type Handle = {
|
|
115
124
|
run: typeof run;
|
|
@@ -135,5 +144,6 @@ export type Handle = {
|
|
|
135
144
|
addressOf?: (el: Element, word?: number) => Address | null;
|
|
136
145
|
box?: (i: number) => Element | null;
|
|
137
146
|
patch?: typeof patchBox;
|
|
147
|
+
release: typeof release;
|
|
138
148
|
};
|
|
139
149
|
export {};
|
package/dist/seam/client.js
CHANGED
|
@@ -65,8 +65,11 @@ let fontEpoch = 0;
|
|
|
65
65
|
/** The document's face set, kept: it is the same object for the life of the document, and a test that watches how often the engine asks the document for it (progressive.test.js) counts the intake's and the driver's asks and no others. */
|
|
66
66
|
const fonts = document.fonts;
|
|
67
67
|
knowLoadedFaces(fonts);
|
|
68
|
-
|
|
69
|
-
fontEpoch++; }
|
|
68
|
+
const countFaces = (event) => { if (bringsNewFace(event))
|
|
69
|
+
fontEpoch++; };
|
|
70
|
+
fonts.addEventListener('loadingdone', countFaces);
|
|
71
|
+
/** Set by {@link release}: this module instance is finished with its document, and answers no more. A flag of this instance, never of the classes it defined, which the next document's module scripts inherit from the window. */
|
|
72
|
+
let released = false;
|
|
70
73
|
/**
|
|
71
74
|
* The run's conditions, as one string (*a break decision is valid only under render-identical conditions* (I5), applied to the memo — Decision one of *the run resumes from an entry*): the measuring realm's media environment — the viewport, the device pixel ratio, the colour scheme — the text of the stylesheets the document wears, as the rewrite and the presentation left them (the document's own: a sheet inside an `<svg>` or inside a copy the engine made is a box's, and what it changes is that box's extent, which the rim hears — *the conditions are the sheets of the document*, 2026-10-22), and the font epoch the run began under, and how many presenters are registered (*a copy may be presented*: a presenter is a condition of every copy, and a registration after a run is seen here as the change it is). The delivered read values are the driver's to compare, not this string's. Taken after a presentation and again before the next run, so the two readings see the same engine litter (the press `<style>` is left out) and a difference between them is the host's own.
|
|
72
75
|
* @param epoch - {@link fontEpoch} as the run began, which is not the count at the moment of asking: a face that arrived during the run must show at the next one.
|
|
@@ -152,15 +155,16 @@ function patchBox(index, path, el) {
|
|
|
152
155
|
boxes[index] = el;
|
|
153
156
|
}
|
|
154
157
|
rim?.add(boxes[index], top); // the replacement may hold a picture still loading, or an element not yet defined
|
|
155
|
-
const address =
|
|
158
|
+
const address = handle.addressOf(el);
|
|
156
159
|
if (address)
|
|
157
160
|
edited.push(address);
|
|
158
161
|
return address;
|
|
159
162
|
}
|
|
160
163
|
/** THE RIM, from the intake until the frame lets the run go (`rim.js`, *late truth is an edit*): the subscription to the facts that arrive after a measure. `null` before the intake. */
|
|
161
164
|
let rim = null;
|
|
165
|
+
function stopRim() { rim?.stop(); rim = null; }
|
|
162
166
|
/**
|
|
163
|
-
* The rim's default action (*late truth is an edit*, Decision four, 2026-10-13): what a signal the host did not cancel does. Signals are coalesced over one animation frame into one batch; a batch that holds a font is one whole run (a face is a condition of every box), and any other — an image, an element, a store's write, a box's size — is one `run({ resumeAt })` per box, the earliest in the document first and one at a time, since convergence is believed past one edited box. A signal that arrives while a run is under way waits for it; a run stopped at a page is let go and the answer stays within the pages it had cut, as an edit's does. The runs go through the host's own door, `
|
|
167
|
+
* The rim's default action (*late truth is an edit*, Decision four, 2026-10-13): what a signal the host did not cancel does. Signals are coalesced over one animation frame into one batch; a batch that holds a font is one whole run (a face is a condition of every box), and any other — an image, an element, a store's write, a box's size — is one `run({ resumeAt })` per box, the earliest in the document first and one at a time, since convergence is believed past one edited box. A signal that arrives while a run is under way waits for it; a run stopped at a page is let go and the answer stays within the pages it had cut, as an edit's does. The runs go through the host's own door, the `run` property of the handle object this module put on the window, read at each call, so a host that replaces that property (the essay's init scripts and tests do) sees the rim's runs as it sees its own. A host that replaces the whole object `window.__beltypeCicero` does not: the rim keeps calling this module's own handle (so a client that was released, or one a newer client replaced, never reaches into the next document's), and nothing in the repositories searched does that.
|
|
164
168
|
*
|
|
165
169
|
* A box heard through a piece a page draws it with (*a fragment announces its own size*, 2026-10-22) carries the page it was heard on, and its resume is at the address that page begins at, the earliest such page; every other box carries `null` and is resumed at its own address.
|
|
166
170
|
*/
|
|
@@ -200,13 +204,13 @@ async function lateTruth() {
|
|
|
200
204
|
while (batch) {
|
|
201
205
|
const { font, boxes } = batch;
|
|
202
206
|
batch = null;
|
|
203
|
-
const h =
|
|
207
|
+
const h = handle;
|
|
204
208
|
try {
|
|
205
209
|
/** One step, after any run under way has ended; `null` where the run before left nothing to answer. */
|
|
206
210
|
const ready = async () => {
|
|
207
211
|
while (live && !live.gate)
|
|
208
212
|
await live.finished;
|
|
209
|
-
return previous?.composed ?? null;
|
|
213
|
+
return released ? null : previous?.composed ?? null;
|
|
210
214
|
};
|
|
211
215
|
if (font) {
|
|
212
216
|
const composed = await ready();
|
|
@@ -269,6 +273,8 @@ function reached(session) {
|
|
|
269
273
|
/** The driver's `pace`: called after every page of the first layout. It stops the run where the host asked it to stop — by returning a promise the next call releases — and otherwise lets it go on without a pause. */
|
|
270
274
|
function pacer(session, progress) {
|
|
271
275
|
session.progress = progress;
|
|
276
|
+
if (session.cancelled)
|
|
277
|
+
return Promise.reject(LET_GO); // a run released while it measures stops at its next page
|
|
272
278
|
if (!reached(session))
|
|
273
279
|
return undefined;
|
|
274
280
|
return new Promise((resolve, reject) => {
|
|
@@ -324,8 +330,7 @@ async function takeIntake(relaxDpr) {
|
|
|
324
330
|
for (const [el, parent, next] of moved.toReversed())
|
|
325
331
|
parent?.insertBefore(el, next);
|
|
326
332
|
root.remove();
|
|
327
|
-
|
|
328
|
-
rim = null;
|
|
333
|
+
stopRim();
|
|
329
334
|
presented = null;
|
|
330
335
|
}
|
|
331
336
|
};
|
|
@@ -354,7 +359,7 @@ async function cutTo(el) {
|
|
|
354
359
|
if (!session)
|
|
355
360
|
return;
|
|
356
361
|
if (session.gate)
|
|
357
|
-
await
|
|
362
|
+
await handle.run({ composed: session.composed, until: el });
|
|
358
363
|
else
|
|
359
364
|
await session.finished;
|
|
360
365
|
}
|
|
@@ -407,7 +412,7 @@ function show(session, { account, index, total, from, to, resume }) {
|
|
|
407
412
|
/** The host's doors onto the presentation, seated on the handle after every presentation (*the engine answers the page*, 2026-09-04; beltype#19, beltype#20). A verb, never the index: the tokenization stays in the realm it is checked in, and a verb survives the parked horizon of the engine leaving this realm where an exposed index holding live DOM references would not. */
|
|
408
413
|
function seatDoors(blocks) {
|
|
409
414
|
const { root } = presented;
|
|
410
|
-
const h =
|
|
415
|
+
const h = handle;
|
|
411
416
|
h.pageOf = (el, charOffset = 0) => root.pageOf(el, charOffset);
|
|
412
417
|
// The windowing's doors (*only the pages near the reader are built*, 2026-10-12): the pages near the reader are built with the presentation and the rest as the reader nears them or the browser is idle, so a host about to read a page asks for it first. `reveal` builds the page that begins a source element and scrolls its drawn copy into view; `buildPage` builds one page; `buildAll` builds every page and is awaited by whoever reads them all (the printer, a gate); `built` says how many pages have their content.
|
|
413
418
|
h.reveal = (el, options) => root.reveal(el, options);
|
|
@@ -452,6 +457,8 @@ function seatDoors(blocks) {
|
|
|
452
457
|
* @returns what the console line reports; the presented pages stay live in the DOM. `pages` is how many are cut; `complete` whether that is the document's end. `resumed` names the pages cut again, from 1: `from` through `to`, with `shift` the pages the run added (or, negative, removed) and `kept` the prior pages after `to` left standing. `why` is a plain sentence, set exactly when `resumed` is `null`: the first run on a frame, no `resumeAt`, another mode, conditions that moved (and which part), a block count that moved, or a reference whose page number sent the run round again. `composition` is the composed mode's count (`null` in the default): `misses` the paragraphs composed, `hits` the ones found already composed, `blocks` their sum, and `laid` the top-level blocks cloned and measured to read their widths (the rest were answered by the cache whole).
|
|
453
458
|
*/
|
|
454
459
|
export function run(opts) {
|
|
460
|
+
if (released)
|
|
461
|
+
return Promise.reject(new Error('BelType/Cicero: this client was released; the next document loads its own'));
|
|
455
462
|
const call = runOnce(opts);
|
|
456
463
|
calls.add(call);
|
|
457
464
|
const answered = () => calls.delete(call);
|
|
@@ -543,12 +550,14 @@ async function runOnce({ composed = false, resumeAt = null, until = null, layer
|
|
|
543
550
|
...(target != null ? { pace: (progress) => pacer(session, progress) } : {})
|
|
544
551
|
});
|
|
545
552
|
live = session;
|
|
553
|
+
if (released)
|
|
554
|
+
session.cancelled = true; // called before the release, so the first page it cuts is its last
|
|
546
555
|
return settle(session, t0);
|
|
547
556
|
}
|
|
548
557
|
async function carryOn(session, until, t0) {
|
|
549
558
|
session.target = until;
|
|
550
559
|
if (reached(session))
|
|
551
|
-
return
|
|
560
|
+
return handle.last; // already cut as far as it is asked
|
|
552
561
|
dress(false);
|
|
553
562
|
presented.root.hold();
|
|
554
563
|
const settled = settle(session, t0);
|
|
@@ -569,6 +578,8 @@ async function settle(session, t0) {
|
|
|
569
578
|
catch (error) {
|
|
570
579
|
if (live === session)
|
|
571
580
|
live = null;
|
|
581
|
+
if (error === LET_GO && released && !session.partial && session.fresh)
|
|
582
|
+
presented?.undo(); // a first run released before it showed a page
|
|
572
583
|
if (error !== LET_GO) {
|
|
573
584
|
forcingMemo.clear(); // a run that threw filled it with reads no one vouched for
|
|
574
585
|
if (!session.partial && session.fresh)
|
|
@@ -596,11 +607,11 @@ async function settle(session, t0) {
|
|
|
596
607
|
edited.splice(0, session.taken);
|
|
597
608
|
session.taken = 0;
|
|
598
609
|
compositionCache.conditions = conditions;
|
|
599
|
-
|
|
600
|
-
|
|
610
|
+
handle.last = { ...answerOf(session, t0, progress, resumed), complete: false };
|
|
611
|
+
handle.ledger = progress.account;
|
|
601
612
|
console.info(`BelType/Cicero: ${k} pages cut so far, in ${Math.round(performance.now() - t0)} ms — the run is stopped at a page and goes on when asked`);
|
|
602
613
|
await reloadKnownFaces(); // the presentation re-created a layered frame's faces, and the next slice measures in them
|
|
603
|
-
return
|
|
614
|
+
return handle.last;
|
|
604
615
|
}
|
|
605
616
|
// Complete: the run reached the document's end.
|
|
606
617
|
const result0 = outcome.result;
|
|
@@ -633,9 +644,9 @@ async function settle(session, t0) {
|
|
|
633
644
|
session.taken = 0;
|
|
634
645
|
compositionCache.conditions = conditions; // the reading after the presentation, which is the one the next call sees (the first call's was taken before there was any litter to leave out)
|
|
635
646
|
// A host embedding this document (the editing tools' preview iframe, same-origin) reads its own render condition here rather than the devtools console nobody opens — set before returning, on the same handle the bootstrap already holds.
|
|
636
|
-
|
|
647
|
+
handle.last = result;
|
|
637
648
|
// The run's Ledger itself, beside the summary and not inside it: `last` is read across frames and serialised by hosts, and the account holds live `box` pointers that no host should be handed by accident. A venue test reads it in-page to compare two runs entry by entry (*the run resumes from an entry*).
|
|
638
|
-
|
|
649
|
+
handle.ledger = account;
|
|
639
650
|
if (live === session)
|
|
640
651
|
live = null;
|
|
641
652
|
// The presentation re-created a layered frame's faces, and a host reads the pages next: their loads are STARTED here and not awaited, so they are `loading` (and `document.fonts.ready` waits for them) by the time the call answers. Awaiting them put a task between the presentation and the answer, and a host that puts the caret back when the run answers (the studio experiment) lost a keystroke typed in that gap: the browser had no caret to type at, and fired no beforeinput.
|
|
@@ -654,5 +665,36 @@ function answerOf(session, t0, progress, resumed) {
|
|
|
654
665
|
composition: session.composed ? { blocks: compositionCache.hits + compositionCache.misses, hits: compositionCache.hits, misses: compositionCache.misses, laid: compositionCache.laid } : null
|
|
655
666
|
};
|
|
656
667
|
}
|
|
657
|
-
|
|
668
|
+
// The shell's bootstrap reaches the engine through this handle. Setting it is the module's only load-time effect — rendering waits to be asked, because the bootstrap knows when the document has stopped changing and this module does not. `pageOf` is seated by `run()` rather than here: before a presentation there is no page to answer, and a stub that answered `null` for that reason would be indistinguishable from the refusals that mean something.
|
|
669
|
+
// `compositionCache` is the harness's door and the product's none: a venue test clears it to ask for a cold composition in a frame that has composed before, which is the only way a resumed run can be compared with a fresh one that did not borrow its records.
|
|
670
|
+
// `forcingMemo` and `forgetJudgedSheets` are the same kind of door, for the same comparison: a venue test empties them to ask what a run costs, and what it cuts, when it borrows nothing from the runs before it (*the forced-break index is kept across runs*).
|
|
671
|
+
// `present` is the host's say over what a copy of an element is (*a copy may be presented*, `src/presenters.js`): `present(selector, make)`, registered before the first run. One registered after a run changes the conditions key, so the next call is a whole run on a presented frame — nothing a presenter touched is reused — and a run stopped at a page goes on under the presenters it began with, its pages already built keeping the elements they were built with.
|
|
672
|
+
// `measured` is the same kind of door again (*a box announces its own size*): a venue test asks what height the last run measured for a top-level box, the number the `size` kind compares a presented box's observed size with.
|
|
673
|
+
// `table` is the geometry the intake captured from the author's `@page` rules, margin boxes and their `content` included, or null before an intake: the root clears every margin box's `content` from the CSSOM when it presents, so a second host in the same document, whose pages are not the root's, cannot read a table of its own from the sheets and takes this one (see CHANGELOG 2026-10-28).
|
|
674
|
+
/**
|
|
675
|
+
* Let go of everything this client registered in the page, so a document that replaces it (`document.open()` keeps the `Window`, its `FontFaceSet` and its listeners) is not answered by this one's.
|
|
676
|
+
*
|
|
677
|
+
* It removes the `loadingdone` listener, stops the rim, drops the batch of late truth waiting to be answered and deletes the handle from the window, all before the first `await`. A run stopped at a page is let go and its pages stay on screen. A run under way with a pace (`until`) rejects with the let-go error at its next page, and a first run that has shown nothing puts the document back as it found it; one without a pace finishes and presents. When nothing is under way, the promise resolves, after the root's idle build, observer and window listeners and the facsimile's observer have been let go. A `run()` made afterwards is rejected by name. Calling it again is a no-op.
|
|
678
|
+
*
|
|
679
|
+
* @remarks **Host obligations.** The custom-element registry belongs to the window and is not released: the next document's `<beltype-root>`, `<beltype-page>` and PDF element are built from the classes this module defined, which keep no state about the document they were defined for. The handle is gone, and the next client module runs only under a URL the window has not imported (measured 2026-11-03: the same URL after `document.open()` does not run again, a keyed one does, and a second `customElements.define` of a name throws, which the three definitions guard against). The host keys the URL.
|
|
680
|
+
* @see SPEC.md Article XII.7
|
|
681
|
+
*/
|
|
682
|
+
export async function release() {
|
|
683
|
+
if (released)
|
|
684
|
+
return;
|
|
685
|
+
released = true;
|
|
686
|
+
fonts.removeEventListener('loadingdone', countFaces);
|
|
687
|
+
stopRim();
|
|
688
|
+
batch = null;
|
|
689
|
+
if (window.__beltypeCicero === handle)
|
|
690
|
+
delete window.__beltypeCicero;
|
|
691
|
+
await Promise.allSettled([...calls]); // a call made before the release has no session yet: it gets one, cancelled, in `runOnce`
|
|
692
|
+
if (live)
|
|
693
|
+
await letGo();
|
|
694
|
+
stopRim(); // the intake of a call that was under way made one after the first stop
|
|
695
|
+
presented?.root.dispose(); // last, so a presentation that finished above does not arm what it just let go
|
|
696
|
+
facsimile?.dispose();
|
|
697
|
+
}
|
|
698
|
+
const handle = { run, present, release,
|
|
658
699
|
settled, compositionCache, forcingMemo, forgetJudgedSheets: () => { judgedSheets = new WeakMap(); }, measured: (el) => previous?.result.heights?.get(el), get table() { return presented?.table ?? null; } };
|
|
700
|
+
window.__beltypeCicero = handle;
|
package/dist/seam/facsimile.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export declare function locate(source: Element): {
|
|
|
38
38
|
* @remarks The element has no child until {@link draw} gives it its canvas (*a facsimile's copy holds its box off screen*, CHANGELOG 2026-10-28). An `<object>` with a child and no `data` decides at its first layout to render the child, which is a reattach of its layout tree, and on a page the browser skips (`content-visibility: auto`, *a page is contained*) a read from script lays the subtree out for the length of the read, drops the reattach when it puts the subtree back, and leaves the copy with no box at all. Measured 2026-10-28: the same copy with no child keeps its box on every read.
|
|
39
39
|
*/
|
|
40
40
|
export declare function make(source: Element): HTMLObjectElement;
|
|
41
|
+
/** Let go of the observer, which the next element to connect makes again: the element's class is the window's and outlives the client that defined it. */
|
|
42
|
+
export declare function dispose(): void;
|
|
41
43
|
/**
|
|
42
44
|
* Define the element, read the size of every page the document embeds and register the presenter — before the first run (the intake calls this first, and awaits it), because a presenter is a condition of every copy and a registration after a run is a whole run.
|
|
43
45
|
*
|
package/dist/seam/facsimile.js
CHANGED
|
@@ -165,6 +165,8 @@ const watcher = () => seen ??= new IntersectionObserver((entries) => {
|
|
|
165
165
|
draw(entry.target);
|
|
166
166
|
}
|
|
167
167
|
}, { rootMargin: '300px' });
|
|
168
|
+
/** Let go of the observer, which the next element to connect makes again: the element's class is the window's and outlives the client that defined it. */
|
|
169
|
+
export function dispose() { seen?.disconnect(); seen = null; }
|
|
168
170
|
/** The drawn page, at the box's width. The canvas takes the page's own ratio and the CSS says how it sits (`object-fit: contain`, in the box the author sized). */
|
|
169
171
|
async function draw(el) {
|
|
170
172
|
if (!el.isConnected || el.closest('#__beltype-measure'))
|
package/dist/seam/index.js
CHANGED
|
@@ -26,11 +26,13 @@ import { WORDS_RULES } from '../src/data-beltype.js';
|
|
|
26
26
|
*
|
|
27
27
|
* The value is the addendum's own 1.65 rather than an invention, so on the one surface where it can actually cascade — the HTML lane, which ships no addendum — the engine's provision and BelType's default agree. Below `body` it is inert: the addendum's own `body` rule wins by proximity.
|
|
28
28
|
*
|
|
29
|
+
* It is a floor, not an override, so it is written at zero specificity: `:where(:root)` still beats the browser's `normal`, which no stylesheet sets, and loses to any author rule on the root, a plain `html { font: 10pt/13.4pt serif }` among them. As `:root` (0,1,0) it beat that rule's (0,0,1) silently, and five of the six specimens printed at 1.65 instead of their declared leading (trattato: 220 pages for 189; Codeberg issue 4, 2026-11-07).
|
|
30
|
+
*
|
|
29
31
|
* It is a literal here, and not a line in the linked sheet beside it, because a conformance provision that can 404 is not a provision. A missing sheet — a table entry lost in a refactor, a server restarted mid-edit — would arrive as `I5 (DPR ruling): explicit line-height required`, a law refusing on a typographic claim when the real fault is a missing file, sending the reader to their own stylesheet. Cicero's refusals are load-bearing because they are true; one that names the wrong thing spends that.
|
|
30
32
|
*
|
|
31
33
|
* Recorded as a beta limitation, because the check the law performs is at the root and this satisfies it there: a document whose deeper typography still relies on `normal` is outside what *render-identical conditions* (I5) can see.
|
|
32
34
|
*/
|
|
33
|
-
const I5_PROVISION = ':root { line-height: 1.65; }';
|
|
35
|
+
const I5_PROVISION = ':where(:root) { line-height: 1.65; }';
|
|
34
36
|
/**
|
|
35
37
|
* What the document says about a PDF `<object>` before the engine has met it (*the embedded PDF*, 2026-10-14; `seam/facsimile.js`).
|
|
36
38
|
*
|
package/dist/src/capture.d.ts
CHANGED
|
@@ -137,7 +137,14 @@ export type ReachContext = {
|
|
|
137
137
|
/** Whether any top-level box is not the one that stood in its place before. */
|
|
138
138
|
changed: boolean;
|
|
139
139
|
};
|
|
140
|
-
/**
|
|
140
|
+
/** Is this `size` an orientation keyword and nothing else? Then it turns the sheet the cascade already gave ({@link sizeToPx}). */
|
|
141
|
+
export declare const isOrientationOnly: (tokens: string[]) => boolean;
|
|
142
|
+
/**
|
|
143
|
+
* A `size` descriptor's tokens → [widthPx, heightPx]. Handles lengths, named sizes, and orientation keywords. A `size` is resolved before either page dimension is known, so its lengths get a null percentage base, and a percentage refuses by name (css-page-3 §7.1).
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* An orientation keyword turns whatever sheet stands beside it: `landscape` puts the longer side across, `portrait` down, for a named size and for a pair of lengths alike. §7.1 says a keyword alone means the UA's default sheet turned that way; this engine pins geometry (I5) and has no default sheet, so `pageFor` hands the keyword the sheet the cascade's less specific rule gave (the WPT print reftests' `@page :first { size: portrait }` on their 5 in by 3 in default), and a keyword with no such sheet refuses by name (Codeberg issue 10; until 2026-11-07 it refused as the length `undefined`).
|
|
147
|
+
*/
|
|
141
148
|
export declare function sizeToPx(tokens: string[], resolve: Resolve): [Px, Px];
|
|
142
149
|
/**
|
|
143
150
|
* A declared border width takes room only where the same side's style draws a border (*a page box has a border and a padding*, 2026-09-07).
|
|
@@ -180,6 +187,15 @@ export declare function assertPageArea({ contentW, contentH }: {
|
|
|
180
187
|
contentW: Px;
|
|
181
188
|
contentH: Px;
|
|
182
189
|
}, where: string): void;
|
|
190
|
+
/**
|
|
191
|
+
* Every `@page` rule the document wears, wherever it stands: at a sheet's top, or inside an `@layer`, a `@supports` or an `@media` that holds, or an imported sheet. `layered` loses to `unlayered`, as the cascade has it, whatever their order.
|
|
192
|
+
*
|
|
193
|
+
* @remarks ponytail: the layers keep their document order, not their declared order; a page rule that must win over another layer is unlayered, as BelType's own is.
|
|
194
|
+
*/
|
|
195
|
+
export declare function pageRulesOf(doc: Document): {
|
|
196
|
+
layered: CSSPageRule[];
|
|
197
|
+
unlayered: CSSPageRule[];
|
|
198
|
+
};
|
|
183
199
|
/**
|
|
184
200
|
* Clear every author @page margin-box's own `content` declaration in the live CSSOM — never its `size`/`margin`, never anything outside a margin-at-rule. Called once the geometry table above has already read what it needs from these rules (the second channel a named page cannot close).
|
|
185
201
|
*
|
package/dist/src/capture.js
CHANGED
|
@@ -531,11 +531,12 @@ function createLengthResolver(doc) {
|
|
|
531
531
|
const fast = absoluteToPx(raw);
|
|
532
532
|
if (fast !== null)
|
|
533
533
|
return fast;
|
|
534
|
-
|
|
535
|
-
|
|
534
|
+
// A `size` is resolved before either page dimension exists, so it has no percentage base; css-page-3 §7.1 gives `size` no percentage values at all. Every other length the browser resolves is measured as a margin's is: an `em` resolves against the root's font, which the probe's wrapper wears (Codeberg issue 10; until 2026-11-07 every relative unit refused here).
|
|
535
|
+
if (basePx === null && raw.includes('%'))
|
|
536
|
+
throw refuse(raw, where, 'a `size` descriptor is resolved before either page dimension exists, and css-page-3 §7.1 gives `size` no percentage values at all');
|
|
536
537
|
if (raw === 'auto')
|
|
537
538
|
throw refuse(raw, where, '`auto` on a page margin means UA-determined, and the probe would read it as a plain 0 — a number this engine would then lay out against as though the author had written one');
|
|
538
|
-
const px = measured(raw, basePx, prop);
|
|
539
|
+
const px = measured(raw, basePx ?? 0, prop);
|
|
539
540
|
if (px === null)
|
|
540
541
|
throw refuse(raw, where, 'the browser resolves it to nothing — an unknown unit, a malformed value, or a custom property that is never declared');
|
|
541
542
|
return px;
|
|
@@ -560,11 +561,20 @@ const SIZE_NAMES = {
|
|
|
560
561
|
legal: [215.9, 355.6], // css-page-3 legal 8.5 × 14in; Chromium 215.90 × 355.60mm
|
|
561
562
|
ledger: [279.4, 431.8] // css-page-3 ledger 11 × 17in; Chromium 279.40 × 431.80mm
|
|
562
563
|
};
|
|
563
|
-
/**
|
|
564
|
+
/** css-page-3 §7.1's two orientation keywords. */
|
|
565
|
+
const ORIENTATIONS = new Set(['portrait', 'landscape']);
|
|
566
|
+
/** Is this `size` an orientation keyword and nothing else? Then it turns the sheet the cascade already gave ({@link sizeToPx}). */
|
|
567
|
+
export const isOrientationOnly = (tokens) => tokens.length === 1 && ORIENTATIONS.has(tokens[0].toLowerCase());
|
|
568
|
+
/**
|
|
569
|
+
* A `size` descriptor's tokens → [widthPx, heightPx]. Handles lengths, named sizes, and orientation keywords. A `size` is resolved before either page dimension is known, so its lengths get a null percentage base, and a percentage refuses by name (css-page-3 §7.1).
|
|
570
|
+
*
|
|
571
|
+
* @remarks
|
|
572
|
+
* An orientation keyword turns whatever sheet stands beside it: `landscape` puts the longer side across, `portrait` down, for a named size and for a pair of lengths alike. §7.1 says a keyword alone means the UA's default sheet turned that way; this engine pins geometry (I5) and has no default sheet, so `pageFor` hands the keyword the sheet the cascade's less specific rule gave (the WPT print reftests' `@page :first { size: portrait }` on their 5 in by 3 in default), and a keyword with no such sheet refuses by name (Codeberg issue 10; until 2026-11-07 it refused as the length `undefined`).
|
|
573
|
+
*/
|
|
564
574
|
export function sizeToPx(tokens, resolve) {
|
|
565
|
-
const t = tokens.map((s) => s.toLowerCase())
|
|
566
|
-
const
|
|
567
|
-
const rest = t.filter((s) => s
|
|
575
|
+
const t = tokens.map((s) => s.toLowerCase());
|
|
576
|
+
const orientation = t.find((s) => ORIENTATIONS.has(s)) ?? null;
|
|
577
|
+
const rest = t.filter((s) => !ORIENTATIONS.has(s));
|
|
568
578
|
const where = 'a `size` descriptor';
|
|
569
579
|
let w;
|
|
570
580
|
let h;
|
|
@@ -574,10 +584,33 @@ export function sizeToPx(tokens, resolve) {
|
|
|
574
584
|
else if (rest.length === 1) {
|
|
575
585
|
w = h = resolve(rest[0], null, where);
|
|
576
586
|
}
|
|
587
|
+
else if (rest.length === 0) {
|
|
588
|
+
throw refuse(orientation ?? '', where, 'an orientation keyword alone means the UA\'s default sheet turned that way, and this engine has none: declare the sheet in a less specific @page rule, or beside the keyword');
|
|
589
|
+
}
|
|
577
590
|
else {
|
|
578
591
|
[w, h] = [resolve(rest[0], null, where), resolve(rest[1], null, where)];
|
|
579
592
|
}
|
|
580
|
-
|
|
593
|
+
if (orientation === 'landscape')
|
|
594
|
+
return [Math.max(w, h), Math.min(w, h)];
|
|
595
|
+
if (orientation === 'portrait')
|
|
596
|
+
return [Math.min(w, h), Math.max(w, h)];
|
|
597
|
+
return [w, h];
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* The root's custom properties substituted into a page-context value (css-page-3 §4.1: the page context inherits from the root element, so a `var()` there reads the root's value). The probe already inherits them for a longhand it seats, but a shorthand goes through `expandBox` before any probe, and `--margin: 20px 40px` must be split before it is seated (Codeberg issue 10, `page-context-inherits-from-root-custom-property`). A property the root does not declare takes the `var()`'s fallback, or stays as written for the probe to refuse by name.
|
|
601
|
+
*/
|
|
602
|
+
function substituteRootVars(doc, text) {
|
|
603
|
+
if (!text.includes('var('))
|
|
604
|
+
return text;
|
|
605
|
+
const root = doc.defaultView.getComputedStyle(doc.documentElement);
|
|
606
|
+
let out = text;
|
|
607
|
+
for (let i = 0; i < 8 && out.includes('var('); i++) {
|
|
608
|
+
const next = out.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*))?\)/g, (whole, name, fallback) => root.getPropertyValue(name).trim() || fallback?.trim() || whole);
|
|
609
|
+
if (next === out)
|
|
610
|
+
break;
|
|
611
|
+
out = next;
|
|
612
|
+
}
|
|
613
|
+
return out;
|
|
581
614
|
}
|
|
582
615
|
/**
|
|
583
616
|
* What Cicero owns: the one criterion, and it is deliberately not a CSS opinion.
|
|
@@ -600,6 +633,12 @@ const AREA_PREFIXES = ['border', 'padding'];
|
|
|
600
633
|
function ownedByCicero(prop) {
|
|
601
634
|
return OWNED_EXACTLY.has(prop) || OWNED_PREFIXES.some((p) => prop.startsWith(p));
|
|
602
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* What the Chase owns on a margin cell, which is less than on the page host: a cell's `margin-*`, `border-*` and `padding-*` are the box's own (css-page-3 §5.3.2 distributes the boxes' outer widths, padding and border included, and `chase.js`'s `layoutBands` measures them on the cell), where the page box's are the geometry the capture models and the content area wears. Until 2026-11-07 the host's filter was applied to the boxes too, and `@top-left { padding-bottom: 5mm }` reached no cell (Codeberg issue 3). The `width` family stays withheld: §5.3.2's answer for a box that declares one is not built, and the cell's grid seat is the Chase's.
|
|
638
|
+
*/
|
|
639
|
+
function ownedOnCell(prop) {
|
|
640
|
+
return OWNED_EXACTLY.has(prop) || prop.startsWith('grid-template');
|
|
641
|
+
}
|
|
603
642
|
/** One declaration block's `border-*` and `padding-*` triples, in the block's own order — the page box's border and padding, which the Chase writes on the content area. Order and `!important` carried for the same reason `declarationsOf` carries them: they are the cascade. */
|
|
604
643
|
function areaDeclarationsOf(style) {
|
|
605
644
|
const out = [];
|
|
@@ -610,11 +649,11 @@ function areaDeclarationsOf(style) {
|
|
|
610
649
|
}
|
|
611
650
|
return out;
|
|
612
651
|
}
|
|
613
|
-
/** One declaration block as `[property, value, priority]` triples, in the block's own order, minus what Cicero owns. Order and `!important` are carried because they are the cascade: the triples are re-emitted into one rule, where later wins and important beats normal exactly as the author wrote them. */
|
|
614
|
-
function declarationsOf(style) {
|
|
652
|
+
/** One declaration block as `[property, value, priority]` triples, in the block's own order, minus what Cicero owns — on the page host by default, on a margin cell where `owned` is {@link ownedOnCell}. Order and `!important` are carried because they are the cascade: the triples are re-emitted into one rule, where later wins and important beats normal exactly as the author wrote them. */
|
|
653
|
+
function declarationsOf(style, owned = ownedByCicero) {
|
|
615
654
|
const out = [];
|
|
616
655
|
for (const prop of style ?? []) {
|
|
617
|
-
if (
|
|
656
|
+
if (owned(prop))
|
|
618
657
|
continue;
|
|
619
658
|
out.push([prop, style.getPropertyValue(prop), style.getPropertyPriority(prop)]);
|
|
620
659
|
}
|
|
@@ -655,7 +694,7 @@ function marginBoxStylesOf(rule) {
|
|
|
655
694
|
const name = (child.name ?? child.selectorText ?? '').replace(/^@/, '');
|
|
656
695
|
if (!MARGIN_BOXES.includes(name))
|
|
657
696
|
continue;
|
|
658
|
-
const decls = declarationsOf(child.style).filter(([prop]) => prop !== 'content');
|
|
697
|
+
const decls = declarationsOf(child.style, ownedOnCell).filter(([prop]) => prop !== 'content');
|
|
659
698
|
if (decls.length)
|
|
660
699
|
out[name] = decls;
|
|
661
700
|
}
|
|
@@ -796,27 +835,57 @@ function marginBoxesOf(rule) {
|
|
|
796
835
|
return out;
|
|
797
836
|
}
|
|
798
837
|
/**
|
|
799
|
-
*
|
|
838
|
+
* Every `@page` rule the document wears, wherever it stands: at a sheet's top, or inside an `@layer`, a `@supports` or an `@media` that holds, or an imported sheet. `layered` loses to `unlayered`, as the cascade has it, whatever their order.
|
|
800
839
|
*
|
|
801
|
-
*
|
|
840
|
+
* @remarks ponytail: the layers keep their document order, not their declared order; a page rule that must win over another layer is unlayered, as BelType's own is.
|
|
802
841
|
*/
|
|
803
|
-
export function
|
|
804
|
-
|
|
805
|
-
|
|
842
|
+
export function pageRulesOf(doc) {
|
|
843
|
+
const win = doc.defaultView;
|
|
844
|
+
const found = { layered: [], unlayered: [] };
|
|
845
|
+
const rulesOf = (sheet) => {
|
|
806
846
|
try {
|
|
807
|
-
|
|
847
|
+
return sheet.cssRules;
|
|
808
848
|
}
|
|
809
849
|
catch {
|
|
810
|
-
|
|
811
|
-
} // cross-origin sheet: not ours to
|
|
850
|
+
return [];
|
|
851
|
+
} // cross-origin sheet: not ours to read
|
|
852
|
+
};
|
|
853
|
+
const walk = (rules, inLayer) => {
|
|
812
854
|
for (const rule of rules) {
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
855
|
+
if (rule instanceof win.CSSPageRule)
|
|
856
|
+
(inLayer ? found.layered : found.unlayered).push(rule);
|
|
857
|
+
else if (rule.constructor?.name === 'CSSLayerBlockRule')
|
|
858
|
+
walk(rule.cssRules, true);
|
|
859
|
+
else if (rule.constructor?.name === 'CSSSupportsRule') {
|
|
860
|
+
if (win.CSS?.supports?.(rule.conditionText))
|
|
861
|
+
walk(rule.cssRules, inLayer);
|
|
862
|
+
}
|
|
863
|
+
else if (rule.constructor?.name === 'CSSMediaRule') {
|
|
864
|
+
if (win.matchMedia(rule.media.mediaText).matches)
|
|
865
|
+
walk(rule.cssRules, inLayer);
|
|
819
866
|
}
|
|
867
|
+
else if (rule.constructor?.name === 'CSSImportRule' && rule.styleSheet)
|
|
868
|
+
walk(rulesOf(rule.styleSheet), inLayer || rule.layerName != null);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
for (const sheet of styleSheetsOf(doc))
|
|
872
|
+
walk(rulesOf(sheet), false);
|
|
873
|
+
return found;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Clear every author @page margin-box's own `content` declaration in the live CSSOM — never its `size`/`margin`, never anything outside a margin-at-rule. Called once the geometry table above has already read what it needs from these rules (the second channel a named page cannot close).
|
|
877
|
+
*
|
|
878
|
+
* Margin-box chrome is delivered exclusively as a shadow-DOM literal (chase.js's own note under *content is one document*, I6: "it does not live once, so it cannot live twice"), but the author's own @page rules stay linked in the document throughout — their non-page rules keep styling the light-DOM content, so the whole stylesheet can never simply be disabled. Once the press sheet (root.js) renames every `<beltype-page>` to a synthetic geometry name, no author rule with a page-name selector (`@page title`, `@page :left`, …) can be name-matched by the browser's own native cascade any more — only the bare, unnamed base rule still matches every page unconditionally, becoming a second, uncontrolled channel that paints the very same chrome the shadow DOM already delivers correctly. `cases/17-the-borrowed-dress` found this the hard way: a named page's `content: normal` override was honored inside the shadow DOM (confirmed by direct inspection) while the printed page still showed the un-overridden base string, painted by this exact channel. Clearing `content` closes it by construction, for every page, independent of the precise cascade mechanism that let it through.
|
|
879
|
+
*/
|
|
880
|
+
export function neutralizeMarginBoxes(doc) {
|
|
881
|
+
const { layered, unlayered } = pageRulesOf(doc);
|
|
882
|
+
for (const rule of [...layered, ...unlayered]) {
|
|
883
|
+
if (!rule.cssRules)
|
|
884
|
+
continue;
|
|
885
|
+
for (const child of rule.cssRules) {
|
|
886
|
+
const name = (child.name ?? child.selectorText ?? '').replace(/^@/, '');
|
|
887
|
+
if (MARGIN_BOXES.includes(name))
|
|
888
|
+
child.style?.removeProperty('content');
|
|
820
889
|
}
|
|
821
890
|
}
|
|
822
891
|
}
|
|
@@ -943,47 +1012,37 @@ export function registerGcpmProperties(win) {
|
|
|
943
1012
|
*/
|
|
944
1013
|
export function readGeometryTable(doc) {
|
|
945
1014
|
const rules = [];
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
area: areaDeclarationsOf(rule.style),
|
|
978
|
-
boxStyles: marginBoxStylesOf(rule),
|
|
979
|
-
margins: perSide('margin'),
|
|
980
|
-
// A border width's used value is zero unless a style is set, so the style is read beside the width and the two are accumulated separately — a `@page { border: 5px }` declares a width and no style, and takes no room at all.
|
|
981
|
-
borderWidths: perSide('border', '-width'),
|
|
982
|
-
borderStyles: perSide('border', '-style'),
|
|
983
|
-
paddings: perSide('padding'),
|
|
984
|
-
marginBoxes: marginBoxesOf(rule)
|
|
985
|
-
});
|
|
986
|
-
}
|
|
1015
|
+
const { layered, unlayered } = pageRulesOf(doc);
|
|
1016
|
+
for (const rule of [...layered, ...unlayered]) {
|
|
1017
|
+
const sel = parseSelector(rule.selectorText);
|
|
1018
|
+
// The rule's own declaration block, never its serialized text: `rule.cssText` carries the nested margin boxes inside it, so a regex over it credits a folio's `margin-top` to the page and reads a running head's `font-size` as the page's `size` (a page rule reads its own declarations; the arithmetic is on the checklist, the case is `15-the-margin-box`). `size` is a descriptor rather than a property and is exposed here anyway — measured, not assumed.
|
|
1019
|
+
const declared = (prop) => rule.style?.getPropertyValue(prop).trim() || null;
|
|
1020
|
+
const size = declared('size')?.split(/\s+/) ?? null;
|
|
1021
|
+
// Longhands only: the CSSOM has already expanded whatever shorthand was written, and re-expanding it here would re-implement the browser's own work.
|
|
1022
|
+
//
|
|
1023
|
+
// Except where it has expanded nothing. A shorthand carrying a `var()` is held in pending substitution — the block answers `margin` with the author's text and every one of `margin-top`…`margin-left` with the empty string — so reading longhands alone reports no declaration at all, and the page lays out against a default. That is not a plausible failure: measured 2026-09-09, `@page { margin: var(--outer) }` with `--outer: 12mm` composed against BelType's own inlined default of 20mm and against 0 in a bare document, both silently, and the case is `33-the-shorthand-the-cssom-cannot-expand`. So the shorthand is consulted only when all four longhands are empty, which is exactly the pending-substitution state and nothing else — the browser expands every shorthand it can, so a non-empty longhand means there is nothing here to do. `expandBox` hands back the author's raw values, which the resolver then seats and measures per side exactly as it does a longhand's; nothing about lengths is decided here.
|
|
1024
|
+
//
|
|
1025
|
+
// Its reach is one level: `border: 5px var(--style)` leaves `border-width` empty too, and that stays unread. Naming it rather than widening the door — a shorthand of shorthands is a second expansion with a second grammar, and no document has asked for it.
|
|
1026
|
+
const perSide = (prefix, suffix = '') => {
|
|
1027
|
+
const sides = { top: declared(`${prefix}-top${suffix}`), right: declared(`${prefix}-right${suffix}`), bottom: declared(`${prefix}-bottom${suffix}`), left: declared(`${prefix}-left${suffix}`) };
|
|
1028
|
+
if (sides.top || sides.right || sides.bottom || sides.left)
|
|
1029
|
+
return sides;
|
|
1030
|
+
const shorthand = declared(`${prefix}${suffix}`);
|
|
1031
|
+
return (shorthand && expandBox(substituteRootVars(doc, shorthand))) || sides;
|
|
1032
|
+
};
|
|
1033
|
+
rules.push({
|
|
1034
|
+
sel,
|
|
1035
|
+
size,
|
|
1036
|
+
style: declarationsOf(rule.style),
|
|
1037
|
+
area: areaDeclarationsOf(rule.style),
|
|
1038
|
+
boxStyles: marginBoxStylesOf(rule),
|
|
1039
|
+
margins: perSide('margin'),
|
|
1040
|
+
// A border width's used value is zero unless a style is set, so the style is read beside the width and the two are accumulated separately — a `@page { border: 5px }` declares a width and no style, and takes no room at all.
|
|
1041
|
+
borderWidths: perSide('border', '-width'),
|
|
1042
|
+
borderStyles: perSide('border', '-style'),
|
|
1043
|
+
paddings: perSide('padding'),
|
|
1044
|
+
marginBoxes: marginBoxesOf(rule)
|
|
1045
|
+
});
|
|
987
1046
|
}
|
|
988
1047
|
if (!rules.length)
|
|
989
1048
|
throw new Error('no @page rule found — the corpus and the venue always pin geometry (I5)');
|
|
@@ -1021,7 +1080,7 @@ export function readGeometryTable(doc) {
|
|
|
1021
1080
|
const pageStyle = { host: [], cell: [], area: [], boxes: {} };
|
|
1022
1081
|
for (const r of applicable) {
|
|
1023
1082
|
if (r.size)
|
|
1024
|
-
size = r.size;
|
|
1083
|
+
size = isOrientationOnly(r.size) && size ? [...size.filter((t) => !ORIENTATIONS.has(t.toLowerCase())), ...r.size] : r.size; // a keyword alone turns the sheet the cascade gave (`sizeToPx`)
|
|
1025
1084
|
for (const side of SIDES) {
|
|
1026
1085
|
if (r.margins[side] != null)
|
|
1027
1086
|
margins[side] = r.margins[side];
|
package/dist/src/chase.js
CHANGED
|
@@ -153,6 +153,7 @@ export class BeltypePage extends HTMLElement {
|
|
|
153
153
|
for (const [band, row] of [['top', 1], ['bottom', 5]]) {
|
|
154
154
|
const el = doc.createElement('div');
|
|
155
155
|
el.className = 'band';
|
|
156
|
+
el.setAttribute('aria-hidden', 'true'); // pagination furniture is never read (*an artifact is never read*)
|
|
156
157
|
el.dataset.band = band;
|
|
157
158
|
el.style.gridArea = `${row} / 2 / ${row + 1} / 5`;
|
|
158
159
|
this.#bands[band] = el;
|
|
@@ -161,6 +162,7 @@ export class BeltypePage extends HTMLElement {
|
|
|
161
162
|
for (const [name, row, col] of MARGIN_CELLS) {
|
|
162
163
|
const cell = doc.createElement('div');
|
|
163
164
|
cell.className = 'margin-cell';
|
|
165
|
+
cell.setAttribute('aria-hidden', 'true');
|
|
164
166
|
cell.dataset.box = name;
|
|
165
167
|
const band = Object.keys(HORIZONTAL_BANDS).find((b) => HORIZONTAL_BANDS[b].includes(name));
|
|
166
168
|
if (band) {
|