@quario/layout 0.2.0 → 0.3.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/CHANGELOG.md +45 -0
- package/lib/canvas.js +20 -101
- package/lib/image.js +1 -1
- package/lib/index.d.ts +10 -2
- package/lib/index.js +18 -12
- package/lib/layout.js +275 -115
- package/lib/page.js +40 -2
- package/lib/paint.js +30 -7
- package/lib/settings.js +88 -0
- package/lib/style.js +1 -2
- package/lib/text.js +5 -5
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-09-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The text join reads a cell's own `currency` code, ahead of the instance's
|
|
15
|
+
default, so every target built on this package presents a per-cell
|
|
16
|
+
denomination.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **A group instance that renders nothing no longer takes up space.** A group
|
|
21
|
+
whose header and footer items all resolve `visible: false`, with nothing
|
|
22
|
+
visible under it either, used to open the same half-line gap as any other
|
|
23
|
+
instance and could push the content after it onto a new page. A run of them
|
|
24
|
+
spaced whatever followed by a half-line each, so collapsing a level left the
|
|
25
|
+
rows above it at uneven distances. Such an instance now occupies nothing at
|
|
26
|
+
all and adds no PDF bookmark, which is what makes a group collapsible while
|
|
27
|
+
its rows stay in the aggregates. A group that declares `break: "page"` or
|
|
28
|
+
`reset: "page"` still starts its page either way. Every document with such a
|
|
29
|
+
group renders slightly shorter than it did.
|
|
30
|
+
|
|
31
|
+
- **Numbers presented through `format` now show a fixed two fraction digits,
|
|
32
|
+
matching every other target** — `1,000.00` where `1,000` was rendered,
|
|
33
|
+
`21.00%` where `21%` was, and a currency's own minor units in place of a
|
|
34
|
+
universal two. Because this package presents text and then **measures** it,
|
|
35
|
+
a formatted cell is now up to three characters wider than it was: a line
|
|
36
|
+
that just fitted can wrap, which can move a page break in a PDF, the viewer,
|
|
37
|
+
or an editor preview. Nothing else about wrapping changed.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **An image failure now names the item that asked for the bytes.** A file too
|
|
42
|
+
short to carry a size failed saying only that the size could not be read,
|
|
43
|
+
naming no item, so a report with two pictures gave no way to tell which one
|
|
44
|
+
was bad. The message is now prefixed with the item's `source` path, as every
|
|
45
|
+
other render error is.
|
|
46
|
+
|
|
47
|
+
- **An image the browser cannot decode no longer costs the whole page.** A
|
|
48
|
+
PNG or JPEG whose pixel data is corrupt past the size in its header lays out
|
|
49
|
+
like any other — the size is all that is read of it — and used to throw out
|
|
50
|
+
of `paint()` before its first draw op, leaving the page blank: no white
|
|
51
|
+
fill, none of the other content, and no licence marking. The image is now
|
|
52
|
+
drawn as nothing and the page is drawn around it, so what a bad image costs
|
|
53
|
+
is the image.
|
|
54
|
+
|
|
10
55
|
## [0.2.0] - 2026-09-05
|
|
11
56
|
|
|
12
57
|
### Added
|
package/lib/canvas.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The drawing surface. Every mark the band flow makes goes through here, so
|
|
3
|
-
* nothing above this module knows
|
|
3
|
+
* nothing above this module knows how a page is drawn on. What a page *is* —
|
|
4
|
+
* its box, and the content box a margin leaves — is `page.js`'s `Frame`, which
|
|
5
|
+
* arrives here already derived: nothing below that module works one out. What
|
|
6
|
+
* this one owns is what becomes of it after — `blank` spreads it flat onto the
|
|
7
|
+
* canvas and `adopt` narrows `top`/`bottom` in place, which is why `layout.js`
|
|
8
|
+
* re-derives the untouched page frame it hangs a header from.
|
|
4
9
|
*
|
|
5
10
|
* Two adapters satisfy one interface. `listing` records marks onto the display
|
|
6
11
|
* list — pages of ops and hit boxes, which the painters consume; `measuring`
|
|
@@ -18,72 +23,19 @@
|
|
|
18
23
|
* it lands: a list coordinate is measured from the page's top-left corner,
|
|
19
24
|
* `y` descending, the way a screen reads. Points throughout.
|
|
20
25
|
*/
|
|
21
|
-
import { baseSans
|
|
22
|
-
import { BLACK, dressed, shift
|
|
26
|
+
import { baseSans } from "./fonts.js";
|
|
27
|
+
import { BLACK, dressed, shift } from "./style.js";
|
|
23
28
|
|
|
29
|
+
/** @typedef {import('./page.js').Frame} Frame */
|
|
24
30
|
/** @typedef {import('./text.js').Line} Line */
|
|
25
31
|
/** @typedef {import('./style.js').Color} Color */
|
|
32
|
+
// A canvas holds a render's settings and turns nothing into one: what a
|
|
33
|
+
// settings is, and how an opening event becomes it, is settings.js's.
|
|
34
|
+
/** @typedef {import('./settings.js').Settings} Settings */
|
|
26
35
|
// The list's own shapes — `Op`, `Box`, `Page` — are described once, in the
|
|
27
36
|
// hand-written public declarations, and read back here.
|
|
28
37
|
/** @import { Box, Op, Page } from './index.d.ts' */
|
|
29
38
|
|
|
30
|
-
// The page box and the content box, and nothing whatever else: geometry, all
|
|
31
|
-
// of it derived below from a page and a margin. Fixed for the document — the
|
|
32
|
-
// one exception is `top`/`bottom`, which the page bands narrow once through
|
|
33
|
-
// `adopt` below, while the first page is still untouched. Everything else that
|
|
34
|
-
// holds for a whole render is `Settings`, next door; a frame carrying either
|
|
35
|
-
// half of that was a type with two lifetimes, and the copy that kept a probe
|
|
36
|
-
// reading the same locale as the draw had to be written out by hand.
|
|
37
|
-
/**
|
|
38
|
-
* @typedef {{ width: number, height: number, margin: number,
|
|
39
|
-
* content: number, top: number, bottom: number }} Frame
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
// A frame from the page box: how `content`/`top`/`bottom` fall out of a page
|
|
43
|
-
// and a margin is derived here, once, so no caller and no suite has to restate
|
|
44
|
-
// it and drift from what a real render uses.
|
|
45
|
-
/** @type {(width: number, height: number, margin: number) => Frame} */
|
|
46
|
-
let frame = (width, height, margin) => ({
|
|
47
|
-
width,
|
|
48
|
-
height,
|
|
49
|
-
margin,
|
|
50
|
-
content: width - 2 * margin,
|
|
51
|
-
top: height - margin,
|
|
52
|
-
bottom: margin,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// This layout's baseline type size. Not a host option: a document's type size
|
|
56
|
-
// is the document's own, so it is `style.size` on the report and the number
|
|
57
|
-
// here is only what text renders at when nothing declares one. The XLSX target
|
|
58
|
-
// carries the same 10 for the same reason (docs/adr/0014, docs/adr/0033).
|
|
59
|
-
let BASE = 10;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Everything that holds for a whole render and is not geometry: the faces to
|
|
63
|
-
* measure against, the report default narrowed to `family` and `size` (landing
|
|
64
|
-
* in `family` and `base` here), and the three intl facts a formatted value
|
|
65
|
-
* resolves in. Settled once, at
|
|
66
|
-
* `report-start`, by `adoptSettings` below, and only read thereafter.
|
|
67
|
-
*
|
|
68
|
-
* A canvas holds one of these by reference, never a copy, which is the whole
|
|
69
|
-
* reason it is an object. A measuring canvas built off the same settings reads
|
|
70
|
-
* exactly what the listing canvas reads, so a page band cannot be reserved
|
|
71
|
-
* against one locale and drawn in another — an agreement that used to rest on
|
|
72
|
-
* a hand-written copy staying in step.
|
|
73
|
-
*
|
|
74
|
-
* `family` is null when the report declares none, and the intl three are
|
|
75
|
-
* absent when the engine settled none.
|
|
76
|
-
*
|
|
77
|
-
* @typedef {{ fonts: import('./fonts.js').Fonts, base: number,
|
|
78
|
-
* family: string | null, locale?: string, currency?: string,
|
|
79
|
-
* timeZone?: string }} Settings
|
|
80
|
-
*/
|
|
81
|
-
|
|
82
|
-
// What a render starts from: the loaded faces, and this target's own baseline
|
|
83
|
-
// standing in for a default no report has declared yet.
|
|
84
|
-
/** @type {(fonts: import('./fonts.js').Fonts) => Settings} */
|
|
85
|
-
let settings = (fonts) => ({ fonts, base: BASE, family: null });
|
|
86
|
-
|
|
87
39
|
// `y` is the cursor on the open page and `fresh` says nothing has been drawn on
|
|
88
40
|
// it yet, which is what makes a break legal. `count` is how many pages exist.
|
|
89
41
|
/**
|
|
@@ -93,8 +45,8 @@ let settings = (fonts) => ({ fonts, base: BASE, family: null });
|
|
|
93
45
|
* rect: (color: Color, x: number, y: number, w: number, h: number) => void,
|
|
94
46
|
* stroke: (x1: number, y1: number, x2: number, y2: number, thickness: number,
|
|
95
47
|
* color: Color, dash: number[] | null) => void,
|
|
96
|
-
* picture: (
|
|
97
|
-
* w: number, h: number) => void,
|
|
48
|
+
* picture: (path: string | undefined, bytes: Uint8Array, format: string,
|
|
49
|
+
* x: number, y: number, w: number, h: number) => void,
|
|
98
50
|
* drawLine: (line: Line, x: number, yTop: number, avail: number,
|
|
99
51
|
* align: any) => void,
|
|
100
52
|
* box: (path: string | undefined, x: number, yTop: number, w: number,
|
|
@@ -182,9 +134,12 @@ let listing = (box, render) => {
|
|
|
182
134
|
dash,
|
|
183
135
|
});
|
|
184
136
|
|
|
137
|
+
// `path` rides along for the one consumer that needs to name the item back
|
|
138
|
+
// to the author: a target whose embedder rejects the bytes has nothing else
|
|
139
|
+
// to say which image it was, the box beside the op being a different record.
|
|
185
140
|
/** @type {Canvas['picture']} */
|
|
186
|
-
let picture = (bytes, format, x, y, w, h) =>
|
|
187
|
-
page.ops.push({ kind: "image", bytes, format, x, y: down(y + h), w, h });
|
|
141
|
+
let picture = (path, bytes, format, x, y, w, h) =>
|
|
142
|
+
page.ops.push({ kind: "image", path, bytes, format, x, y: down(y + h), w, h });
|
|
188
143
|
|
|
189
144
|
// `advances` rides only where a painter can use it: a base-14 face's
|
|
190
145
|
// one-per-code-point shaping, which is what lets a screen stand-in be
|
|
@@ -314,42 +269,6 @@ let adopt = (canvas, box) => {
|
|
|
314
269
|
canvas.y = canvas.top;
|
|
315
270
|
};
|
|
316
271
|
|
|
317
|
-
/**
|
|
318
|
-
* The document-wide facts off `report-start`, taken once, before anything is
|
|
319
|
-
* measured. The report default is narrowed to `family` and `size`, and each
|
|
320
|
-
* replaces this target's own baseline outright: row heights and band gaps scale
|
|
321
|
-
* with the document's type rather than staying at a size nothing is set in, and
|
|
322
|
-
* text declaring no family is set in the document's. Settling the pair here is
|
|
323
|
-
* the whole of this target's reading of docs/adr/0033 — the default reaches a
|
|
324
|
-
* node as a fallback the settings carry, never as a layer merged into its
|
|
325
|
-
* style, so a document-wide fact costs no allocation however many cells a
|
|
326
|
-
* report has.
|
|
327
|
-
*
|
|
328
|
-
* `sizeOf` and `familyName` are this target's one reading each of what a
|
|
329
|
-
* declared size and family amount to, so they read the default here too — both
|
|
330
|
-
* reach this unchecked from a computed style, and two spellings of that
|
|
331
|
-
* leniency would drift. Each falls back to what the settings already carry, so
|
|
332
|
-
* a default declaring one of the pair leaves the other alone, and one whose
|
|
333
|
-
* value is unusable leaves this target's own baseline standing.
|
|
334
|
-
*
|
|
335
|
-
* Settled once, the statement `adopt` makes about geometry: these hold for a
|
|
336
|
-
* whole render, so a second event setting them would mean two documents in one.
|
|
337
|
-
* The freeze is what enforces it, and the check is what names it.
|
|
338
|
-
*
|
|
339
|
-
* @param {Settings} render The render's settings.
|
|
340
|
-
* @param {any} event The `report-start` event.
|
|
341
|
-
*/
|
|
342
|
-
let adoptSettings = (render, event) => {
|
|
343
|
-
if (Object.isFrozen(render)) throw Error("adoptSettings: the render settings are fixed");
|
|
344
|
-
let style = event.style || {};
|
|
345
|
-
render.base = sizeOf(style, render.base);
|
|
346
|
-
render.family = familyName(style) || render.family;
|
|
347
|
-
render.locale = event.locale;
|
|
348
|
-
render.currency = event.currency;
|
|
349
|
-
render.timeZone = event.timeZone;
|
|
350
|
-
Object.freeze(render);
|
|
351
|
-
};
|
|
352
|
-
|
|
353
272
|
// The unlicensed-output marking (LICENSE section 6): one translucent line
|
|
354
273
|
// drawn corner-to-corner across the finished page — over the content, not
|
|
355
274
|
// under it, so no filled table header or background rectangle can cover it.
|
|
@@ -388,4 +307,4 @@ let stamp = (canvas, text) => {
|
|
|
388
307
|
};
|
|
389
308
|
};
|
|
390
309
|
|
|
391
|
-
export { adopt,
|
|
310
|
+
export { adopt, decorateLine, listing, measuring, stamp };
|
package/lib/image.js
CHANGED
|
@@ -53,6 +53,6 @@ export let intrinsic = (bytes, format) => {
|
|
|
53
53
|
// The engine vouched for the magic numbers, not for the rest of the file:
|
|
54
54
|
// a truncated header reaches here as a zero, and failing loudly beats
|
|
55
55
|
// drawing an image with no size (SCHEMA.md, "Image item").
|
|
56
|
-
if (!(w > 0 && h > 0)) throw Error("
|
|
56
|
+
if (!(w > 0 && h > 0)) throw Error("could not read the image's size from its bytes");
|
|
57
57
|
return { w: w * PER_PX, h: h * PER_PX };
|
|
58
58
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Target } from "quario";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Page geometry, in PostScript points. Host configuration, except that a
|
|
5
|
+
* document may declare `page.margin` in the host's stead — never both.
|
|
6
|
+
*/
|
|
4
7
|
export interface LayoutPage {
|
|
5
8
|
/** Named size or `[width, height]` in points. Default `'A4'`. */
|
|
6
9
|
size?: "A4" | "letter" | [number, number];
|
|
@@ -81,9 +84,14 @@ export interface LineOp {
|
|
|
81
84
|
dash: number[] | null;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* An image, placed. `bytes` is the array the source expression yielded, and
|
|
89
|
+
* `path` names the schema node it came from — the one thing a target whose
|
|
90
|
+
* embedder rejects those bytes has to report the failure with.
|
|
91
|
+
*/
|
|
85
92
|
export interface ImageOp {
|
|
86
93
|
kind: "image";
|
|
94
|
+
path: string | undefined;
|
|
87
95
|
bytes: Uint8Array;
|
|
88
96
|
format: "png" | "jpeg";
|
|
89
97
|
x: number;
|
package/lib/index.js
CHANGED
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
* file adds none. The package publishes `lib/` verbatim.
|
|
22
22
|
*/
|
|
23
23
|
import { breathe, walk } from "quario";
|
|
24
|
-
import { listing,
|
|
24
|
+
import { listing, stamp } from "./canvas.js";
|
|
25
25
|
import { checkFonts, loadFonts } from "./fonts.js";
|
|
26
26
|
import { flow, furniture } from "./layout.js";
|
|
27
27
|
import { geometry } from "./page.js";
|
|
28
|
+
import { settings } from "./settings.js";
|
|
28
29
|
|
|
29
30
|
export { PX_PER_POINT, hit, paint } from "./paint.js";
|
|
30
31
|
export { pageBox } from "./page.js";
|
|
@@ -78,8 +79,13 @@ export function layout(options) {
|
|
|
78
79
|
let fonts = await loadFonts(custom);
|
|
79
80
|
let gen = stream(data);
|
|
80
81
|
let first = gen.next();
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
// The opening event carries both halves this render is built from: the page
|
|
83
|
+
// frame and the document-wide settings. Peeked once here — the settings are
|
|
84
|
+
// complete and immutable by construction, so no later event can settle them
|
|
85
|
+
// a second time.
|
|
86
|
+
let opening = first.done ? null : first.value;
|
|
87
|
+
let geo = geometry(options?.page, opening);
|
|
88
|
+
let canvas = listing(geo, settings(fonts, opening));
|
|
83
89
|
// The band flow owns the placement state and every handler over it, and
|
|
84
90
|
// opens the first page as it is built; this file only hands it the stream.
|
|
85
91
|
let { handlers, finish } = flow(canvas);
|
|
@@ -88,21 +94,21 @@ export function layout(options) {
|
|
|
88
94
|
yield* gen;
|
|
89
95
|
}
|
|
90
96
|
await walk(events(), handlers);
|
|
91
|
-
// The flow
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
let { marks,
|
|
97
|
-
|
|
97
|
+
// The flow reserved the page bands off the opening event on its way past;
|
|
98
|
+
// the passes below want the rest of it — the band closures to render per
|
|
99
|
+
// page, and the marking's wording to stamp. It is the same event peeked
|
|
100
|
+
// above, so this file reads it straight rather than through `finish`.
|
|
101
|
+
// Empty when nothing was peeked, which reads as a document owing neither.
|
|
102
|
+
let { marks, pages } = finish();
|
|
103
|
+
let doc = opening || {};
|
|
98
104
|
// The passes below run over the finished pages, not the stream — no walk
|
|
99
105
|
// at all — so they open each page themselves and breathe on their own
|
|
100
106
|
// rather than through the driver.
|
|
101
|
-
await furnish(canvas,
|
|
107
|
+
await furnish(canvas, doc.page, pages);
|
|
102
108
|
// The unlicensed marking goes on last, over content and page furniture
|
|
103
109
|
// alike, once per page (LICENSE section 6). Its wording rode in on
|
|
104
110
|
// `report-start`; only the placement is this layout's.
|
|
105
|
-
await markPages(canvas,
|
|
111
|
+
await markPages(canvas, doc.marking);
|
|
106
112
|
return /** @type {Layout} */ ({
|
|
107
113
|
width: geo.width,
|
|
108
114
|
height: geo.height,
|
package/lib/layout.js
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
* document's bands claim before anything is placed, and where they are drawn
|
|
22
22
|
* on each finished page once the count is known.
|
|
23
23
|
*/
|
|
24
|
-
import { display, isReportBand, text } from "quario";
|
|
24
|
+
import { display, imageError, isReportBand, text } from "quario";
|
|
25
25
|
import { balance } from "./balance.js";
|
|
26
|
-
import { adopt,
|
|
26
|
+
import { adopt, measuring } from "./canvas.js";
|
|
27
|
+
import { frame } from "./page.js";
|
|
27
28
|
import { intrinsic } from "./image.js";
|
|
28
29
|
import { CELL_PAD, NO_PAD, WHOLE, insetOf, isWhole, paintBox, sliceInset, unbox } from "./box.js";
|
|
29
30
|
import { BAND, col, GUTTER, LEAD, merge, roled, shift, sizeOf, vshift } from "./style.js";
|
|
@@ -31,29 +32,32 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
|
|
|
31
32
|
|
|
32
33
|
/** @typedef {import('./balance.js').Unit} Unit */
|
|
33
34
|
/** @typedef {import('./canvas.js').Canvas} Canvas */
|
|
34
|
-
/** @typedef {import('./
|
|
35
|
-
/** @typedef {import('./
|
|
35
|
+
/** @typedef {import('./page.js').Frame} Frame */
|
|
36
|
+
/** @typedef {import('./settings.js').Settings} Settings */
|
|
36
37
|
/** @typedef {import('./text.js').Line} Line */
|
|
37
38
|
|
|
38
39
|
// `open` is a stack, one entry per group instance still open: the header items
|
|
39
40
|
// it holds back until content arrives, and — once those are drawn — the blocks
|
|
40
|
-
// a page turn replays them from.
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
41
|
+
// a page turn replays them from. Each entry also carries the `gap` and `skip`
|
|
42
|
+
// that stood before it opened, which is what a hollow instance puts back.
|
|
43
|
+
// `gap` is the space owed before the next group instance, `marks` the outline
|
|
44
|
+
// entries, which `anchor` positions from the cursor, and `starts` the page
|
|
45
|
+
// indexes where a `reset: "page"` sequence begins (the document itself is the
|
|
46
|
+
// sequence that starts at page 0).
|
|
44
47
|
/**
|
|
45
|
-
* @typedef {{ held: any[], blocks: Block[] }} Group
|
|
48
|
+
* @typedef {{ held: any[], blocks: Block[], gap: number, skip: boolean }} Group
|
|
46
49
|
*/
|
|
47
50
|
/**
|
|
48
51
|
* @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
|
|
49
52
|
* starts: number[], region: Region | null, pending: Pending | null,
|
|
50
|
-
* pin: number | null, skipGap: boolean, inHeader: boolean
|
|
51
|
-
* route: (event: any, payload?: Block | Measured | null) => void }} Flow
|
|
53
|
+
* pin: number | null, skipGap: boolean, inHeader: boolean }} Flow
|
|
52
54
|
*/
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* Where the cursor is and whether a strip is open — the whole of what
|
|
57
|
+
* page-vs-strip geometry depends on, and less than the whole flow. The five
|
|
58
|
+
* accessors under "the cursor" below take this, and so do the flow-spacing
|
|
59
|
+
* helpers beside them, which is how page furniture drives spacing off a canvas
|
|
60
|
+
* and no region at all.
|
|
57
61
|
* @typedef {{ canvas: Canvas, region: Region | null }} Cursor
|
|
58
62
|
*/
|
|
59
63
|
|
|
@@ -87,15 +91,36 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
|
|
|
87
91
|
* @typedef {{ block: Block, w: number }} SlotPart
|
|
88
92
|
*/
|
|
89
93
|
|
|
94
|
+
// A block no page break may pass through, and the negation of `slice`. The
|
|
95
|
+
// region balancer answers the same question its own way, keyed on the event
|
|
96
|
+
// rather than the block (`UNITS`), so a new kind of one belongs there too.
|
|
97
|
+
/** @type {(block: Block) => boolean} */
|
|
98
|
+
let unsliceable = (block) => Boolean(block.picture || block.parts);
|
|
99
|
+
|
|
90
100
|
// How `fit` sizes a picture against the width it has: `natural` is the
|
|
91
101
|
// image's own size in points, never wider than the content box; `width`
|
|
92
102
|
// scales to the content box either way. The ratio is kept in both, so a
|
|
93
103
|
// height is never anything but the width's consequence.
|
|
94
104
|
/** @type {(event: any, avail: number) => Picture} */
|
|
95
105
|
let pictureOf = (event, avail) => {
|
|
96
|
-
|
|
106
|
+
// Located here rather than in `image.js`, which is handed bytes and a
|
|
107
|
+
// format and has never heard of a schema: this is the innermost place that
|
|
108
|
+
// knows both what went wrong and which item asked for it. SCHEMA.md calls
|
|
109
|
+
// this failure a render error, and a render error names its node.
|
|
110
|
+
let sized;
|
|
111
|
+
try {
|
|
112
|
+
sized = intrinsic(event.bytes, event.format);
|
|
113
|
+
} catch (cause) {
|
|
114
|
+
throw imageError(event.path, /** @type {Error} */ (cause).message, cause);
|
|
115
|
+
}
|
|
116
|
+
let { w, h } = sized;
|
|
97
117
|
let width = event.fit === "width" ? avail : Math.min(w, avail);
|
|
98
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
bytes: event.bytes,
|
|
120
|
+
format: event.format,
|
|
121
|
+
w: width,
|
|
122
|
+
h: (h * width) / w,
|
|
123
|
+
};
|
|
99
124
|
};
|
|
100
125
|
|
|
101
126
|
// `under` is the enclosing style a slot's own layers over — a split's, when
|
|
@@ -161,7 +186,15 @@ let drawPicture = (canvas, block, x, avail) => {
|
|
|
161
186
|
let at = x + shift(block.align, avail - outer);
|
|
162
187
|
paintBox(canvas, at, canvas.y, outer, block.h, block.style, block.bg);
|
|
163
188
|
canvas.box(block.path, at, canvas.y, outer, block.h);
|
|
164
|
-
canvas.picture(
|
|
189
|
+
canvas.picture(
|
|
190
|
+
block.path,
|
|
191
|
+
bytes,
|
|
192
|
+
format,
|
|
193
|
+
at + inset.l,
|
|
194
|
+
canvas.y - inset.t - block.drop - h,
|
|
195
|
+
w,
|
|
196
|
+
h,
|
|
197
|
+
);
|
|
165
198
|
canvas.y -= block.h;
|
|
166
199
|
canvas.fresh = false;
|
|
167
200
|
};
|
|
@@ -330,14 +363,24 @@ let drawBlock = (canvas, block, x, avail) =>
|
|
|
330
363
|
* @typedef {{ count: number, owner: number }} Pending
|
|
331
364
|
*/
|
|
332
365
|
|
|
366
|
+
// --- the cursor -------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
// The five below are the whole of how anything reaches page-vs-strip geometry,
|
|
369
|
+
// and they take a `Cursor` — a canvas and the region open on it, if one is —
|
|
370
|
+
// because the region is the only thing that changes any of their answers. The
|
|
371
|
+
// drawing primitives take a bare `Canvas` and read it flat, and that is not an
|
|
372
|
+
// exception to this: a primitive draws where it is told and does not choose a
|
|
373
|
+
// column, so it has nothing to ask. Page furniture reads flat for the same
|
|
374
|
+
// reason — `band` places at the page's own margin and never inside a region.
|
|
375
|
+
// The flow-spacing helpers below take a `Cursor` too: `keepLead` asks
|
|
376
|
+
// `freshOf`, so the spacing and the geometry are one seam, not two.
|
|
377
|
+
|
|
333
378
|
// Where the cursor's column starts and how wide it is: the strip's, inside a
|
|
334
|
-
// region, and the page's content box outside one.
|
|
335
|
-
|
|
336
|
-
// presenting the page, because `pageFrame` re-derives page furniture from it.
|
|
337
|
-
/** @type {(state: Flow) => number} */
|
|
379
|
+
// region, and the page's content box outside one.
|
|
380
|
+
/** @type {(state: Cursor) => number} */
|
|
338
381
|
let originOf = (state) =>
|
|
339
382
|
state.canvas.margin + (state.region ? state.region.index * (state.region.width + GUTTER) : 0);
|
|
340
|
-
/** @type {(state:
|
|
383
|
+
/** @type {(state: Cursor) => number} */
|
|
341
384
|
let widthOf = (state) => (state.region ? state.region.width : state.canvas.content);
|
|
342
385
|
// The bottom the cursor fills to. A balanced strip's floor sits above the page
|
|
343
386
|
// bottom and is a target rather than a bound: a fresh strip still takes at
|
|
@@ -345,7 +388,7 @@ let widthOf = (state) => (state.region ? state.region.width : state.canvas.conte
|
|
|
345
388
|
// overruns. The last strip answers to the page instead — a balanced share is
|
|
346
389
|
// rounded, so the strips before it may each stop a little short, and the
|
|
347
390
|
// remainder has to land somewhere.
|
|
348
|
-
/** @type {(state:
|
|
391
|
+
/** @type {(state: Cursor) => number} */
|
|
349
392
|
let floorOf = (state) => {
|
|
350
393
|
let region = state.region;
|
|
351
394
|
if (!region || region.height == null || region.index === region.count - 1)
|
|
@@ -353,11 +396,11 @@ let floorOf = (state) => {
|
|
|
353
396
|
return region.top - region.height;
|
|
354
397
|
};
|
|
355
398
|
// The y a fresh column starts from: a strip's shared top inside a region.
|
|
356
|
-
/** @type {(state:
|
|
399
|
+
/** @type {(state: Cursor) => number} */
|
|
357
400
|
let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
|
|
358
401
|
// Has this column taken anything yet? Answered positionally, because the only
|
|
359
402
|
// thing that marks a page used is the cursor moving off its top, and the
|
|
360
|
-
// drawing primitives that move it take a `Canvas` rather than a `
|
|
403
|
+
// drawing primitives that move it take a `Canvas` rather than a `Cursor`.
|
|
361
404
|
// `canvas.fresh` stays the *page*'s own answer — it is what keeps `turn` from
|
|
362
405
|
// leaving an empty page behind — so a strip claims no second meaning for it.
|
|
363
406
|
// A block of no height would read fresh here and used there; none is reachable
|
|
@@ -438,7 +481,7 @@ let skipFlow = (state, event, name) => {
|
|
|
438
481
|
/** @type {(state: Cursor, event: any) => number} */
|
|
439
482
|
let flowPad = (state, event) => leadOf(state, event) + spaceOf(event.style, "spaceAfter");
|
|
440
483
|
|
|
441
|
-
// The
|
|
484
|
+
// The unsliceable blocks -- a picture and a split -- move whole to a fresh
|
|
442
485
|
// column rather than breaking, so a balanced floor never cuts one: only the
|
|
443
486
|
// page's own floor bears on it. A picture that no column can hold is scaled
|
|
444
487
|
// down to the one it lands on, since obeying the guarantee literally would
|
|
@@ -538,12 +581,12 @@ let item = (state, event, measured = null) => {
|
|
|
538
581
|
skipFlow(state, event, "spaceAfter");
|
|
539
582
|
};
|
|
540
583
|
|
|
541
|
-
// What a block does when it does not fit where it stands.
|
|
542
|
-
//
|
|
543
|
-
//
|
|
584
|
+
// What a block does when it does not fit where it stands. An unsliceable block
|
|
585
|
+
// moves whole to a fresh column; anything else flows down it, breaking where
|
|
586
|
+
// `fitLines` says.
|
|
544
587
|
/** @type {(state: Flow, block: Block) => void} */
|
|
545
588
|
let overflow = (state, block) =>
|
|
546
|
-
block
|
|
589
|
+
unsliceable(block) ? placeWhole(state, block) : slice(state, block);
|
|
547
590
|
|
|
548
591
|
// The height a page turn carries over: the drawn headers of the groups still
|
|
549
592
|
// open, which every page their instances continue onto replays. Computed rather
|
|
@@ -666,18 +709,14 @@ let openRegion = (state) => {
|
|
|
666
709
|
};
|
|
667
710
|
};
|
|
668
711
|
|
|
669
|
-
//
|
|
670
|
-
// full-width content resumes under it on the same page (SCHEMA.md).
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (region.held) commit(state);
|
|
678
|
-
if (!state.region) return;
|
|
679
|
-
let canvas = state.canvas;
|
|
680
|
-
canvas.y = Math.min(canvas.y, ...region.ends);
|
|
712
|
+
// Land the region: the cursor drops to whichever strip reached lowest, and
|
|
713
|
+
// full-width content resumes under it on the same page (SCHEMA.md). The last
|
|
714
|
+
// reader of `ends`, which `restrip` empties and `advance` fills — closing is
|
|
715
|
+
// the flow's, because a region still holding has to be laid out first, but
|
|
716
|
+
// where the region lands is geometry and belongs here.
|
|
717
|
+
/** @type {(state: Flow, region: Region) => void} */
|
|
718
|
+
let landRegion = (state, region) => {
|
|
719
|
+
state.canvas.y = Math.min(state.canvas.y, ...region.ends);
|
|
681
720
|
state.region = null;
|
|
682
721
|
};
|
|
683
722
|
|
|
@@ -739,11 +778,14 @@ let UNITS = {
|
|
|
739
778
|
},
|
|
740
779
|
row: restated,
|
|
741
780
|
"total-row": restated,
|
|
781
|
+
// The kinds `unsliceable` names, restated because a strip is costed from the
|
|
782
|
+
// event rather than the block -- this table's `whole` is the balancer's
|
|
783
|
+
// model, not the pagination truth. A third such kind belongs in both places.
|
|
742
784
|
image: unbroken,
|
|
743
785
|
split: unbroken,
|
|
744
786
|
};
|
|
745
787
|
|
|
746
|
-
// An
|
|
788
|
+
// An entry no strip may cut owes it nothing but its own height.
|
|
747
789
|
/** @type {(entry: any) => Unit} */
|
|
748
790
|
function unbroken(entry) {
|
|
749
791
|
return unsplit(entry.h, entry.h);
|
|
@@ -766,20 +808,19 @@ let stripHeight = (state, region, own) =>
|
|
|
766
808
|
? null
|
|
767
809
|
: balance(unitsOf(state, own), region.count, roomOf(state, region), rowHeight(state.canvas));
|
|
768
810
|
|
|
769
|
-
|
|
811
|
+
// Commit the region: decide the strip height, stop holding, and hand the held
|
|
812
|
+
// entries back in the order they arrived, for `flow`'s `replay` to walk.
|
|
813
|
+
//
|
|
814
|
+
// Marking the spans is also what picks out the region's own children, and the
|
|
815
|
+
// height is decided from those — so it runs before the replay, not with it.
|
|
816
|
+
/** @type {(state: Flow) => any[]} */
|
|
770
817
|
let commit = (state) => {
|
|
771
818
|
let region = /** @type {Region} */ (state.region);
|
|
772
819
|
let held = /** @type {any[]} */ (region.held);
|
|
773
|
-
// Marking the spans is also what picks out the region's own children, and
|
|
774
|
-
// the height is decided from those — so it runs before the replay, not with
|
|
775
|
-
// it.
|
|
776
820
|
region.height = stripHeight(state, region, spans(held));
|
|
777
821
|
region.held = null;
|
|
778
822
|
restrip(region, region.top);
|
|
779
|
-
|
|
780
|
-
if (breaksFor(state, region, entry.span)) advance(state);
|
|
781
|
-
state.route(entry.event, entry.payload);
|
|
782
|
-
}
|
|
823
|
+
return held;
|
|
783
824
|
};
|
|
784
825
|
|
|
785
826
|
// A group instance is the atomic unit inside a region: one that would cross a
|
|
@@ -864,10 +905,11 @@ let NESTING = { "group-start": 1, "group-end": -1 };
|
|
|
864
905
|
// of what it holds. A measured block rides along so the replay never wraps the
|
|
865
906
|
// same text twice; a held header counts towards the total like anything else,
|
|
866
907
|
// but the flush that draws it measures from the event, so only its height is
|
|
867
|
-
// kept.
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
908
|
+
// kept. Answers whether that event filled it: a buffer that outgrows what the
|
|
909
|
+
// strips could hold has answered the balancing question early, and everything
|
|
910
|
+
// after it streams.
|
|
911
|
+
/** @type {(state: Flow, event: any, payload: Block | Measured | null, hollow: boolean) => boolean} */
|
|
912
|
+
let buffer = (state, event, payload, hollow) => {
|
|
871
913
|
let region = /** @type {Region} */ (state.region);
|
|
872
914
|
// The one door into the held list, so it is where the buffer's own walk is
|
|
873
915
|
// kept. A bracket buffered here is a bracket the open stack will not see
|
|
@@ -877,7 +919,33 @@ let buffer = (state, event, payload) => {
|
|
|
877
919
|
/** @type {any[]} */ (region.held).push(entry);
|
|
878
920
|
region.measured += entry.h;
|
|
879
921
|
if (event.type === "table-end") region.measured += remeasure(state, region, entry);
|
|
880
|
-
if (
|
|
922
|
+
if (hollow) region.measured -= refund(/** @type {any[]} */ (region.held));
|
|
923
|
+
return outgrown(state, region);
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// Give back what `WORTH` charged the opening of an instance that turned out
|
|
927
|
+
// hollow, and zero the entry so the replay's `spans` walk agrees. Taken as the
|
|
928
|
+
// close lands rather than at commit because `outgrown` is tested on every
|
|
929
|
+
// buffered event: a region carrying a phantom height can give up on balancing
|
|
930
|
+
// before it ever reaches the commit that would have corrected it.
|
|
931
|
+
//
|
|
932
|
+
// The walk back is over brackets only, which is safe precisely because the
|
|
933
|
+
// caller has already established the instance is hollow — there is nothing
|
|
934
|
+
// else between the two ends to walk over. Nested hollow instances refunded
|
|
935
|
+
// themselves on the way in, so each charge is given back exactly once.
|
|
936
|
+
/** @type {(held: any[]) => number} */
|
|
937
|
+
let refund = (held) => {
|
|
938
|
+
let depth = 0;
|
|
939
|
+
for (let i = held.length - 2; i >= 0; i--) {
|
|
940
|
+
if (held[i].event.type === "group-end") depth++;
|
|
941
|
+
else if (depth) depth--;
|
|
942
|
+
else {
|
|
943
|
+
let charged = held[i].h;
|
|
944
|
+
held[i].h = 0;
|
|
945
|
+
return charged;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return 0;
|
|
881
949
|
};
|
|
882
950
|
|
|
883
951
|
// A table's events reach the buffer one at a time, and each is worth a bare
|
|
@@ -1029,16 +1097,14 @@ let flush = (state, extra) => {
|
|
|
1029
1097
|
};
|
|
1030
1098
|
|
|
1031
1099
|
// Enough of an item that its group header is never left introducing nothing.
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1100
|
+
// An unsliceable block is kept whole -- the same blocks `overflow` moves
|
|
1101
|
+
// rather than slices. There is no first part of one to keep company with: a
|
|
1102
|
+
// split's own `lines` are empty, so clipping to `KEEP_LINES` would reserve
|
|
1103
|
+
// nothing at all and strand the run it was called to protect.
|
|
1036
1104
|
let KEEP_LINES = 2;
|
|
1037
1105
|
/** @type {(block: Block) => number} */
|
|
1038
1106
|
let keepWith = (block) =>
|
|
1039
|
-
block.
|
|
1040
|
-
? block.h
|
|
1041
|
-
: Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
|
|
1107
|
+
unsliceable(block) ? block.h : Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
|
|
1042
1108
|
|
|
1043
1109
|
// Flush any pending header with this item's opening, then place the item. The
|
|
1044
1110
|
// block is measured once here and handed on rather than wrapped twice.
|
|
@@ -1070,16 +1136,52 @@ let holding = (state) => state.open.some((group) => group.held.length) || state.
|
|
|
1070
1136
|
// starts a new `page.number` / `page.total` sequence on the page it opens.
|
|
1071
1137
|
// A duplicate start on a page that already began a sequence is a zero-length
|
|
1072
1138
|
// range; `numbered` skips it.
|
|
1139
|
+
// Run `seen` on every event before the handler that acts on it. The stream is
|
|
1140
|
+
// the only place hollowness can be read in order: past this point a region may
|
|
1141
|
+
// hold an event back for a commit that lands after its own instance has closed.
|
|
1142
|
+
/** @type {(seen: (event: any) => void, handlers: Record<string, (event: any) => void>) => Record<string, (event: any) => void>} */
|
|
1143
|
+
let tracked = (seen, handlers) =>
|
|
1144
|
+
Object.fromEntries(
|
|
1145
|
+
Object.entries(handlers).map(([type, handle]) => [
|
|
1146
|
+
type,
|
|
1147
|
+
(/** @type {any} */ event) => {
|
|
1148
|
+
seen(event);
|
|
1149
|
+
handle(event);
|
|
1150
|
+
},
|
|
1151
|
+
]),
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1073
1154
|
/** @type {(state: Flow, event: any) => void} */
|
|
1074
1155
|
let openGroup = (state, event) => {
|
|
1075
1156
|
breakFor(state, event);
|
|
1076
1157
|
if (event.columns) state.pending = { count: event.columns, owner: event.depth };
|
|
1077
|
-
|
|
1158
|
+
let gap = state.gap;
|
|
1159
|
+
let skip = state.skipGap;
|
|
1160
|
+
state.gap = skip ? 0 : Math.max(state.gap, instanceGap(state.canvas));
|
|
1078
1161
|
state.skipGap = false;
|
|
1079
|
-
state.open.push({ held: [], blocks: [] });
|
|
1162
|
+
state.open.push({ held: [], blocks: [], gap, skip });
|
|
1080
1163
|
state.marks.push(markFor(event));
|
|
1081
1164
|
};
|
|
1082
1165
|
|
|
1166
|
+
// Put back everything opening this instance displaced, so a hollow one leaves
|
|
1167
|
+
// the flow exactly as it found it. Restoring rather than zeroing is the whole
|
|
1168
|
+
// point: `Math.max` folded any ancestor's gap into this one, and a close that
|
|
1169
|
+
// zeroed would rob the parent of a gap its own opening earned. The pinned
|
|
1170
|
+
// header's `skip` travels the same way — a hollow instance is not the band the
|
|
1171
|
+
// pin was suppressing the gap for, so the suppression passes through it to
|
|
1172
|
+
// whatever lands next. The mark goes too: the outline is what a reader opens,
|
|
1173
|
+
// and a bookmark onto content this instance did not draw is a worse answer
|
|
1174
|
+
// than no bookmark. It is this instance's mark for certain — a non-hollow
|
|
1175
|
+
// child would have made this instance non-hollow, and a hollow one already
|
|
1176
|
+
// took its own back.
|
|
1177
|
+
/** @type {(state: Flow) => void} */
|
|
1178
|
+
let unopen = (state) => {
|
|
1179
|
+
let frame = state.open[state.open.length - 1];
|
|
1180
|
+
state.gap = frame.gap;
|
|
1181
|
+
state.skipGap = frame.skip;
|
|
1182
|
+
state.marks.pop();
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1083
1185
|
// A real page, inside a region as much as outside one: ADR 0013 declined a
|
|
1084
1186
|
// `break: "column"`, so this never means the next strip. `turn` restrips.
|
|
1085
1187
|
/** @type {(state: Flow, event: any) => void} */
|
|
@@ -1519,15 +1621,11 @@ let tableOf = (events) => {
|
|
|
1519
1621
|
let opening = events[0];
|
|
1520
1622
|
return {
|
|
1521
1623
|
columns: opening.columns.map((/** @type {any} */ col) => ({ ...col })),
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
header: opening.columns
|
|
1528
|
-
.filter((/** @type {any} */ col) => col.header)
|
|
1529
|
-
.map((/** @type {any} */ col) => ({ ...col.header, path: col.path })),
|
|
1530
|
-
headerStyle: opening.style,
|
|
1624
|
+
// The header row arrives whole (docs/adr/0053), so both readers -- the
|
|
1625
|
+
// replay and the region buffer -- take the same list rather than each
|
|
1626
|
+
// rebuilding it from the columns.
|
|
1627
|
+
header: opening.header.cells,
|
|
1628
|
+
headerStyle: opening.header.style,
|
|
1531
1629
|
rows: events.filter((event) => event.type === "row"),
|
|
1532
1630
|
// The events themselves, as `rows` are: a total row is a row, and the
|
|
1533
1631
|
// buffer files each corrected height under the event it was measured from.
|
|
@@ -1678,14 +1776,15 @@ let reserve = (geo, settings, bands) => {
|
|
|
1678
1776
|
};
|
|
1679
1777
|
};
|
|
1680
1778
|
|
|
1681
|
-
// The page frame a canvas presents,
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1779
|
+
// The page frame a canvas presents, recovered from the canvas after `adopt`
|
|
1780
|
+
// narrowed it. Only `top`/`bottom` were overwritten there; `width`, `height`
|
|
1781
|
+
// and `margin` are still the page's own, so this re-derives the two that went
|
|
1782
|
+
// through the same `frame` every other consumer reads — a derivation from live
|
|
1783
|
+
// fields, never a copy that could drift from one. Both halves of page furniture
|
|
1784
|
+
// want it: reservation to measure against, the draw pass to hang the header
|
|
1785
|
+
// from.
|
|
1687
1786
|
/** @type {(canvas: Canvas) => Frame} */
|
|
1688
|
-
let
|
|
1787
|
+
let unnarrowed = (canvas) => frame(canvas.width, canvas.height, canvas.margin);
|
|
1689
1788
|
|
|
1690
1789
|
// One finished page's furniture, drawn in the strips `reserve` left for it:
|
|
1691
1790
|
// the header hanging from the top margin, the footer resting on the bottom
|
|
@@ -1695,7 +1794,7 @@ let pageFrame = (canvas) => frame(canvas.width, canvas.height, canvas.margin);
|
|
|
1695
1794
|
// that made room for them.
|
|
1696
1795
|
/** @type {(canvas: Canvas, bands: any, anchor: any) => void} */
|
|
1697
1796
|
let furniture = (canvas, bands, anchor) => {
|
|
1698
|
-
if (bands.header) band(canvas, bands.header(anchor),
|
|
1797
|
+
if (bands.header) band(canvas, bands.header(anchor), unnarrowed(canvas).top);
|
|
1699
1798
|
if (bands.footer) band(canvas, bands.footer(anchor), canvas.bottom - BAND);
|
|
1700
1799
|
};
|
|
1701
1800
|
|
|
@@ -1728,12 +1827,12 @@ let numbered = (count, starts) => {
|
|
|
1728
1827
|
* `finish` flushes whatever the last event left pending and returns what the
|
|
1729
1828
|
* passes over the finished pages still need: the outline marks — the only way
|
|
1730
1829
|
* to reach them, so an outline read off a band flow that still owes a flush is
|
|
1731
|
-
* not a mistake this seam leaves open — the
|
|
1732
|
-
*
|
|
1733
|
-
*
|
|
1830
|
+
* not a mistake this seam leaves open — and the per-page `{ number, total }`
|
|
1831
|
+
* anchors the furniture pass draws with. The opening event itself the entry
|
|
1832
|
+
* peeked for both halves it is built from, so this seam keeps none of it.
|
|
1734
1833
|
*
|
|
1735
1834
|
* @type {(canvas: Canvas) => { handlers: Record<string, (event: any) => void>,
|
|
1736
|
-
* finish: () => { marks: any[],
|
|
1835
|
+
* finish: () => { marks: any[],
|
|
1737
1836
|
* pages: { number: number, total: number }[] } }}
|
|
1738
1837
|
*/
|
|
1739
1838
|
let flow = (canvas) => {
|
|
@@ -1749,23 +1848,35 @@ let flow = (canvas) => {
|
|
|
1749
1848
|
pin: null,
|
|
1750
1849
|
skipGap: false,
|
|
1751
1850
|
inHeader: false,
|
|
1752
|
-
// Assigned below, once `route` exists: a buffered region replays through
|
|
1753
|
-
// it, and one entry point is what makes the replay take the path the
|
|
1754
|
-
// content would have taken had it never been held.
|
|
1755
|
-
route: () => {},
|
|
1756
1851
|
};
|
|
1757
|
-
// The body walk's own folder, emitting each finished event onward to `route`
|
|
1758
|
-
// below. Declared here and bound after `route` exists.
|
|
1759
|
-
/** @type {(event: any) => void} */
|
|
1760
|
-
let fold;
|
|
1761
1852
|
// A table is buffered whole before it is laid out: column widths come from
|
|
1762
1853
|
// every cell in it, so the last row has to be in hand first.
|
|
1763
1854
|
/** @type {any[]} */
|
|
1764
1855
|
let collected = [];
|
|
1765
|
-
//
|
|
1766
|
-
//
|
|
1767
|
-
|
|
1768
|
-
|
|
1856
|
+
// Hollowness is a property of the stream, not of what gets drawn: an
|
|
1857
|
+
// instance is hollow when nothing but hollow brackets arrived between its
|
|
1858
|
+
// own ends. Tracked at the handler layer, which every event reaches exactly
|
|
1859
|
+
// once — `route` is re-entered by a region's replay, and a buffered instance
|
|
1860
|
+
// has to be judged before it is buffered, not after. `filling` is one flag
|
|
1861
|
+
// per open bracket; the verdict rides the closing event itself, so the
|
|
1862
|
+
// buffer and the replay both read the answer the stream gave.
|
|
1863
|
+
/** @type {boolean[]} */
|
|
1864
|
+
let filling = [];
|
|
1865
|
+
/** @type {WeakSet<any>} */
|
|
1866
|
+
let hollows = new WeakSet();
|
|
1867
|
+
// Whatever instance is open drew something. Called for an ordinary event and
|
|
1868
|
+
// for a child that turned out not to be hollow, because those are the same
|
|
1869
|
+
// claim: an instance draws through its content and through its children.
|
|
1870
|
+
let fill = () => {
|
|
1871
|
+
if (filling.length) filling[filling.length - 1] = true;
|
|
1872
|
+
};
|
|
1873
|
+
/** @type {(event: any) => void} */
|
|
1874
|
+
let track = (event) => {
|
|
1875
|
+
if (event.type === "group-start") filling.push(false);
|
|
1876
|
+
else if (event.type !== "group-end") fill();
|
|
1877
|
+
else if (filling.pop()) fill();
|
|
1878
|
+
else hollows.add(event);
|
|
1879
|
+
};
|
|
1769
1880
|
// The body opens its own first page. Not through `turn`: a canvas that has
|
|
1770
1881
|
// drawn nothing is fresh already, and would be left with no page at all.
|
|
1771
1882
|
canvas.newPage();
|
|
@@ -1803,8 +1914,9 @@ let flow = (canvas) => {
|
|
|
1803
1914
|
// default of its own -- its slots carry theirs.
|
|
1804
1915
|
split: heldOrPlaced,
|
|
1805
1916
|
"group-start": (event) => openGroup(state, event),
|
|
1806
|
-
"group-end": () => {
|
|
1807
|
-
|
|
1917
|
+
"group-end": (event) => {
|
|
1918
|
+
if (hollows.has(event)) unopen(state);
|
|
1919
|
+
else flush(state, 0);
|
|
1808
1920
|
state.open.pop();
|
|
1809
1921
|
},
|
|
1810
1922
|
// A table is collected as the events it arrived as, and shaped by `tableOf`
|
|
@@ -1864,13 +1976,53 @@ let flow = (canvas) => {
|
|
|
1864
1976
|
let route = (event, payload = null) => {
|
|
1865
1977
|
snapPin(event);
|
|
1866
1978
|
if (fullBand(event)) fullWidth(event, payload);
|
|
1867
|
-
else if (deciding())
|
|
1979
|
+
else if (deciding()) hold(event, payload);
|
|
1868
1980
|
else if (opens(event)) begin(event);
|
|
1869
1981
|
else place(event, payload);
|
|
1870
1982
|
};
|
|
1871
1983
|
|
|
1872
|
-
|
|
1873
|
-
fold = splitFolder(route);
|
|
1984
|
+
// The body walk's own folder, emitting each finished event onward to `route`.
|
|
1985
|
+
let fold = splitFolder(route);
|
|
1986
|
+
|
|
1987
|
+
// Hold one event back in the region's buffer, and lay that buffer out if this
|
|
1988
|
+
// is the event that filled it. Not `holdHeader`: that holds a group's header
|
|
1989
|
+
// for the content it introduces, this holds anything at all for a region
|
|
1990
|
+
// still deciding.
|
|
1991
|
+
/** @type {(event: any, payload: Block | Measured | null) => void} */
|
|
1992
|
+
let hold = (event, payload) => {
|
|
1993
|
+
if (buffer(state, event, payload, hollows.has(event))) replay();
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
// Lay the buffer out: `commit` decides the strip height and hands the held
|
|
1997
|
+
// entries back in order, and this walks them through `route`. The walk lives
|
|
1998
|
+
// here rather than under the region section marker for the reason everything
|
|
1999
|
+
// there is module-level — that section is reachable without the router, and
|
|
2000
|
+
// a walk through `route` is not.
|
|
2001
|
+
let replay = () => {
|
|
2002
|
+
let region = /** @type {Region} */ (state.region);
|
|
2003
|
+
for (let entry of commit(state)) {
|
|
2004
|
+
if (breaksFor(state, region, entry.span)) advance(state);
|
|
2005
|
+
route(entry.event, entry.payload);
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
// Close the region: anything still held is laid out first, and the region
|
|
2010
|
+
// then lands where `landRegion` puts it.
|
|
2011
|
+
//
|
|
2012
|
+
// Nothing the replay routes can arrive back here, which is why the local
|
|
2013
|
+
// `region` survives the call. `route` asks `fullBand` before `deciding()`, so
|
|
2014
|
+
// a full band is never buffered; and a replayed event, asked `fullBand` a
|
|
2015
|
+
// second time, answers as it did the first, because `nesting()`'s two
|
|
2016
|
+
// branches walk the same brackets — `buffer` carries `region.nesting` through
|
|
2017
|
+
// them while nothing is placed, `state.open` through them once the replay is
|
|
2018
|
+
// placing. The other closer, `endOwned`, is wired above `route` at the
|
|
2019
|
+
// handler layer, where a replay does not reach.
|
|
2020
|
+
let closeRegion = () => {
|
|
2021
|
+
let region = state.region;
|
|
2022
|
+
if (!region) return;
|
|
2023
|
+
if (deciding()) replay();
|
|
2024
|
+
landRegion(state, region);
|
|
2025
|
+
};
|
|
1874
2026
|
|
|
1875
2027
|
// Everything drawn across the page rather than down a strip: the declaring
|
|
1876
2028
|
// node's own bands, its footer among them.
|
|
@@ -1890,7 +2042,7 @@ let flow = (canvas) => {
|
|
|
1890
2042
|
/** @type {(depth: number) => void} */
|
|
1891
2043
|
let endOwned = (depth) => {
|
|
1892
2044
|
if (owner() !== depth) return;
|
|
1893
|
-
if (state.region) closeRegion(
|
|
2045
|
+
if (state.region) closeRegion();
|
|
1894
2046
|
else state.pending = null;
|
|
1895
2047
|
};
|
|
1896
2048
|
|
|
@@ -1905,7 +2057,7 @@ let flow = (canvas) => {
|
|
|
1905
2057
|
let fullWidth = (event, payload) => {
|
|
1906
2058
|
// Asked before the region closes, because that is what it reads.
|
|
1907
2059
|
let own = ownFooter(event);
|
|
1908
|
-
closeRegion(
|
|
2060
|
+
closeRegion();
|
|
1909
2061
|
if (own) state.pending = null;
|
|
1910
2062
|
place(event, payload);
|
|
1911
2063
|
};
|
|
@@ -1921,7 +2073,7 @@ let flow = (canvas) => {
|
|
|
1921
2073
|
let block = keepBlock(state, event, stripWidth(state, owed.count));
|
|
1922
2074
|
flush(state, block ? keepWith(block) : 0);
|
|
1923
2075
|
openRegion(state);
|
|
1924
|
-
|
|
2076
|
+
hold(event, block);
|
|
1925
2077
|
};
|
|
1926
2078
|
|
|
1927
2079
|
/** @type {(event: any) => void} */
|
|
@@ -1935,12 +2087,17 @@ let flow = (canvas) => {
|
|
|
1935
2087
|
};
|
|
1936
2088
|
|
|
1937
2089
|
return {
|
|
1938
|
-
|
|
2090
|
+
// Every handler is wrapped so `track` sees the stream once, in order, ahead
|
|
2091
|
+
// of the dispatch that may buffer the event for later. One wrap rather than
|
|
2092
|
+
// a call in each handler: a new event type must count towards the instance
|
|
2093
|
+
// it arrives in without anyone remembering to say so.
|
|
2094
|
+
handlers: tracked(track, {
|
|
1939
2095
|
"report-start": (event) => {
|
|
1940
|
-
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
|
|
2096
|
+
// The document-wide settings were built complete from this same event
|
|
2097
|
+
// before the walk began (index.js peeks it), so this handler reads them
|
|
2098
|
+
// through `canvas.settings` and settles nothing — only the page bands,
|
|
2099
|
+
// whose height is not known until here, are taken off it below. The
|
|
2100
|
+
// entry peeked the event too, so nothing is kept for it here.
|
|
1944
2101
|
if (event.columns) state.pending = { count: event.columns, owner: -1 };
|
|
1945
2102
|
if (!event.page) {
|
|
1946
2103
|
pinHeader(event);
|
|
@@ -1952,7 +2109,7 @@ let flow = (canvas) => {
|
|
|
1952
2109
|
// one thing a measurement must not be able to do. What a band needs is
|
|
1953
2110
|
// a property of the page box in any case, not of a content box
|
|
1954
2111
|
// something may already have narrowed.
|
|
1955
|
-
adopt(canvas, reserve(
|
|
2112
|
+
adopt(canvas, reserve(unnarrowed(canvas), canvas.settings, event.page));
|
|
1956
2113
|
pinHeader(event);
|
|
1957
2114
|
},
|
|
1958
2115
|
// Everything else routes: `placed` above says what each event does, and
|
|
@@ -1975,14 +2132,17 @@ let flow = (canvas) => {
|
|
|
1975
2132
|
endOwned(event.depth);
|
|
1976
2133
|
route(event);
|
|
1977
2134
|
},
|
|
1978
|
-
},
|
|
2135
|
+
}),
|
|
1979
2136
|
finish: () => {
|
|
1980
2137
|
snapPin({ type: "report-end" });
|
|
1981
2138
|
// A root region reaches here undrained when a report declares `columns`
|
|
1982
2139
|
// and no full-width footer followed its body.
|
|
1983
|
-
closeRegion(
|
|
2140
|
+
closeRegion();
|
|
1984
2141
|
flush(state, 0);
|
|
1985
|
-
return {
|
|
2142
|
+
return {
|
|
2143
|
+
marks: state.marks,
|
|
2144
|
+
pages: numbered(canvas.count, state.starts),
|
|
2145
|
+
};
|
|
1986
2146
|
},
|
|
1987
2147
|
};
|
|
1988
2148
|
};
|
package/lib/page.js
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
* it is written. Owned here because every consumer of the list — the PDF
|
|
4
4
|
* target, the viewer, the editor — lays out on the same page, and a size that
|
|
5
5
|
* meant one thing on screen and another on paper would be a silent lie.
|
|
6
|
+
*
|
|
7
|
+
* The derivation belongs with the validation: `frame` below turns a validated
|
|
8
|
+
* page and margin into the geometry everything downstream lays out in, so a
|
|
9
|
+
* page becomes a content box here and `canvas.js` only takes the answer,
|
|
10
|
+
* rather than the drawing surface defining the page it draws on. Deriving a
|
|
11
|
+
* frame is not owning its lifetime, and this module claims only the first:
|
|
12
|
+
* `adopt` (`canvas.js`) narrows `top`/`bottom` once for the page bands, and
|
|
13
|
+
* `reserve` (`layout.js`) mints the frame it narrows from.
|
|
6
14
|
*/
|
|
7
|
-
import { frame } from "./canvas.js";
|
|
8
15
|
|
|
9
16
|
// The named sizes. `@quario/pdf` used to own this table, and the two element
|
|
10
17
|
// packages restated it; the layout package is where all three now read it.
|
|
@@ -80,11 +87,42 @@ export let pageBox = (page, at = "page") => {
|
|
|
80
87
|
return { width, height, margin: marginOf(page?.margin, width, height, at + ".margin") };
|
|
81
88
|
};
|
|
82
89
|
|
|
90
|
+
// The page box and the content box, and nothing whatever else: geometry, all
|
|
91
|
+
// of it derived below from a page and a margin. Fixed for the document — the
|
|
92
|
+
// one exception is `top`/`bottom`, which the page bands narrow once through
|
|
93
|
+
// `adopt` (`canvas.js`), while the first page is still untouched. Everything
|
|
94
|
+
// else that holds for a whole render is that module's `Settings`; a frame
|
|
95
|
+
// carrying either half of that was a type with two lifetimes, and the copy that
|
|
96
|
+
// kept a probe reading the same locale as the draw had to be written out by
|
|
97
|
+
// hand.
|
|
98
|
+
/**
|
|
99
|
+
* @typedef {{ width: number, height: number, margin: number,
|
|
100
|
+
* content: number, top: number, bottom: number }} Frame
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
// A frame from the page box: how `content`/`top`/`bottom` fall out of a page
|
|
104
|
+
// and a margin is derived here, once, so no caller and no suite has to restate
|
|
105
|
+
// it and drift from what a real render uses. A render reaches a frame through
|
|
106
|
+
// `geometry` below and never through this; the package entry carries neither,
|
|
107
|
+
// so no host reaches one at all. Exported for `layout.js`, which re-derives the
|
|
108
|
+
// page frame a canvas presents, and for the suites that build a fixture frame
|
|
109
|
+
// — the day that re-derivation goes, the export stands for the suites alone,
|
|
110
|
+
// and they should take `geometry` rather than keep it standing.
|
|
111
|
+
/** @type {(width: number, height: number, margin: number) => Frame} */
|
|
112
|
+
export let frame = (width, height, margin) => ({
|
|
113
|
+
width,
|
|
114
|
+
height,
|
|
115
|
+
margin,
|
|
116
|
+
content: width - 2 * margin,
|
|
117
|
+
top: height - margin,
|
|
118
|
+
bottom: margin,
|
|
119
|
+
});
|
|
120
|
+
|
|
83
121
|
/**
|
|
84
122
|
* The page box and the content box in one, for a render: the host's size,
|
|
85
123
|
* and whichever margin the host and the opening event agreed on.
|
|
86
124
|
*
|
|
87
|
-
* @type {(page: any, opening?: any) =>
|
|
125
|
+
* @type {(page: any, opening?: any) => Frame}
|
|
88
126
|
*/
|
|
89
127
|
export let geometry = (page = {}, opening) => {
|
|
90
128
|
let { width, height } = pageBox(page, "options.page");
|
package/lib/paint.js
CHANGED
|
@@ -123,7 +123,9 @@ let loadFaces = async (fonts) => {
|
|
|
123
123
|
};
|
|
124
124
|
|
|
125
125
|
// Every bitmap decoded so far, by the bytes it was decoded from: one logo on
|
|
126
|
-
// every page is decoded once.
|
|
126
|
+
// every page is decoded once. A rejection is remembered too, so a file the
|
|
127
|
+
// browser refused is refused from memory ever after — a caller repainting the
|
|
128
|
+
// same page never pays for a second decode of bytes that will not decode.
|
|
127
129
|
/** @type {WeakMap<Uint8Array, Promise<ImageBitmap>>} */
|
|
128
130
|
let BITMAPS = new WeakMap();
|
|
129
131
|
|
|
@@ -204,17 +206,30 @@ let PAINTERS = { rect: paintRect, line: paintLine, text: paintText, mark: paintM
|
|
|
204
206
|
|
|
205
207
|
/** @type {(ctx: CanvasRenderingContext2D, op: Op, bitmaps: Map<Op, ImageBitmap>) => void} */
|
|
206
208
|
let paintOp = (ctx, op, bitmaps) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
// An image with no bitmap did not decode, and is drawn as nothing. The map
|
|
210
|
+
// is the one place that is known: `decode` keeps no entry for a file the
|
|
211
|
+
// browser refused, so there is no second flag to read and no way to ask
|
|
212
|
+
// this question twice.
|
|
213
|
+
if (op.kind === "image") {
|
|
214
|
+
let bitmap = bitmaps.get(op);
|
|
215
|
+
if (bitmap) ctx.drawImage(bitmap, op.x, op.y, op.w, op.h);
|
|
216
|
+
} else PAINTERS[op.kind](ctx, op);
|
|
210
217
|
};
|
|
211
218
|
|
|
212
|
-
// Every image on the page, decoded together rather than one after another
|
|
219
|
+
// Every image on the page, decoded together rather than one after another,
|
|
220
|
+
// and each settling on its own: gathering with `Promise.all` made one file the
|
|
221
|
+
// browser refused the whole page's failure, thrown before the first draw op.
|
|
222
|
+
// `paint`'s own doc says why that is the wrong price. An image that did not
|
|
223
|
+
// decode has no entry here, which is how `paintOp` knows.
|
|
213
224
|
/** @type {(page: Page) => Promise<Map<Op, ImageBitmap>>} */
|
|
214
225
|
let decode = async (page) => {
|
|
215
226
|
let images = page.ops.filter((op) => op.kind === "image");
|
|
216
|
-
let bitmaps = await Promise.all(images.map(bitmapOf));
|
|
217
|
-
return new Map(
|
|
227
|
+
let bitmaps = await Promise.all(images.map((op) => bitmapOf(op).catch(() => null)));
|
|
228
|
+
return new Map(
|
|
229
|
+
/** @type {[Op, ImageBitmap][]} */ (
|
|
230
|
+
images.map((op, i) => [op, bitmaps[i]]).filter(([, bitmap]) => bitmap)
|
|
231
|
+
),
|
|
232
|
+
);
|
|
218
233
|
};
|
|
219
234
|
|
|
220
235
|
/**
|
|
@@ -230,6 +245,14 @@ let decode = async (page) => {
|
|
|
230
245
|
* superseded call reach a canvas still on screen — which is what the viewer's
|
|
231
246
|
* stage retires a page for (docs/adr/0046).
|
|
232
247
|
*
|
|
248
|
+
* **An image the browser will not decode is drawn as nothing, and the page is
|
|
249
|
+
* drawn around it.** The engine vouched for the magic numbers and the layout
|
|
250
|
+
* read the size out of the header, so a file corrupt past that point is not
|
|
251
|
+
* known to be bad until here; costing the whole page for it — every other op
|
|
252
|
+
* and the marking with it — is a worse answer than costing the image. What is
|
|
253
|
+
* lost is what could not be drawn. A caller wanting the failure instead should
|
|
254
|
+
* decode before it paints.
|
|
255
|
+
*
|
|
233
256
|
* @param {CanvasRenderingContext2D} ctx The context to paint on.
|
|
234
257
|
* @param {Page} page One page of a `Layout`.
|
|
235
258
|
* @param {{ scale?: number, fonts?: any }} [options]
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything that holds for a whole render and is not geometry — the render's
|
|
3
|
+
* `Settings`, and the one reading of what a report default amounts to.
|
|
4
|
+
*
|
|
5
|
+
* A `Frame` (canvas.js) is a page's geometry; a `Settings` is the other half:
|
|
6
|
+
* the faces to measure against, the report default narrowed to `base` and
|
|
7
|
+
* `family`, and the three intl facts a formatted value resolves in. A canvas
|
|
8
|
+
* holds one by reference, so a measuring canvas built for a probe reads exactly
|
|
9
|
+
* what the listing canvas reads and a band cannot be reserved against one
|
|
10
|
+
* locale and drawn in another. This module owns what a settings *is* and how a
|
|
11
|
+
* report's opening event becomes one; canvas.js draws with it, text.js measures
|
|
12
|
+
* against it, and neither has to know what `style.size` or `style.family`
|
|
13
|
+
* amount to.
|
|
14
|
+
*/
|
|
15
|
+
import { familyName } from "./fonts.js";
|
|
16
|
+
import { sizeOf } from "./style.js";
|
|
17
|
+
|
|
18
|
+
// This layout's baseline type size. Not a host option: a document's type size
|
|
19
|
+
// is the document's own, so it is `style.size` on the report and the number
|
|
20
|
+
// here is only what text renders at when nothing declares one. The XLSX target
|
|
21
|
+
// carries the same 10 for the same reason (docs/adr/0014, docs/adr/0033).
|
|
22
|
+
let BASE = 10;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Everything that holds for a whole render and is not geometry: the faces to
|
|
26
|
+
* measure against, the report default narrowed to `family` and `size` (landing
|
|
27
|
+
* in `family` and `base` here), and the three intl facts a formatted value
|
|
28
|
+
* resolves in.
|
|
29
|
+
*
|
|
30
|
+
* A canvas holds one of these by reference, never a copy, which is the whole
|
|
31
|
+
* reason it is an object. A measuring canvas built off the same settings reads
|
|
32
|
+
* exactly what the listing canvas reads, so a page band cannot be reserved
|
|
33
|
+
* against one locale and drawn in another — an agreement that used to rest on
|
|
34
|
+
* a hand-written copy staying in step.
|
|
35
|
+
*
|
|
36
|
+
* `family` is null when the report declares none, and each of the intl three is
|
|
37
|
+
* undefined when the engine settled none.
|
|
38
|
+
*
|
|
39
|
+
* @typedef {{ fonts: import('./fonts.js').Fonts, base: number,
|
|
40
|
+
* family: string | null, locale?: string, currency?: string,
|
|
41
|
+
* timeZone?: string }} Settings
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The render's settings, complete by construction from the opening event the
|
|
46
|
+
* target already peeked. The report default is narrowed to `family` and `size`,
|
|
47
|
+
* and each replaces this target's own baseline outright: row heights and band
|
|
48
|
+
* gaps scale with the document's type rather than staying at a size nothing is
|
|
49
|
+
* set in, and text declaring no family is set in the document's. Reading the
|
|
50
|
+
* pair here is the whole of this target's reading of docs/adr/0033 — the default
|
|
51
|
+
* reaches a node as a fallback the settings carry, never as a layer merged into
|
|
52
|
+
* its style, so a document-wide fact costs no allocation however many cells a
|
|
53
|
+
* report has.
|
|
54
|
+
*
|
|
55
|
+
* `sizeOf` and `familyName` are this target's one reading each of what a
|
|
56
|
+
* declared size and family amount to, so they read the default here too — both
|
|
57
|
+
* reach this unchecked from a computed style, and two spellings of that
|
|
58
|
+
* leniency would drift. Each falls back to this target's own baseline, so a
|
|
59
|
+
* default declaring one of the pair leaves the other at the baseline, and one
|
|
60
|
+
* whose value is unusable leaves the baseline standing.
|
|
61
|
+
*
|
|
62
|
+
* Built once and never settled again: these hold for a whole render, so there
|
|
63
|
+
* is no event that could set them a second time — a second settling is not
|
|
64
|
+
* refused, it is unexpressible. `opening` is null when the stream is empty (no
|
|
65
|
+
* report to read a default from), which yields the bare baseline: the target's
|
|
66
|
+
* own size, no family, and the intl three unset. Those three are read straight
|
|
67
|
+
* off the event whether or not it declares them — a formatted value is the only
|
|
68
|
+
* thing that reads them, and an empty stream produces none, so an unset one
|
|
69
|
+
* never reaches `Intl`.
|
|
70
|
+
*
|
|
71
|
+
* @param {import('./fonts.js').Fonts} fonts The loaded faces.
|
|
72
|
+
* @param {any} [opening] The peeked `report-start` event, or null.
|
|
73
|
+
* @returns {Settings}
|
|
74
|
+
*/
|
|
75
|
+
let settings = (fonts, opening) => {
|
|
76
|
+
let event = opening || {};
|
|
77
|
+
let style = event.style || {};
|
|
78
|
+
return {
|
|
79
|
+
fonts,
|
|
80
|
+
base: sizeOf(style, BASE),
|
|
81
|
+
family: familyName(style) || null,
|
|
82
|
+
locale: event.locale,
|
|
83
|
+
currency: event.currency,
|
|
84
|
+
timeZone: event.timeZone,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export { settings };
|
package/lib/style.js
CHANGED
|
@@ -69,8 +69,7 @@ let sizeOf = (style, base) => (Number.isFinite(style.size) && style.size > 0 ? s
|
|
|
69
69
|
// Style blocks layer outward-in: row under cell, split under slot. Those are
|
|
70
70
|
// the two innermost of the four layers; the outer two reach a node without
|
|
71
71
|
// being merged into it -- the band-role default through `roled` below, and the
|
|
72
|
-
// report default through the render's settings, as `
|
|
73
|
-
// `adoptSettings` explains.
|
|
72
|
+
// report default through the render's settings, as `settings.js` explains.
|
|
74
73
|
/** @type {(under: any, over: any) => any} */
|
|
75
74
|
let merge = (under, over) => (under ? (over ? { ...under, ...over } : under) : over || {});
|
|
76
75
|
|
package/lib/text.js
CHANGED
|
@@ -9,10 +9,10 @@ import { LEAD, col, dressed, sizeOf, upper } from "./style.js";
|
|
|
9
9
|
|
|
10
10
|
// What measuring needs and no more, and it is exactly the render's settings:
|
|
11
11
|
// the faces to measure against, the base size and family a style falls back to,
|
|
12
|
-
// and the intl three a formatted value resolves in. Declared
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
/** @typedef {import('./
|
|
12
|
+
// and the intl three a formatted value resolves in. Declared in `settings.js`,
|
|
13
|
+
// the module that owns them; a caller hands over `canvas.settings` rather than
|
|
14
|
+
// the canvas, so nothing here can touch a page.
|
|
15
|
+
/** @typedef {import('./settings.js').Settings} Settings */
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* @typedef {{ text: string, font: any, size: number, color: any,
|
|
@@ -29,7 +29,7 @@ import { LEAD, col, dressed, sizeOf, upper } from "./style.js";
|
|
|
29
29
|
/** @type {(token: any, style: any, settings: Settings) => string} */
|
|
30
30
|
let rawOf = (token, style, settings) => {
|
|
31
31
|
if ("literal" in token) return token.literal;
|
|
32
|
-
return format(token.value, style
|
|
32
|
+
return format(token.value, style, settings) ?? display(token.value);
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// This layout has no text-transform to defer to, so `uppercase` is applied to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/layout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The paged display list for quario — the layout the PDF target writes and the viewer paints — in the makings, not yet released",
|
|
5
5
|
"homepage": "https://getquario.com",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
44
44
|
"@types/fontkit": "^2.0.9",
|
|
45
45
|
"fontkit": "^2.0.4",
|
|
46
|
-
"quario": "^0.
|
|
46
|
+
"quario": "^0.6.0",
|
|
47
47
|
"size-limit": "^13.0.3",
|
|
48
48
|
"typescript": "^7.0.2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"fontkit": "^2.0.4",
|
|
52
|
-
"quario": "^0.
|
|
52
|
+
"quario": "^0.6.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"fontkit": {
|