@quario/pdf 0.1.0 → 0.2.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 CHANGED
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-09-01
11
+
12
+ ### Added
13
+
14
+ - **The report default sets the document's face and base size.** A report's
15
+ top-level `style` replaces this target's own baseline, so its `size` scales
16
+ row heights and band gaps with the type rather than leaving them at a size
17
+ nothing is set in. It is the layer _under_ the band-role defaults: a report
18
+ declaring `size: 12` still renders its report header at 14, and an item's own
19
+ style wins over both.
20
+
21
+ - **`uppercase` draws capitals.** With no text-transform to defer to, this
22
+ target capitalises the string before measuring it, so wrapping and column
23
+ widths are those of the text actually drawn. The mapping is Unicode default
24
+ case, never the host's locale, so output stays byte-reproducible.
25
+
26
+ - **Splits lay out across the content width.** Each slot is measured and
27
+ wrapped inside its own share, all slots draw from a common top, and the
28
+ split takes the height of its tallest slot so the band below clears them
29
+ all. **A split is never broken across a page**: one that does not fit the
30
+ remaining height moves whole, exactly as an image does, and one taller than
31
+ any page renders in full past the bottom margin. The split's own style is
32
+ the layer under each slot's, and its `background` fills the whole split
33
+ behind them.
34
+
35
+ ### Changed
36
+
37
+ - **A bare `Date` draws as ISO 8601 UTC, the same on every machine.** Cell
38
+ text and outline bookmark titles for `Date` values used `String(date)`,
39
+ which bakes the host's timezone and locale into the document — at odds with
40
+ this target's byte-reproducibility guarantee. Both now render through the
41
+ engine's shared display rule, so a `Date` group key titles its bookmark
42
+ with the same ISO text its cells draw.
43
+
44
+ ### Removed
45
+
46
+ - **`options.baseSize`.** A document's type size is the document's own, so it
47
+ is the report's `style.size` — portable, travelling with the definition to
48
+ every target — rather than a host option one target honoured. Text with
49
+ nothing declared still renders at 10 points. Replace `pdf({ baseSize: 11 })`
50
+ with `"style": { "size": 11 }` on the report.
51
+
10
52
  ## [0.1.0] - 2026-08-27
11
53
 
12
54
  ### Added
package/README.md CHANGED
@@ -89,7 +89,6 @@ Page geometry is target configuration, never schema:
89
89
  ```js
90
90
  {
91
91
  page: { size: "A4", margin: 54 }, // 'A4' | 'letter' | [width, height] in points
92
- baseSize: 10, // base font size in points
93
92
  meta: { title, author, subject }, // optional /Info — strings only, never a date
94
93
  fonts: { Inter: { regular, bold, italic, boldItalic } }, // TrueType bytes
95
94
  }
@@ -97,8 +96,9 @@ Page geometry is target configuration, never schema:
97
96
 
98
97
  `size` defaults to `A4` (595.28 × 841.89 pt); an array is a custom `[width, height]` in points. An
99
98
  unknown size name is a definition error, thrown by the factory. `margin` applies to all four sides and defaults to 54
100
- (0.75 in). `baseSize` defaults to 10 and style `size` declarations override it per element; line
101
- leading is 1.4× a line's largest font size.
99
+ (0.75 in). Text renders at 10pt when nothing declares a size: a document's type size is the
100
+ document's own, so it is the report's `style.size`, not a host option. Line leading is 1.4× a line's
101
+ largest font size.
102
102
 
103
103
  ## Layout
104
104
 
package/lib/balance.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // The one number a balanced region decides: how tall each of its strips is
2
- // (ADR 0020 — balancing is a strip height, not a pass). Pure arithmetic over
2
+ // (ADR 0027 — balancing is a strip height, not a pass). Pure arithmetic over
3
3
  // what the buffer holds, so nothing here reaches a canvas or the band flow.
4
4
  //
5
5
  // Balancing by height alone under-fills, because most of what a region holds
package/lib/canvas.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * mid-measure and return a silently wrong height, so `newPage` throws there.
13
13
  */
14
14
  import { degrees, drawImage, drawText, rgb } from "pdf-lib";
15
- import { BLACK, shift } from "./style.js";
15
+ import { BLACK, dressed, shift } from "./style.js";
16
16
 
17
17
  // The operator builder wants explicit rotation and skew; report text has none.
18
18
  let NO_TURN = degrees(0);
@@ -23,24 +23,32 @@ let NO_TURN = degrees(0);
23
23
  /** @typedef {import('pdf-lib').PDFFont} PDFFont */
24
24
  /** @typedef {import('pdf-lib').PDFName} PDFName */
25
25
 
26
- // The page box and the content box. Fixed for the whole render, from the moment
27
- // it commits to a page: the page bands are measured against the page box and
28
- // take their height off `top`/`bottom` through `adopt` below, while the first
29
- // page is still untouched.
26
+ // The page box and the content box. Settled at `report-start` and fixed
27
+ // thereafter: the geometry is the factory's and never moves, while `base` and
28
+ // `style` take the report default one event into the render, before anything is
29
+ // measured. The page bands are measured against the page box and take their
30
+ // height off `top`/`bottom` through `adopt` below, while the first page is
31
+ // still untouched.
30
32
  /**
33
+ * `style` is the report default — the outermost style layer, resolved once per
34
+ * render from `report-start` and carried here so a measuring canvas built off
35
+ * this frame reads the same one the drawing canvas does. Null when the report
36
+ * declares none.
37
+ *
31
38
  * @typedef {{ width: number, height: number, margin: number, base: number,
32
- * content: number, top: number, bottom: number }} Frame
39
+ * content: number, top: number, bottom: number, style: any }} Frame
33
40
  */
34
41
 
35
42
  // A frame from the page box and the base size: how `content`/`top`/`bottom`
36
43
  // fall out of a page and a margin is derived here, once, so no caller and no
37
44
  // suite has to restate it and drift from what a real render uses.
38
- /** @type {(width: number, height: number, margin: number, base: number) => Frame} */
39
- let frame = (width, height, margin, base) => ({
45
+ /** @type {(width: number, height: number, margin: number, base: number, style?: any) => Frame} */
46
+ let frame = (width, height, margin, base, style = null) => ({
40
47
  width,
41
48
  height,
42
49
  margin,
43
50
  base,
51
+ style,
44
52
  content: width - 2 * margin,
45
53
  top: height - margin,
46
54
  bottom: margin,
@@ -84,17 +92,11 @@ let GREY = rgb(0.5, 0.5, 0.5);
84
92
  // line's ascender (the face metric already measured for baseline placement).
85
93
  // Empty lines (no width) draw nothing. Shared by the drawing adapter and the
86
94
  // layout recorder so both exercise the same path.
87
- /** @type {{ flag: 'underline' | 'strikethrough', at: (baseline: number, asc: number) => number }[]} */
88
- let DECO = [
89
- { flag: "underline", at: (baseline, asc) => baseline - asc * 0.12 },
90
- { flag: "strikethrough", at: (baseline, asc) => baseline + asc * 0.35 },
91
- ];
92
-
93
95
  /** @type {(line: Line) => any} */
94
96
  let ink = (line) => (line.pieces[0] && line.pieces[0].color) || BLACK;
95
97
 
96
98
  /** @type {(line: Line) => boolean} */
97
- let wantsDeco = (line) => !!(line.w && (line.underline || line.strikethrough));
99
+ let wantsDeco = (line) => !!line.w && dressed(line);
98
100
 
99
101
  /**
100
102
  * @type {(stroke: (x1: number, x2: number, y: number, thickness: number, color: any) => void,
@@ -105,9 +107,8 @@ let decorateLine = (stroke, line, left, baseline) => {
105
107
  let thickness = Math.max(line.asc / 12, 0.5);
106
108
  let right = left + line.w;
107
109
  let color = ink(line);
108
- for (let { flag, at } of DECO) {
109
- if (line[flag]) stroke(left, right, at(baseline, line.asc), thickness, color);
110
- }
110
+ if (line.underline) stroke(left, right, baseline - line.asc * 0.12, thickness, color);
111
+ if (line.strikethrough) stroke(left, right, baseline + line.asc * 0.35, thickness, color);
111
112
  };
112
113
 
113
114
  /**
package/lib/fonts.js CHANGED
@@ -49,8 +49,6 @@ let useFontkit = async (doc) => {
49
49
  doc.registerFontkit(fontkit);
50
50
  };
51
51
 
52
- let VARIANTS = ["regular", "bold", "italic", "boldItalic"];
53
-
54
52
  /** @type {(doc: any) => Promise<Record<string, any[]>>} */
55
53
  let embedBase = async (doc) => {
56
54
  /** @type {Record<string, any[]>} */
@@ -91,7 +89,8 @@ let familyOf = async (doc, name, def) => {
91
89
  let path = asFamily(name, def);
92
90
  /** @type {any[]} */
93
91
  let faces = [];
94
- for (let variant of VARIANTS) faces.push(await embedVariant(doc, def, path, variant, faces));
92
+ for (let variant of ["regular", "bold", "italic", "boldItalic"])
93
+ faces.push(await embedVariant(doc, def, path, variant, faces));
95
94
  return faces;
96
95
  };
97
96
 
package/lib/image.js CHANGED
@@ -18,9 +18,6 @@ let PER_PX = 72 / 96;
18
18
  /** @type {(bytes: Uint8Array, at: number) => number} */
19
19
  let word = (bytes, at) => (bytes[at] << 8) | bytes[at + 1];
20
20
 
21
- /** @type {(bytes: Uint8Array) => { w: number, h: number }} */
22
- let pngSize = (bytes) => ({ w: word(bytes, 18), h: word(bytes, 22) });
23
-
24
21
  /** @type {(code: number) => boolean} */
25
22
  let inSof = (code) => code >= 0xc0 && code <= 0xcf;
26
23
 
@@ -42,9 +39,6 @@ let jpegSize = (bytes) => {
42
39
  return { w: 0, h: 0 };
43
40
  };
44
41
 
45
- /** @type {(bytes: Uint8Array, format: string) => { w: number, h: number }} */
46
- let pixels = (bytes, format) => (format === "png" ? pngSize(bytes) : jpegSize(bytes));
47
-
48
42
  /**
49
43
  * The image's intrinsic size in points. The format is the engine's sniff,
50
44
  * riding on the event, so nothing here decides it a second time.
@@ -54,7 +48,8 @@ let pixels = (bytes, format) => (format === "png" ? pngSize(bytes) : jpegSize(by
54
48
  * @returns {{ w: number, h: number }} The size, in points.
55
49
  */
56
50
  export let intrinsic = (bytes, format) => {
57
- let { w, h } = pixels(bytes, format);
51
+ // A PNG carries the two numbers in its IHDR at a fixed offset.
52
+ let { w, h } = format === "png" ? { w: word(bytes, 18), h: word(bytes, 22) } : jpegSize(bytes);
58
53
  // The engine vouched for the magic numbers, not for the rest of the file:
59
54
  // a truncated header reaches here as a zero, and failing loudly beats
60
55
  // drawing an image with no size (SCHEMA.md, "Image item").
package/lib/index.d.ts CHANGED
@@ -26,8 +26,6 @@ export interface PdfFontFamily {
26
26
  /** Host controls, taken and validated at the factory call. */
27
27
  export interface PdfOptions {
28
28
  page?: PdfPage;
29
- /** Base font size in points — the size styles override per element. Default `10`. */
30
- baseSize?: number;
31
29
  meta?: PdfMeta;
32
30
  /**
33
31
  * TrueType families to embed (subset), selected from styles by
package/lib/index.js CHANGED
@@ -36,29 +36,17 @@ let err = (msg) => {
36
36
  throw Error(msg);
37
37
  };
38
38
 
39
- /** @type {(value: any) => boolean} */
40
- let finite = (value) => typeof value === "number" && Number.isFinite(value);
41
-
42
39
  /** @type {(value: number) => boolean} */
43
40
  let positive = (value) => Number.isFinite(value) && value > 0;
44
41
 
45
- /** @type {(options: any) => any} */
46
- let pageOf = (options) => options?.page ?? {};
47
-
48
- /** @type {(options: any) => { meta: any, fonts: any }} */
49
- let host = (options) => ({ meta: options?.meta, fonts: options?.fonts });
50
-
51
42
  /** @type {(size: any) => any} */
52
- let named = (size) => {
43
+ let named = (size = "A4") => {
53
44
  let dimensions = Array.isArray(size) ? size : SIZES[size];
54
45
  // oxlint-disable-next-line no-unused-expressions
55
46
  dimensions || err('options.page.size: unknown page size "' + size + '"');
56
47
  return dimensions;
57
48
  };
58
49
 
59
- /** @type {(page: any) => any} */
60
- let pageSize = (page) => named(page.size ?? "A4");
61
-
62
50
  /** @type {(dimensions: any) => { width: number, height: number }} */
63
51
  let pair = (dimensions) => {
64
52
  let width = +dimensions[0],
@@ -69,26 +57,19 @@ let pair = (dimensions) => {
69
57
  return { width, height };
70
58
  };
71
59
 
72
- /** @type {(page: any) => any} */
73
- let marginArg = (page) => page.margin ?? 54;
74
-
75
60
  /** @type {(margin: any, width: number, height: number) => number} */
76
- let marginOf = (margin, width, height) => {
77
- let fits = finite(margin) && margin >= 0 && 2 * margin < Math.min(width, height);
61
+ let marginOf = (margin = 54, width, height) => {
62
+ let fits = Number.isFinite(margin) && margin >= 0 && 2 * margin < Math.min(width, height);
78
63
  // oxlint-disable-next-line no-unused-expressions
79
64
  fits || err("options.page.margin: expected a non-negative number smaller than half the page");
80
65
  return margin;
81
66
  };
82
67
 
83
- /** @type {(options: any) => any} */
84
- let baseArg = (options) => options?.baseSize ?? 10;
85
-
86
- /** @type {(base: any) => number} */
87
- let baseOf = (base) => {
88
- // oxlint-disable-next-line no-unused-expressions
89
- (finite(base) && base > 0) || err("options.baseSize: expected a positive number of points");
90
- return base;
91
- };
68
+ // This target's baseline type size. Not a host option: a document's type size
69
+ // is the document's own, so it is `style.size` on the report and the number
70
+ // here is only what text renders at when nothing declares one. The XLSX target
71
+ // carries the same 10 for the same reason (docs/adr/0014, docs/adr/0033).
72
+ let BASE = 10;
92
73
 
93
74
  // The page box and the content box in one, settled here beside the validation
94
75
  // that produced them. This frame never moves at all: it serves every render the
@@ -97,30 +78,27 @@ let baseOf = (base) => {
97
78
  // band flow's, off that render's own canvas.
98
79
  /** @type {(options: any) => import('./canvas.js').Frame} */
99
80
  let geometry = (options) => {
100
- let page = pageOf(options);
101
- let { width, height } = pair(pageSize(page));
102
- return frame(width, height, marginOf(marginArg(page), width, height), baseOf(baseArg(options)));
103
- };
104
-
105
- let META = [
106
- ["title", "setTitle"],
107
- ["author", "setAuthor"],
108
- ["subject", "setSubject"],
109
- ];
110
-
111
- /** @type {(doc: any, meta: any, entry: string[]) => void} */
112
- let field = (doc, meta, [key, method]) => {
113
- if (typeof meta[key] === "string") doc[method](meta[key]);
81
+ let { page = {} } = options ?? {};
82
+ let { width, height } = pair(named(page.size));
83
+ return frame(width, height, marginOf(page.margin, width, height), BASE);
114
84
  };
115
85
 
116
86
  // Optional document information. Never a date: pdf-lib stamps the current time
117
87
  // unless told otherwise, and a timestamp would make the same input render
118
- // different bytes on every run.
88
+ // different bytes on every run. Each key names the pdf-lib setter it feeds,
89
+ // here rather than in a table, and a value of any other shape is left off.
119
90
  /** @type {(doc: any, meta: any) => void} */
120
91
  let describe = (doc, meta) => {
121
92
  doc.setCreationDate(new Date(0));
122
93
  doc.setModificationDate(new Date(0));
123
- if (meta) for (let entry of META) field(doc, meta, entry);
94
+ let field = (/** @type {string} */ method, /** @type {any} */ value) => {
95
+ if (typeof value === "string") doc[method](value);
96
+ };
97
+ if (meta) {
98
+ field("setTitle", meta.title);
99
+ field("setAuthor", meta.author);
100
+ field("setSubject", meta.subject);
101
+ }
124
102
  };
125
103
 
126
104
  /** @type {(canvas: import('./canvas.js').Drawing, draw: (i: number) => void) => Promise<void>} */
@@ -132,9 +110,6 @@ let overPages = async (canvas, draw) => {
132
110
  }
133
111
  };
134
112
 
135
- /** @type {(opening: any) => { bands: any, marking: any }} */
136
- let openingOf = (opening) => ({ bands: opening?.page, marking: opening?.marking });
137
-
138
113
  /** @type {(canvas: import('./canvas.js').Drawing, bands: any,
139
114
  * pages: { number: number, total: number }[]) => Promise<void>} */
140
115
  let furnish = async (canvas, bands, pages) => {
@@ -162,7 +137,8 @@ let markPages = async (canvas, marking) => {
162
137
  */
163
138
  export function pdf(options) {
164
139
  let geo = geometry(options);
165
- let { meta, fonts: custom } = host(options);
140
+ let meta = options?.meta,
141
+ custom = options?.fonts;
166
142
  /** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
167
143
  let compile = (stream) => async (data) => {
168
144
  let doc = await PDFDocument.create();
@@ -179,12 +155,13 @@ export function pdf(options) {
179
155
  // page bands off it — and hands it back with the marks. What is left on it
180
156
  // is what the passes below want: the band closures to render per page, and
181
157
  // the marking's wording to stamp.
158
+ // Null when nothing settled one, which reads as a document owing neither.
182
159
  let { marks, opening, pages } = finish();
183
- let { bands, marking } = openingOf(opening);
160
+ opening = opening || {};
184
161
  // The passes below run over the finished pages, not the stream — no walk
185
162
  // at all — so they open each page themselves and breathe on their own
186
163
  // rather than through the driver.
187
- await furnish(canvas, bands, pages);
164
+ await furnish(canvas, opening.page, pages);
188
165
  // Every image the walk and the furniture pass placed, embedded and drawn
189
166
  // now: a walk handler cannot await, and the page bands' images are not
190
167
  // placed until the pass above has run. Before the marking, so an image can
@@ -193,7 +170,7 @@ export function pdf(options) {
193
170
  // The unlicensed marking goes on last, over content and page furniture
194
171
  // alike, once per page (LICENSE section 6). Its wording rode in on
195
172
  // `report-start`; only the placement is this target's.
196
- await markPages(canvas, marking);
173
+ await markPages(canvas, opening.marking);
197
174
  outline(doc, marks, canvas.refs);
198
175
  return doc.save();
199
176
  };
package/lib/layout.js CHANGED
@@ -21,11 +21,11 @@
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 { isReportBand, text } from "quario";
24
+ import { display, isReportBand, text } from "quario";
25
25
  import { balance } from "./balance.js";
26
26
  import { adopt, frame, measuring } from "./canvas.js";
27
27
  import { intrinsic } from "./image.js";
28
- import { BAND, GUTTER, LEAD, PADX, PADY, col, merge, roled, shift } from "./style.js";
28
+ import { BAND, GUTTER, LEAD, PADX, PADY, col, roled, shift, sizeOf, stack } from "./style.js";
29
29
  import { atoms, dress, heightOf, wrap } from "./text.js";
30
30
 
31
31
  /** @typedef {import('./balance.js').Unit} Unit */
@@ -57,27 +57,38 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
57
57
  * @typedef {{ bytes: Uint8Array, format: string, w: number, h: number }} Picture
58
58
  */
59
59
  /**
60
- * @typedef {{ lines: Line[], picture?: Picture, bg: string | null, align: any,
61
- * h: number }} Block
60
+ * `align` is absent on a split, which has none of its own: alignment is a
61
+ * slot's, reached through the style layering.
62
+ * @typedef {{ lines: Line[], picture?: Picture, parts?: SlotPart[],
63
+ * bg: string | null, align?: any, h: number }} Block
64
+ */
65
+ /**
66
+ * One slot of a split, as laid out: the block it drew to and the width it was
67
+ * measured at. There is no offset — a slot's is the sum of the widths before
68
+ * it, which `drawSplit` accumulates as it walks them.
69
+ * @typedef {{ block: Block, w: number }} SlotPart
62
70
  */
63
71
 
64
72
  // How `fit` sizes a picture against the width it has: `natural` is the
65
73
  // image's own size in points, never wider than the content box; `width`
66
74
  // scales to the content box either way. The ratio is kept in both, so a
67
75
  // height is never anything but the width's consequence.
68
- /** @type {(canvas: Canvas, event: any, avail: number) => Picture} */
69
- let pictureOf = (canvas, event, avail) => {
76
+ /** @type {(event: any, avail: number) => Picture} */
77
+ let pictureOf = (event, avail) => {
70
78
  let { w, h } = intrinsic(event.bytes, event.format);
71
79
  let width = event.fit === "width" ? avail : Math.min(w, avail);
72
80
  return { bytes: event.bytes, format: event.format, w: width, h: (h * width) / w };
73
81
  };
74
82
 
75
- /** @type {(canvas: Canvas, event: any, avail: number) => Block} */
76
- let blockOf = (canvas, event, avail) => {
77
- let style = merge(null, event.style);
83
+ // `under` is the enclosing style a slot's own layers over — a split's, when
84
+ // this is one of its slots. Null everywhere else, where nothing encloses.
85
+ /** @type {(canvas: Canvas, event: any, avail: number, under?: any) => Block} */
86
+ let blockOf = (canvas, event, avail, under = null) => {
87
+ let style = stack(canvas, under, event.style);
78
88
  let bg = col(style.background);
89
+ if (event.type === "split") return splitBlock(canvas, event, avail, style, bg);
79
90
  if (event.type === "image") {
80
- let picture = pictureOf(canvas, event, avail);
91
+ let picture = pictureOf(event, avail);
81
92
  return { lines: [], picture, bg, align: style.align, h: picture.h };
82
93
  }
83
94
  let lines = dress(wrap(canvas, atoms(canvas, event.tokens, style), Math.max(avail, 1)), style);
@@ -118,9 +129,99 @@ let drawLines = (canvas, block, x, avail) => {
118
129
  canvas.fresh = false;
119
130
  };
120
131
 
132
+ // An authored share fixes its slot; the width-less ones divide what is left,
133
+ // evenly. Evenly rather than by natural width, because a split is a line of
134
+ // furniture whose geometry should read off the document — the same answer the
135
+ // HTML target's `flex:1` gives, so one definition places alike in both.
136
+ // Each slot is measured at its own share and drawn from a common top, so the
137
+ // split is as tall as its tallest slot and the line's geometry does not move
138
+ // with the data. The split's own declarations are the layer under each slot's,
139
+ // so a split styled `bold` reads the same here as it does through CSS
140
+ // inheritance in the HTML target and the `under` layer in XLSX. It has no
141
+ // `align` of its own: alignment is a slot's, reached through that layering.
142
+ /**
143
+ * @type {(canvas: Canvas, event: any, avail: number, style: any,
144
+ * bg: string | null) => Block}
145
+ */
146
+ let splitBlock = (canvas, event, avail, style, bg) => {
147
+ let widths = slotWidths(event.slots, avail);
148
+ /** @type {SlotPart[]} */
149
+ let parts = [];
150
+ let h = 0;
151
+ for (let [i, slot] of event.items.entries()) {
152
+ let block = blockOf(canvas, slot, Math.max(widths[i], 1), style);
153
+ if (block.h > h) h = block.h;
154
+ parts.push({ block, w: widths[i] });
155
+ }
156
+ return { lines: [], parts, bg, h };
157
+ };
158
+
159
+ /** @type {(slots: { width?: number }[], avail: number) => number[]} */
160
+ let slotWidths = (slots, avail) => {
161
+ let autos = 0,
162
+ spent = 0;
163
+ /** @type {(number | null)[]} */
164
+ let fixed = slots.map(({ width }) => {
165
+ // Bound to a local first: `Number.isFinite` does not narrow a property.
166
+ if (width === undefined || !Number.isFinite(width)) return (autos++, null);
167
+ let share = (width / 100) * avail;
168
+ spent += share;
169
+ return share;
170
+ });
171
+ let each = autos ? Math.max(avail - spent, 0) / autos : 0;
172
+ return fixed.map((width) => width ?? each);
173
+ };
174
+
175
+ // A split reaches this target as a bracket around ordinary item and image
176
+ // events. Folding it into one event is what lets the rest of the target treat
177
+ // it as a single block -- measured, kept together, and paged by exactly the
178
+ // machinery every other block goes through. Stateful rather than a transform
179
+ // over an array, because the body walk is streaming; the page-band pass drives
180
+ // the same folder over its own list, so the two cannot drift.
181
+ // The event types that become a `Block`. They are also exactly the three the
182
+ // split folder owns: item and image reach `route` through it, so a slot's are
183
+ // collected rather than placed, and `split` is minted by the folder and never
184
+ // arrives from the stream at all.
185
+ let BLOCKS = new Set(["item", "image", "split"]);
186
+
187
+ /** @type {(emit: (event: any) => void) => (event: any) => void} */
188
+ let splitFolder = (emit) => {
189
+ /** @type {{ event: any, items: any[] } | null} */
190
+ let bracket = null;
191
+ // The engine emits the bracket as one array, so `split-end` always has its
192
+ // opening and this never reads a null.
193
+ let close = () => {
194
+ let { event, items } = /** @type {{ event: any, items: any[] }} */ (bracket);
195
+ bracket = null;
196
+ emit({ type: "split", role: event.role, style: event.style, slots: event.slots, items });
197
+ };
198
+ return (event) => {
199
+ if (event.type === "split-start") bracket = { event, items: [] };
200
+ else if (event.type === "split-end") close();
201
+ else if (bracket) bracket.items.push(event);
202
+ else emit(event);
203
+ };
204
+ };
205
+
206
+ /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
207
+ let drawSplit = (canvas, block, x, avail) => {
208
+ if (block.bg) canvas.rect(block.bg, x, canvas.y - block.h, avail, block.h);
209
+ let top = canvas.y;
210
+ let at = x;
211
+ for (let part of block.parts || []) {
212
+ canvas.y = top;
213
+ drawBlock(canvas, part.block, at, part.w);
214
+ at += part.w;
215
+ }
216
+ // The split takes its own height whatever the slots took, so a short slot
217
+ // beside a wrapped one does not pull the band up.
218
+ canvas.y = top - block.h;
219
+ canvas.fresh = false;
220
+ };
221
+
121
222
  /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
122
223
  let drawBlock = (canvas, block, x, avail) =>
123
- (block.picture ? drawPicture : drawLines)(canvas, block, x, avail);
224
+ (block.parts ? drawSplit : block.picture ? drawPicture : drawLines)(canvas, block, x, avail);
124
225
 
125
226
  // A region is a run of strips: what a columned node's content flows down
126
227
  // (CONTEXT.md "Strip"). `index` is the strip being filled, `top` the y every
@@ -229,17 +330,23 @@ let fitLines = (canvas, block, i, floor, fresh) => {
229
330
  /** @type {(canvas: Canvas, block: Block, floor: number) => boolean} */
230
331
  let whole = (canvas, block, floor) => canvas.y - block.h >= floor;
231
332
 
232
- // An image is never split: it moves whole to a fresh column. One that no
233
- // column can hold is scaled down to the one it lands on -- obeying the
234
- // guarantee literally would mean dropping it (SCHEMA.md, best-effort). It is
235
- // the unsplittable block, so a balanced floor never cuts it: only the page's
236
- // own floor can shrink it.
333
+ // The unsplittable blocks -- a picture and a split -- move whole to a fresh
334
+ // column rather than breaking, so a balanced floor never cuts one: only the
335
+ // page's own floor bears on it. A picture that no column can hold is scaled
336
+ // down to the one it lands on, since obeying the guarantee literally would
337
+ // mean dropping it (SCHEMA.md, best-effort); a split has nothing to scale, so
338
+ // one taller than any column renders in full past the bottom margin, the
339
+ // posture a paragraph line taller than a page already has.
340
+ //
341
+ // A split also *cannot* be sliced: its own `lines` are empty -- the content
342
+ // lives in its slots -- so the slicing path would draw nothing and leave the
343
+ // cursor where it stood, taking everything after it down with it.
237
344
  /** @type {(state: Flow, block: Block) => void} */
238
- let placePicture = (state, block) => {
345
+ let placeWhole = (state, block) => {
239
346
  advance(state);
240
347
  let canvas = state.canvas;
241
348
  let room = canvas.y - canvas.bottom;
242
- if (block.h > room) shrink(block, room);
349
+ if (block.picture && block.h > room) shrink(block, room);
243
350
  drawBlock(canvas, block, originOf(state), widthOf(state));
244
351
  };
245
352
 
@@ -287,10 +394,16 @@ let item = (state, event, measured = null) => {
287
394
  let block = measured || blockOf(canvas, event, avail);
288
395
  // Whole block fits where it stands: one piece, no slicing.
289
396
  if (whole(canvas, block, floorOf(state))) return drawBlock(canvas, block, originOf(state), avail);
290
- if (block.picture) return placePicture(state, block);
291
- slice(state, block);
397
+ overflow(state, block);
292
398
  };
293
399
 
400
+ // What a block does when it does not fit where it stands. A picture and a
401
+ // split never break -- each moves whole to a fresh column; anything else flows
402
+ // down it, breaking where `fitLines` says.
403
+ /** @type {(state: Flow, block: Block) => void} */
404
+ let overflow = (state, block) =>
405
+ block.picture || block.parts ? placeWhole(state, block) : slice(state, block);
406
+
294
407
  // The height a page turn carries over: the drawn headers of the groups still
295
408
  // open, which every page their instances continue onto replays. Computed rather
296
409
  // than kept — a stored total is a second copy of `open` that can drift from it.
@@ -481,9 +594,16 @@ let UNITS = {
481
594
  },
482
595
  row: restated,
483
596
  "total-row": restated,
484
- image: (entry) => unsplit(entry.h, entry.h),
597
+ image: unbroken,
598
+ split: unbroken,
485
599
  };
486
600
 
601
+ // An unsplittable block owes a strip nothing but its own height.
602
+ /** @type {(entry: any) => Unit} */
603
+ function unbroken(entry) {
604
+ return unsplit(entry.h, entry.h);
605
+ }
606
+
487
607
  // A row opening a strip carries the column headings restated above it.
488
608
  /** @type {(entry: any, table: Restated) => Unit} */
489
609
  function restated(entry, table) {
@@ -664,7 +784,7 @@ let entryFor = (state, event, block, width) => {
664
784
  // decided anything. Events that are not blocks contribute an `estimate`.
665
785
  /** @type {(state: Flow, event: any, width: number) => Block | null} */
666
786
  let measureFor = (state, event, width) =>
667
- event.type === "item" || event.type === "image" ? blockOf(state.canvas, event, width) : null;
787
+ BLOCKS.has(event.type) ? blockOf(state.canvas, event, width) : null;
668
788
 
669
789
  // Give every outline mark still waiting a position: where its content begins.
670
790
  /** @type {(state: Flow) => void} */
@@ -684,7 +804,7 @@ let pending = (state) => state.open.filter((group) => group.held.length);
684
804
 
685
805
  // A held run, as blocks. A region measured its headers while buffering them,
686
806
  // at this same width, so a replayed run reuses those rather than wrapping the
687
- // same text a second time (ADR 0020).
807
+ // same text a second time (ADR 0027).
688
808
  /** @type {(canvas: Canvas, held: any[], avail: number) => Block[]} */
689
809
  let blocksOf = (canvas, held, avail) =>
690
810
  held.map((entry) => entry.block || blockOf(canvas, entry.event, avail));
@@ -805,7 +925,9 @@ let breakFor = (state, event) => {
805
925
 
806
926
  /** @type {(event: any) => any} */
807
927
  let markFor = (event) => ({
808
- title: event.name + ": " + String(event.key),
928
+ // display(), not String(): a Date group key must title its outline bookmark
929
+ // with the same ISO 8601 UTC text its cells render, on every machine.
930
+ title: event.name + ": " + display(event.key),
809
931
  titled: false,
810
932
  depth: event.depth,
811
933
  page: -1,
@@ -848,7 +970,7 @@ let headOf = (canvas, span) => span - instanceGap(canvas);
848
970
  // gives flow spacing no meaning inside a table row, in either target.
849
971
  /** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
850
972
  let cellOf = (canvas, cell, rowStyle) => {
851
- let style = merge(rowStyle, cell.style);
973
+ let style = stack(canvas, rowStyle, cell.style);
852
974
  let list = atoms(canvas, cell.tokens, style);
853
975
  let natural = wrap(canvas, list, Infinity).reduce((widest, line) => Math.max(widest, line.w), 0);
854
976
  return {
@@ -945,7 +1067,7 @@ let sum = (values) => values.reduce((total, value) => total + value, 0);
945
1067
  // same fail-loud posture as the measuring canvas's `newPage`.
946
1068
  /** @type {(cols: any[], natural: number[], avail: number) => number[]} */
947
1069
  let columnWidths = (cols, natural, avail) => {
948
- let fixed = cols.map((col) => typeof col.width === "number" && Number.isFinite(col.width));
1070
+ let fixed = cols.map((column) => Number.isFinite(column.width));
949
1071
  if (!fixed.some(Boolean)) {
950
1072
  // Natural widths always carry the cell padding, so the sum is never zero.
951
1073
  let wanted = sum(natural);
@@ -1211,7 +1333,12 @@ let band = (canvas, items, yTop) => {
1211
1333
  fresh = canvas.fresh;
1212
1334
  canvas.y = yTop;
1213
1335
  canvas.fresh = false;
1214
- for (let event of items) drawBlock(canvas, blockOf(canvas, event, avail), canvas.margin, avail);
1336
+ // Folded straight into the draw, the way the body walk folds straight into
1337
+ // `route`: the folder emits in order and nothing here needs the band whole.
1338
+ let fold = splitFolder((event) =>
1339
+ drawBlock(canvas, blockOf(canvas, event, avail), canvas.margin, avail),
1340
+ );
1341
+ for (let event of items) fold(event);
1215
1342
  let h = yTop - canvas.y;
1216
1343
  canvas.y = saved;
1217
1344
  canvas.fresh = fresh;
@@ -1265,7 +1392,8 @@ let reserve = (geo, fonts, bands) => {
1265
1392
  // want it: reservation to measure against, the draw pass to hang the header
1266
1393
  // from.
1267
1394
  /** @type {(canvas: Canvas) => Frame} */
1268
- let pageBox = (canvas) => frame(canvas.width, canvas.height, canvas.margin, canvas.base);
1395
+ let pageBox = (canvas) =>
1396
+ frame(canvas.width, canvas.height, canvas.margin, canvas.base, canvas.style);
1269
1397
 
1270
1398
  // One finished page's furniture, drawn in the strips `reserve` left for it:
1271
1399
  // the header hanging from the top margin, the footer resting on the bottom
@@ -1335,6 +1463,10 @@ let flow = (canvas) => {
1335
1463
  // content would have taken had it never been held.
1336
1464
  route: () => {},
1337
1465
  };
1466
+ // The body walk's own folder, emitting each finished event onward to `route`
1467
+ // below. Declared here and bound after `route` exists.
1468
+ /** @type {(event: any) => void} */
1469
+ let fold;
1338
1470
  // A table is buffered whole before it is laid out: column widths come from
1339
1471
  // every cell in it, so the last row has to be in hand first.
1340
1472
  /** @type {any[]} */
@@ -1350,6 +1482,14 @@ let flow = (canvas) => {
1350
1482
  // What each event does once it is placed for real. A buffered region replays
1351
1483
  // through this same table, so held content lays out by exactly the path it
1352
1484
  // would have taken had it never been held.
1485
+ // A block with no title of its own: held back under a group header, placed
1486
+ // anywhere else. Both the image and the split flow this way.
1487
+ /** @type {(event: any, block: Block | null) => void} */
1488
+ let heldOrPlaced = (event, block) =>
1489
+ event.role === "group-header"
1490
+ ? holdHeader(state, event, null, block)
1491
+ : placeItem(state, event, block);
1492
+
1353
1493
  /** @type {Record<string, (event: any, block: Block | null) => void>} */
1354
1494
  let placed = {
1355
1495
  // A header is held rather than placed, and holding is bookkeeping the
@@ -1362,10 +1502,11 @@ let flow = (canvas) => {
1362
1502
  // An image flows exactly as an item does, held back in a group header and
1363
1503
  // placed anywhere else -- the block it becomes carries a height like any
1364
1504
  // other. It wears no role default: those describe text.
1365
- image: (event, block) =>
1366
- event.role === "group-header"
1367
- ? holdHeader(state, event, null, block)
1368
- : placeItem(state, event, block),
1505
+ image: heldOrPlaced,
1506
+ // A split flows exactly as an image does, for the same reason: one block,
1507
+ // held back in a group header and placed anywhere else. It wears no role
1508
+ // default of its own -- its slots carry theirs.
1509
+ split: heldOrPlaced,
1369
1510
  "group-start": (event) => openGroup(state, event),
1370
1511
  "group-end": () => {
1371
1512
  flush(state, 0);
@@ -1405,6 +1546,7 @@ let flow = (canvas) => {
1405
1546
  };
1406
1547
 
1407
1548
  state.route = route;
1549
+ fold = splitFolder(route);
1408
1550
 
1409
1551
  // Everything drawn across the page rather than down a strip: the declaring
1410
1552
  // node's own bands, its footer among them.
@@ -1458,10 +1600,25 @@ let flow = (canvas) => {
1458
1600
  buffer(state, event, block);
1459
1601
  };
1460
1602
 
1603
+ // The report default onto the canvas, before anything is measured. Its
1604
+ // `size` replaces this target's baseline outright, so row heights and band
1605
+ // gaps scale with the document's type rather than staying at a size nothing
1606
+ // is set in; the block itself becomes the outermost layer `stack` composes
1607
+ // (docs/adr/0033). `sizeOf` is what decides a size is usable anywhere in this
1608
+ // target, so it decides it here too -- a computed `size` reaches this
1609
+ // unchecked, and two spellings of that leniency would drift.
1610
+ /** @type {(style: any) => void} */
1611
+ let adoptDefault = (style) => {
1612
+ if (!style) return;
1613
+ canvas.style = style;
1614
+ canvas.base = sizeOf(style, canvas.base);
1615
+ };
1616
+
1461
1617
  return {
1462
1618
  handlers: {
1463
1619
  "report-start": (event) => {
1464
1620
  opening = event;
1621
+ adoptDefault(event.style);
1465
1622
  if (event.columns) state.pending = { count: event.columns, owner: -1 };
1466
1623
  if (!event.page) return;
1467
1624
  // A frame of its own, never the canvas itself: `measuring` builds
@@ -1473,14 +1630,21 @@ let flow = (canvas) => {
1473
1630
  adopt(canvas, reserve(pageBox(canvas), canvas.fonts, event.page));
1474
1631
  },
1475
1632
  // Everything else routes: `placed` above says what each event does, and
1476
- // one entry point is what lets a buffered region replay through it.
1477
- // Only the roled item needs a word of its own on the way in.
1633
+ // one entry point is what lets a buffered region replay through it. The
1634
+ // three the folder owns are left out `split` is not a stream event at
1635
+ // all (the folder mints it) and item and image reach `route` through it.
1478
1636
  ...Object.fromEntries(
1479
- Object.keys(placed).map((type) => [
1480
- type,
1481
- type === "item" ? (event) => route(roled(event)) : (event) => route(event),
1482
- ]),
1637
+ Object.keys(placed)
1638
+ .filter((type) => !BLOCKS.has(type))
1639
+ .map((type) => [type, (/** @type {any} */ event) => route(event)]),
1483
1640
  ),
1641
+ // Item, image and both bracket events all go through the folder, which
1642
+ // routes what is not inside a split straight onward. The item is the one
1643
+ // that wears a band-role default on the way in.
1644
+ "split-start": (event) => fold(event),
1645
+ "split-end": (event) => fold(event),
1646
+ item: (event) => fold(roled(event)),
1647
+ image: (event) => fold(event),
1484
1648
  "group-end": (event) => {
1485
1649
  endOwned(event.depth);
1486
1650
  route(event);
package/lib/outline.js CHANGED
@@ -49,26 +49,6 @@ let linkKids = (entry, refs, kids, n) => {
49
49
  entry.Count = n;
50
50
  };
51
51
 
52
- /** @type {(ctx: any, refs: any[], root: any, parent: number[], marks: any[], pageRefs: any[], i: number, mark: any) => void} */
53
- let writeEntry = (ctx, refs, root, parent, marks, pageRefs, i, mark) => {
54
- let siblings = kidsOf(parent, parent[i]);
55
- let index = siblings.indexOf(i);
56
- let kids = kidsOf(parent, i);
57
- let target = pageRefs[Math.min(mark.page, pageRefs.length - 1)];
58
- /** @type {Record<string, any>} */
59
- let entry = {
60
- Title: PDFHexString.fromText(mark.title),
61
- Parent: parentRef(parent, i, root, refs),
62
- // `/XYZ <x> <y> null`: scroll to the mark's line, keep the zoom. The
63
- // left edge is the mark's own column, so two instances sharing a y in
64
- // two page columns are two distinct destinations.
65
- Dest: [target, PDFName.of("XYZ"), PDFNumber.of(mark.x), PDFNumber.of(mark.y), null],
66
- };
67
- linkSiblings(entry, refs, siblings, index);
68
- linkKids(entry, refs, kids, descendants(marks, i));
69
- ctx.assign(refs[i], ctx.obj(entry));
70
- };
71
-
72
52
  /**
73
53
  * Attach an outline built from `marks` to the document.
74
54
  *
@@ -82,8 +62,24 @@ export function outline(doc, marks, pageRefs) {
82
62
  let parent = parents(marks);
83
63
  let refs = marks.map(() => ctx.nextRef());
84
64
  let root = ctx.nextRef();
85
- for (let [i, mark] of marks.entries())
86
- writeEntry(ctx, refs, root, parent, marks, pageRefs, i, mark);
65
+ for (let [i, mark] of marks.entries()) {
66
+ let siblings = kidsOf(parent, parent[i]);
67
+ let index = siblings.indexOf(i);
68
+ let kids = kidsOf(parent, i);
69
+ let target = pageRefs[Math.min(mark.page, pageRefs.length - 1)];
70
+ /** @type {Record<string, any>} */
71
+ let entry = {
72
+ Title: PDFHexString.fromText(mark.title),
73
+ Parent: parentRef(parent, i, root, refs),
74
+ // `/XYZ <x> <y> null`: scroll to the mark's line, keep the zoom. The
75
+ // left edge is the mark's own column, so two instances sharing a y in
76
+ // two page columns are two distinct destinations.
77
+ Dest: [target, PDFName.of("XYZ"), PDFNumber.of(mark.x), PDFNumber.of(mark.y), null],
78
+ };
79
+ linkSiblings(entry, refs, siblings, index);
80
+ linkKids(entry, refs, kids, descendants(marks, i));
81
+ ctx.assign(refs[i], ctx.obj(entry));
82
+ }
87
83
 
88
84
  let top = kidsOf(parent, -1);
89
85
  ctx.assign(
package/lib/style.js CHANGED
@@ -4,13 +4,12 @@
4
4
  * render: a value of the wrong shape contributes nothing rather than reaching
5
5
  * the page.
6
6
  *
7
- * `finite`/`HEX` are restated per target on purpose: the coercions are each
8
- * target's own edge, never shared engine code.
7
+ * `HEX` is restated per target on purpose: the coercion is each target's own
8
+ * edge, never shared engine code. Finiteness is not one of those coercions —
9
+ * `Number.isFinite` never coerces, so this target asks it directly.
9
10
  */
10
11
  import { rgb } from "pdf-lib";
11
12
 
12
- /** @type {(value: any) => boolean} */
13
- let finite = (value) => typeof value === "number" && Number.isFinite(value);
14
13
  let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
15
14
 
16
15
  // Leading as a fraction of the font size, cell padding, the gap between a
@@ -51,12 +50,24 @@ let col = (value) => {
51
50
  };
52
51
 
53
52
  /** @type {(style: any, base: number) => number} */
54
- let sizeOf = (style, base) => (finite(style.size) && style.size > 0 ? style.size : base);
53
+ let sizeOf = (style, base) => (Number.isFinite(style.size) && style.size > 0 ? style.size : base);
55
54
 
56
55
  // Style blocks layer outward-in: row under cell.
57
56
  /** @type {(under: any, over: any) => any} */
58
57
  let merge = (under, over) => (under ? (over ? { ...under, ...over } : under) : over || {});
59
58
 
59
+ // The whole stack a resolved style is composed of, outermost first: the report
60
+ // default the canvas carries, the enclosing block's style (a table row's, a
61
+ // split's), and the node's own. This target's reading of docs/adr/0033, in one
62
+ // place, so a third resolution funnel cannot quietly omit the outer layer.
63
+ //
64
+ // Only `family` is load-bearing in the outer layer: `settle` has already put
65
+ // the report default's `size` on `canvas.base`, which `sizeOf` falls back to
66
+ // for every drawn atom, so the size reaches the page whether or not it is
67
+ // merged here.
68
+ /** @type {(canvas: { style?: any }, under: any, own: any) => any} */
69
+ let stack = (canvas, under, own) => merge(canvas.style, merge(under, own));
70
+
60
71
  // Band-role omakase defaults — the outermost layer of that same stack, under
61
72
  // the author's own style, which therefore always wins. Only the headline
62
73
  // roles carry one; the XLSX target carries the same two, byte-identical, in
@@ -81,4 +92,15 @@ let roled = (event) =>
81
92
  ? { ...event, style: merge(ROLES[event.role], event.style) }
82
93
  : event;
83
94
 
84
- export { BAND, BLACK, GUTTER, LEAD, PADX, PADY, col, merge, roled, shift, sizeOf };
95
+ // Whether a resolved style asks for either text decoration -- one reading for
96
+ // the wrapper that stamps it on a line and the canvas that draws it.
97
+ /** @type {(style: any) => boolean} */
98
+ let dressed = (style) => !!(style && (style.underline || style.strikethrough));
99
+
100
+ // Whether a resolved style asks for capitals. This target has no
101
+ // text-transform to defer to, so the reading is here beside the rest of the
102
+ // vocabulary and `text.js` applies it before measuring.
103
+ /** @type {(style: any) => boolean} */
104
+ let upper = (style) => !!(style && style.uppercase);
105
+
106
+ export { BAND, BLACK, GUTTER, LEAD, PADX, PADY, col, dressed, roled, shift, sizeOf, stack, upper };
package/lib/text.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * their resolved typography, and a greedy breaker turns those into lines. Pure
4
4
  * measurement against the font registry — nothing here touches the page.
5
5
  */
6
+ import { display } from "quario";
6
7
  import { ascOf, face, printable, width } from "./fonts.js";
7
- import { LEAD, col, sizeOf } from "./style.js";
8
+ import { LEAD, col, dressed, sizeOf, upper } from "./style.js";
8
9
 
9
10
  // What measuring needs and no more: the embedded faces to measure against
10
11
  // and the base size a style falls back to. A `Canvas` satisfies it, so
@@ -27,7 +28,15 @@ import { LEAD, col, sizeOf } from "./style.js";
27
28
  */
28
29
 
29
30
  /** @type {(token: any) => string} */
30
- let rawOf = (token) => ("literal" in token ? token.literal : String(token.value ?? ""));
31
+ let rawOf = (token) => ("literal" in token ? token.literal : display(token.value));
32
+
33
+ // This target has no text-transform to defer to, so `uppercase` is applied to
34
+ // the string before it is measured -- the widths have to be the widths of what
35
+ // is actually drawn. `toUpperCase` rather than `toLocaleUpperCase`: this
36
+ // target's output is byte-reproducible, so the host's locale must not reach
37
+ // the glyphs.
38
+ /** @type {(text: string, style: any) => string} */
39
+ let cased = (text, style) => (upper(style) ? text.toUpperCase() : text);
31
40
 
32
41
  /** @type {(font: any, line: string) => string[]} */
33
42
  let partsOf = (font, line) => printable(font, line).split(/( +)/).filter(Boolean);
@@ -61,7 +70,7 @@ let atoms = (metrics, tokens, style) => {
61
70
  let out = /** @type {Atom[]} */ ([]);
62
71
  let { font, size, color } = look(metrics, style);
63
72
  for (let token of tokens)
64
- for (let [i, line] of rawOf(token).split("\n").entries())
73
+ for (let [i, line] of cased(rawOf(token), style).split("\n").entries())
65
74
  pushLine(out, font, size, color, i, line);
66
75
  return out;
67
76
  };
@@ -225,9 +234,6 @@ let stamp = (line, underline, strikethrough) => {
225
234
  if (strikethrough) line.strikethrough = true;
226
235
  };
227
236
 
228
- /** @type {(style: any) => boolean} */
229
- let dressed = (style) => !!(style && (style.underline || style.strikethrough));
230
-
231
237
  /** @type {(lines: Line[], style: any) => Line[]} */
232
238
  let dress = (lines, style) => {
233
239
  if (!dressed(style)) return lines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/pdf",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The browserless, paginated PDF render target for quario — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -42,13 +42,13 @@
42
42
  "@arethetypeswrong/cli": "^0.18.3",
43
43
  "@pdf-lib/fontkit": "^1.1.1",
44
44
  "@size-limit/preset-small-lib": "^13.0.3",
45
- "quario": "^0.1.0",
45
+ "quario": "^0.2.0",
46
46
  "size-limit": "^13.0.3",
47
47
  "typescript": "^7.0.2"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@pdf-lib/fontkit": "^1.1.1",
51
- "quario": "^0.1.0"
51
+ "quario": "^0.2.0"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@pdf-lib/fontkit": {