snowkit 0.0.0-stage → 0.1.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.
@@ -0,0 +1,1188 @@
1
+ export const PAGE_SIZES = { a4: [595.28, 841.89], letter: [612, 792], legal: [612, 1008], a5: [419.53, 595.28] };
2
+
3
+ const INK = [0.11, 0.11, 0.11];
4
+ const LINK = [0.05, 0.33, 0.73];
5
+ const MUTED = [0.42, 0.42, 0.42];
6
+ const RULE = [0.8, 0.8, 0.8];
7
+ const CODE_BG = [0.955, 0.953, 0.948];
8
+ const HEAD_BG = [0.945, 0.945, 0.945];
9
+ const BAR = [0.83, 0.83, 0.83];
10
+ const LIST_INDENT = 20;
11
+ const QUOTE_INDENT = 16;
12
+ const CELL_PAD = 5;
13
+ const HEAD = { h1: [2.0, 1.2, 0.55], h2: [1.55, 1.15, 0.5], h3: [1.27, 1.0, 0.4], h4: [1.1, 0.9, 0.35], h5: [1.0, 0.8, 0.3], h6: [0.95, 0.8, 0.3] };
14
+
15
+ const SKIP = new Set(['head', 'script', 'style', 'title', 'meta', 'link', 'noscript', 'template', 'iframe', 'object', 'embed', 'svg', 'canvas', 'video', 'audio', 'map', 'select', 'button', 'textarea', 'datalist', 'option', 'base', 'param', 'source', 'track', 'dialog']);
16
+ const CONTAINER = new Set(['html', 'body', 'div', 'section', 'article', 'header', 'footer', 'main', 'nav', 'aside', 'address', 'form', 'figure', 'center', 'details', 'summary', 'fieldset', 'hgroup', 'legend', 'search', 'p', 'caption', 'figcaption', '#root']);
17
+ const BLOCK = new Set([...CONTAINER, 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'blockquote', 'pre', 'table', 'hr', 'menu']);
18
+ const NAMED = {
19
+ black: [0, 0, 0], white: [1, 1, 1], red: [0.8, 0, 0], green: [0, 0.5, 0], blue: [0, 0, 0.8], gray: [0.5, 0.5, 0.5], grey: [0.5, 0.5, 0.5],
20
+ orange: [1, 0.65, 0], purple: [0.5, 0, 0.5], navy: [0, 0, 0.5], teal: [0, 0.5, 0.5], maroon: [0.5, 0, 0], olive: [0.5, 0.5, 0], silver: [0.75, 0.75, 0.75],
21
+ yellow: [1, 1, 0], brown: [0.65, 0.16, 0.16], pink: [1, 0.75, 0.8], darkgray: [0.66, 0.66, 0.66], darkgrey: [0.66, 0.66, 0.66], lightgray: [0.83, 0.83, 0.83],
22
+ lightgrey: [0.83, 0.83, 0.83], darkred: [0.55, 0, 0], darkgreen: [0, 0.39, 0], darkblue: [0, 0, 0.55], crimson: [0.86, 0.08, 0.24], gold: [1, 0.84, 0],
23
+ indigo: [0.29, 0, 0.51], coral: [1, 0.5, 0.31], tomato: [1, 0.39, 0.28], steelblue: [0.27, 0.51, 0.71], slategray: [0.44, 0.5, 0.56], dimgray: [0.41, 0.41, 0.41],
24
+ };
25
+
26
+ const tagOf = (n) => (n && n.nodeType === 1 ? String(n.tagName || n.nodeName || '').toLowerCase() : '');
27
+ const kids = (n) => Array.from((n && n.childNodes) || []);
28
+
29
+ function attr(n, k) {
30
+ if (!n || n.nodeType !== 1) return null;
31
+ if (typeof n.getAttribute === 'function') return n.getAttribute(k);
32
+ return n.attrs && Object.prototype.hasOwnProperty.call(n.attrs, k) ? n.attrs[k] : null;
33
+ }
34
+
35
+ export function parseColor(raw) {
36
+ const s = String(raw || '').trim().toLowerCase();
37
+ if (!s || s === 'transparent' || s === 'inherit' || s === 'currentcolor' || s === 'initial') return null;
38
+ if (NAMED[s]) return NAMED[s].slice();
39
+ let m = /^#([0-9a-f]{3,8})$/.exec(s);
40
+ if (m) {
41
+ let h = m[1];
42
+ if (h.length === 3 || h.length === 4) h = h.slice(0, 3).split('').map((c) => c + c).join('');
43
+ else if (h.length === 8) h = h.slice(0, 6);
44
+ if (h.length !== 6) return null;
45
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
46
+ }
47
+ m = /^rgba?\(\s*([\d.]+%?)[\s,]+([\d.]+%?)[\s,]+([\d.]+%?)(?:[\s,/]+([\d.]+%?))?\s*\)$/.exec(s);
48
+ if (m) {
49
+ if (m[4] !== undefined && parseFloat(m[4]) === 0) return null;
50
+ return [m[1], m[2], m[3]].map((v) => Math.max(0, Math.min(1, v.endsWith('%') ? parseFloat(v) / 100 : parseFloat(v) / 255)));
51
+ }
52
+ return null;
53
+ }
54
+
55
+ export function parseStyle(text) {
56
+ const out = {};
57
+ for (const part of String(text || '').split(';')) {
58
+ const i = part.indexOf(':');
59
+ if (i < 0) continue;
60
+ const k = part.slice(0, i).trim().toLowerCase();
61
+ const v = part.slice(i + 1).replace(/!important/i, '').trim();
62
+ if (k) out[k] = v;
63
+ }
64
+ return out;
65
+ }
66
+
67
+ function sizeScale(v, base) {
68
+ const s = String(v || '').trim().toLowerCase();
69
+ const named = { 'xx-small': 0.6, 'x-small': 0.7, small: 0.85, medium: 1, large: 1.2, 'x-large': 1.5, 'xx-large': 2, smaller: 0.85, larger: 1.2 };
70
+ if (named[s]) return named[s];
71
+ const m = /^([\d.]+)(px|pt|em|rem|%)?$/.exec(s);
72
+ if (!m) return null;
73
+ const n = parseFloat(m[1]);
74
+ if (!(n > 0)) return null;
75
+ const unit = m[2] || 'px';
76
+ const r = unit === 'px' ? (n * 0.75) / base : unit === 'pt' ? n / base : unit === '%' ? n / 100 : n;
77
+ return Math.max(0.5, Math.min(3.5, r));
78
+ }
79
+
80
+ function hidden(n, css) {
81
+ if (attr(n, 'hidden') !== null) return true;
82
+ const d = (css.display || '').toLowerCase();
83
+ return d === 'none' || (css.visibility || '').toLowerCase() === 'hidden';
84
+ }
85
+
86
+ function formatFrom(n, fmt, css, base) {
87
+ const tag = tagOf(n);
88
+ const f = { ...fmt };
89
+ if (tag === 'b' || tag === 'strong') f.b = true;
90
+ if (tag === 'i' || tag === 'em' || tag === 'cite' || tag === 'dfn' || tag === 'var' || tag === 'address') f.i = true;
91
+ if (tag === 'u' || tag === 'ins') f.u = true;
92
+ if (tag === 's' || tag === 'del' || tag === 'strike') f.s = true;
93
+ if (tag === 'code' || tag === 'kbd' || tag === 'samp' || tag === 'tt') f.code = true;
94
+ if (tag === 'mark') f.bg = [1, 0.92, 0.5];
95
+ if (tag === 'sup') f.sup = true;
96
+ if (tag === 'sub') f.sub = true;
97
+ if (tag === 'small') f.scale = (f.scale || 1) * 0.85;
98
+ if (tag === 'big') f.scale = (f.scale || 1) * 1.15;
99
+ if (tag === 'font' && attr(n, 'color')) f.color = parseColor(attr(n, 'color')) || f.color;
100
+ if (tag === 'a') {
101
+ const href = attr(n, 'href') || '';
102
+ if (/^(https?:|mailto:|tel:)/i.test(href.trim())) {
103
+ f.link = href.trim();
104
+ if (!css.color) f.color = LINK;
105
+ }
106
+ }
107
+ const fw = (css['font-weight'] || '').toLowerCase();
108
+ if (fw === 'bold' || fw === 'bolder' || +fw >= 600) f.b = true;
109
+ else if (fw === 'normal' || fw === 'lighter' || (+fw > 0 && +fw < 600)) f.b = false;
110
+ const fs = (css['font-style'] || '').toLowerCase();
111
+ if (fs === 'italic' || fs === 'oblique') f.i = true;
112
+ else if (fs === 'normal') f.i = false;
113
+ const td = (css['text-decoration'] || css['text-decoration-line'] || '').toLowerCase();
114
+ if (td.includes('underline')) f.u = true;
115
+ if (td.includes('line-through')) f.s = true;
116
+ if (td === 'none') f.u = f.s = false;
117
+ if (css.color) f.color = parseColor(css.color) || f.color;
118
+ const bg = css['background-color'] || (css.background && parseColor(css.background.split(/\s+/)[0]) ? css.background.split(/\s+/)[0] : '');
119
+ if (bg && parseColor(bg)) {
120
+ const c = parseColor(bg);
121
+ if (!(c[0] > 0.97 && c[1] > 0.97 && c[2] > 0.97)) f.bg = c;
122
+ }
123
+ if (/mono|courier|consolas|menlo/i.test(css['font-family'] || '')) f.mono = true;
124
+ const va = (css['vertical-align'] || '').toLowerCase();
125
+ if (va === 'super') f.sup = true;
126
+ if (va === 'sub') f.sub = true;
127
+ if (css['font-size']) {
128
+ const k = sizeScale(css['font-size'], base);
129
+ if (k) f.scale = k;
130
+ }
131
+ const tt = (css['text-transform'] || '').toLowerCase();
132
+ if (tt === 'uppercase' || tt === 'lowercase' || tt === 'capitalize') f.transform = tt;
133
+ return f;
134
+ }
135
+
136
+ function cleanText(s) {
137
+ return String(s).replace(/[\u0000-\u0008\u000b\u000e-\u001f\u007f\u00ad\u200b\u2060\ufeff]/g, '');
138
+ }
139
+
140
+ function transformText(t, mode) {
141
+ if (mode === 'uppercase') return t.toUpperCase();
142
+ if (mode === 'lowercase') return t.toLowerCase();
143
+ if (mode === 'capitalize') return t.replace(/(^|\s)(\S)/g, (m, a, b) => a + b.toUpperCase());
144
+ return t;
145
+ }
146
+
147
+ function textRun(raw, fmt) {
148
+ const t = cleanText(raw).replace(/[ \t\n\r\f]+/g, ' ');
149
+ if (!t) return [];
150
+ return [{ ...fmt, text: transformText(t, fmt.transform) }];
151
+ }
152
+
153
+ function inlineRuns(node, fmt, base, out = []) {
154
+ for (const n of kids(node)) {
155
+ if (n.nodeType === 3) {
156
+ out.push(...textRun(n.nodeValue || n.data || '', fmt));
157
+ continue;
158
+ }
159
+ if (n.nodeType !== 1) continue;
160
+ const tag = tagOf(n);
161
+ if (SKIP.has(tag) || tag === 'input') continue;
162
+ const css = parseStyle(attr(n, 'style'));
163
+ if (hidden(n, css)) continue;
164
+ if (tag === 'br') {
165
+ out.push({ br: true });
166
+ continue;
167
+ }
168
+ if (tag === 'wbr') continue;
169
+ if (tag === 'img') {
170
+ out.push({ img: attr(n, 'src') || '', alt: attr(n, 'alt') || '', width: px(attr(n, 'width') || css.width), height: px(attr(n, 'height') || css.height) });
171
+ continue;
172
+ }
173
+ const f = formatFrom(n, fmt, css, base);
174
+ if (tag === 'q') out.push(...textRun('\u201c', f));
175
+ inlineRuns(n, f, base, out);
176
+ if (tag === 'q') out.push(...textRun('\u201d', f));
177
+ }
178
+ return out;
179
+ }
180
+
181
+ function px(v) {
182
+ const m = /^\s*([\d.]+)\s*(px)?\s*$/i.exec(String(v || ''));
183
+ return m ? parseFloat(m[1]) : null;
184
+ }
185
+
186
+ function alignOf(n, css, fallback) {
187
+ const a = (css['text-align'] || attr(n, 'align') || '').toLowerCase();
188
+ if (a === 'center' || a === 'right' || a === 'justify' || a === 'left') return a;
189
+ if (a === 'start') return 'left';
190
+ if (a === 'end') return 'right';
191
+ if (tagOf(n) === 'center') return 'center';
192
+ return fallback;
193
+ }
194
+
195
+ function breakBefore(css) {
196
+ return /always|page|left|right/.test(String(css['page-break-before'] || css['break-before'] || '').toLowerCase());
197
+ }
198
+
199
+ function breakAfter(css) {
200
+ return /always|page|left|right/.test(String(css['page-break-after'] || css['break-after'] || '').toLowerCase());
201
+ }
202
+
203
+ function deco(ctx) {
204
+ return { indent: ctx.indent, bars: ctx.bars, align: ctx.align };
205
+ }
206
+
207
+ function paras(runs, ctx, out) {
208
+ let cur = [];
209
+ const flush = (force) => {
210
+ if (!cur.length && !force) return;
211
+ while (cur.length && cur[cur.length - 1].br) cur.pop();
212
+ while (cur.length && cur[0].text !== undefined && !cur[0].text.trim()) cur.shift();
213
+ while (cur.length && cur[cur.length - 1].text !== undefined && !cur[cur.length - 1].text.trim()) cur.pop();
214
+ if (cur.length && cur[0].text) cur[0] = { ...cur[0], text: cur[0].text.replace(/^ +/, '') };
215
+ if (cur.length && cur[cur.length - 1].text) cur[cur.length - 1] = { ...cur[cur.length - 1], text: cur[cur.length - 1].text.replace(/ +$/, '') };
216
+ const b = { type: 'para', runs: cur, style: ctx.style || 'p', ...deco(ctx) };
217
+ if (ctx.mark && !ctx.mark.used) {
218
+ b.mark = ctx.mark;
219
+ ctx.mark.used = true;
220
+ }
221
+ if (b.runs.some((r) => r.text && r.text.trim()) || b.mark) out.push(b);
222
+ cur = [];
223
+ };
224
+ for (const r of runs) {
225
+ if (r.img !== undefined) {
226
+ flush();
227
+ if (ctx.mark && !ctx.mark.used) flush(true);
228
+ out.push({ type: 'image', src: r.img, alt: r.alt, width: r.width, height: r.height, ...deco(ctx) });
229
+ } else cur.push(r);
230
+ }
231
+ flush();
232
+ return out;
233
+ }
234
+
235
+ function preText(node) {
236
+ let s = '';
237
+ for (const n of kids(node)) {
238
+ if (n.nodeType === 3) s += n.nodeValue || n.data || '';
239
+ else if (n.nodeType === 1) {
240
+ const tag = tagOf(n);
241
+ if (tag === 'br') s += '\n';
242
+ else if (!SKIP.has(tag)) s += preText(n);
243
+ }
244
+ }
245
+ return s;
246
+ }
247
+
248
+ function firstCheckbox(li) {
249
+ for (const n of kids(li)) {
250
+ if (n.nodeType === 3 && (n.nodeValue || '').trim()) return null;
251
+ if (n.nodeType !== 1) continue;
252
+ const tag = tagOf(n);
253
+ if (tag === 'input' && String(attr(n, 'type') || '').toLowerCase() === 'checkbox') return attr(n, 'checked') !== null;
254
+ if (tag === 'p' || tag === 'label' || tag === 'span') return firstCheckbox(n);
255
+ return null;
256
+ }
257
+ return null;
258
+ }
259
+
260
+ function listBlocks(el, ctx, out) {
261
+ const ordered = tagOf(el) === 'ol';
262
+ let n = ordered ? parseInt(attr(el, 'start') || '1', 10) : 1;
263
+ if (!Number.isFinite(n)) n = 1;
264
+ const type = attr(el, 'type') || parseStyle(attr(el, 'style'))['list-style-type'] || '';
265
+ const level = ctx.level || 0;
266
+ for (const li of kids(el)) {
267
+ if (li.nodeType !== 1) continue;
268
+ const tag = tagOf(li);
269
+ if (tag === 'ul' || tag === 'ol') {
270
+ listBlocks(li, { ...ctx, level: level + 1, indent: ctx.indent + LIST_INDENT }, out);
271
+ continue;
272
+ }
273
+ if (tag !== 'li') continue;
274
+ const css = parseStyle(attr(li, 'style'));
275
+ if (hidden(li, css)) continue;
276
+ const v = parseInt(attr(li, 'value') || '', 10);
277
+ if (Number.isFinite(v)) n = v;
278
+ const mark = { ordered, n, level, task: firstCheckbox(li), kind: listKind(type, ordered), used: false };
279
+ const inner = { ...ctx, indent: ctx.indent + LIST_INDENT, level: level + 1, mark, style: undefined, fmt: formatFrom(li, ctx.fmt, css, ctx.base) };
280
+ const before = out.length;
281
+ blocksOf(li, inner, out);
282
+ if (!mark.used) out.splice(before, 0, { type: 'para', runs: [], style: 'p', mark, ...deco(inner) });
283
+ mark.used = true;
284
+ n++;
285
+ }
286
+ }
287
+
288
+ function listKind(type, ordered) {
289
+ const raw = String(type).trim();
290
+ const t = raw.toLowerCase();
291
+ if (!ordered) return t === 'none' ? 'none' : t === 'square' ? 'square' : t === 'circle' ? 'circle' : '';
292
+ if (raw === 'A' || t === 'upper-alpha' || t === 'upper-latin') return 'A';
293
+ if (raw === 'I' || t === 'upper-roman') return 'I';
294
+ if (t === 'a' || t === 'lower-alpha' || t === 'lower-latin') return 'a';
295
+ if (t === 'i' || t === 'lower-roman') return 'i';
296
+ return t === 'none' ? 'none' : '1';
297
+ }
298
+
299
+ function roman(n) {
300
+ const map = [[1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], [100, 'c'], [90, 'xc'], [50, 'l'], [40, 'xl'], [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i']];
301
+ let s = '';
302
+ let k = Math.max(1, Math.min(3999, n));
303
+ for (const [v, r] of map) while (k >= v) {
304
+ s += r;
305
+ k -= v;
306
+ }
307
+ return s;
308
+ }
309
+
310
+ export function markerLabel(mark) {
311
+ if (!mark.ordered) return '';
312
+ const n = mark.n;
313
+ if (mark.kind === 'a' || mark.kind === 'A') {
314
+ let k = Math.max(1, n);
315
+ let s = '';
316
+ while (k > 0) {
317
+ k--;
318
+ s = String.fromCharCode(97 + (k % 26)) + s;
319
+ k = Math.floor(k / 26);
320
+ }
321
+ return (mark.kind === 'A' ? s.toUpperCase() : s) + '.';
322
+ }
323
+ if (mark.kind === 'i') return roman(n) + '.';
324
+ if (mark.kind === 'I') return roman(n).toUpperCase() + '.';
325
+ if (mark.kind === 'none') return '';
326
+ return n + '.';
327
+ }
328
+
329
+ function tableBlock(el, ctx) {
330
+ const rows = [];
331
+ const visit = (node, head) => {
332
+ for (const n of kids(node)) {
333
+ const tag = tagOf(n);
334
+ if (tag === 'thead') visit(n, true);
335
+ else if (tag === 'tbody' || tag === 'tfoot') visit(n, false);
336
+ else if (tag === 'tr') {
337
+ const cells = [];
338
+ for (const c of kids(n)) {
339
+ const ct = tagOf(c);
340
+ if (ct !== 'td' && ct !== 'th') continue;
341
+ const css = parseStyle(attr(c, 'style'));
342
+ const th = ct === 'th';
343
+ const fmt = formatFrom(c, { ...ctx.fmt, b: th || ctx.fmt.b }, css, ctx.base);
344
+ const bg = css['background-color'] ? parseColor(css['background-color']) : css.background ? parseColor(css.background.split(/\s+/)[0]) : null;
345
+ delete fmt.bg;
346
+ const inner = { ...ctx, fmt, indent: 0, bars: [], align: alignOf(c, css, th && head ? 'left' : 'left'), mark: null, style: undefined, level: 0 };
347
+ const blocks = [];
348
+ blocksOf(c, inner, blocks);
349
+ cells.push({ blocks: blocks.filter((b) => b.type !== 'break'), colspan: Math.max(1, Math.min(50, parseInt(attr(c, 'colspan') || '1', 10) || 1)), rowspan: Math.max(1, Math.min(500, parseInt(attr(c, 'rowspan') || '1', 10) || 1)), head: head || (th && !rows.length), bg });
350
+ }
351
+ if (cells.length) rows.push({ cells, head });
352
+ } else if (tag === 'caption') continue;
353
+ else if (tag && !SKIP.has(tag)) visit(n, head);
354
+ }
355
+ };
356
+ visit(el, false);
357
+ let caption = null;
358
+ for (const n of kids(el)) if (tagOf(n) === 'caption') caption = n;
359
+ return { rows, caption };
360
+ }
361
+
362
+ function blocksOf(node, ctx, out) {
363
+ let loose = [];
364
+ const flush = () => {
365
+ if (loose.some((r) => r.img !== undefined || (r.text && r.text.trim()))) paras(loose, ctx, out);
366
+ loose = [];
367
+ };
368
+ for (const n of kids(node)) {
369
+ if (n.nodeType === 3) {
370
+ loose.push(...textRun(n.nodeValue || n.data || '', ctx.fmt));
371
+ continue;
372
+ }
373
+ if (n.nodeType !== 1) continue;
374
+ const tag = tagOf(n);
375
+ if (SKIP.has(tag)) continue;
376
+ const css = parseStyle(attr(n, 'style'));
377
+ if (hidden(n, css)) continue;
378
+ const display = (css.display || '').toLowerCase();
379
+ const isBlock = BLOCK.has(tag) || display === 'block' || display === 'flex' || display === 'grid' || display === 'list-item';
380
+ if (!isBlock || display === 'inline' || display === 'inline-block') {
381
+ if (tag === 'br') loose.push({ br: true });
382
+ else if (tag === 'img') loose.push(...inlineRuns({ childNodes: [n] }, ctx.fmt, ctx.base));
383
+ else if (tag !== 'input') {
384
+ const f = formatFrom(n, ctx.fmt, css, ctx.base);
385
+ if (tag === 'q') loose.push(...textRun('\u201c', f));
386
+ inlineRuns(n, f, ctx.base, loose);
387
+ if (tag === 'q') loose.push(...textRun('\u201d', f));
388
+ }
389
+ continue;
390
+ }
391
+ flush();
392
+ if (breakBefore(css)) out.push({ type: 'break' });
393
+ const fmt = formatFrom(n, ctx.fmt, css, ctx.base);
394
+ const align = alignOf(n, css, ctx.align);
395
+ if (/^h[1-6]$/.test(tag)) {
396
+ paras(inlineRuns(n, fmt, ctx.base), { ...ctx, fmt, align, style: tag }, out);
397
+ } else if (tag === 'ul' || tag === 'ol' || tag === 'menu') {
398
+ listBlocks(n, { ...ctx, fmt, align }, out);
399
+ } else if (tag === 'li') {
400
+ listBlocks({ nodeType: 1, tagName: 'ul', childNodes: [n], attrs: {} }, { ...ctx, fmt, align }, out);
401
+ } else if (tag === 'blockquote') {
402
+ blocksOf(n, { ...ctx, fmt, align, bars: [...ctx.bars, ctx.indent + 2], indent: ctx.indent + QUOTE_INDENT }, out);
403
+ } else if (tag === 'pre') {
404
+ if (ctx.mark && !ctx.mark.used) paras([], ctx, out);
405
+ const text = preText(n).replace(/\r\n?/g, '\n').replace(/^\n/, '').replace(/\n$/, '');
406
+ out.push({ type: 'code', text: cleanText(text.replace(/\t/g, ' ')), ...deco(ctx) });
407
+ } else if (tag === 'table') {
408
+ if (ctx.mark && !ctx.mark.used) paras([], ctx, out);
409
+ const t = tableBlock(n, { ...ctx, fmt });
410
+ if (t.caption) paras(inlineRuns(t.caption, { ...fmt, i: true }, ctx.base), { ...ctx, fmt, align: 'center' }, out);
411
+ if (t.rows.length) out.push({ type: 'table', rows: t.rows, ...deco(ctx) });
412
+ } else if (tag === 'hr') {
413
+ out.push({ type: 'rule', ...deco(ctx) });
414
+ } else if (tag === 'dl') {
415
+ for (const d of kids(n)) {
416
+ const dt = tagOf(d);
417
+ if (dt === 'dt') paras(inlineRuns(d, { ...fmt, b: true }, ctx.base), { ...ctx, fmt, align }, out);
418
+ else if (dt === 'dd') blocksOf(d, { ...ctx, fmt, align, indent: ctx.indent + LIST_INDENT }, out);
419
+ else if (dt === 'div') blocksOf({ nodeType: 1, tagName: 'dl', childNodes: [{ nodeType: 1, tagName: 'dl', childNodes: kids(d), attrs: {} }], attrs: {} }, ctx, out);
420
+ }
421
+ } else if (tag === 'figcaption' || tag === 'caption') {
422
+ paras(inlineRuns(n, { ...fmt, i: true, color: fmt.color || MUTED, scale: (fmt.scale || 1) * 0.9 }, ctx.base), { ...ctx, fmt, align: css['text-align'] ? align : 'center' }, out);
423
+ } else if (tag === 'dt') {
424
+ paras(inlineRuns(n, { ...fmt, b: true }, ctx.base), { ...ctx, fmt, align }, out);
425
+ } else if (tag === 'dd') {
426
+ blocksOf(n, { ...ctx, fmt, align, indent: ctx.indent + LIST_INDENT }, out);
427
+ } else {
428
+ blocksOf(n, { ...ctx, fmt, align }, out);
429
+ }
430
+ if (breakAfter(css)) out.push({ type: 'break' });
431
+ }
432
+ flush();
433
+ return out;
434
+ }
435
+
436
+ export function htmlBlocks(root, { base = 11 } = {}) {
437
+ const body = findBody(root) || root;
438
+ const out = [];
439
+ blocksOf(body, { fmt: {}, align: 'left', indent: 0, bars: [], level: 0, mark: null, base }, out);
440
+ while (out.length && out[out.length - 1].type === 'break') out.pop();
441
+ while (out.length && out[0].type === 'break') out.shift();
442
+ return out;
443
+ }
444
+
445
+ function findBody(n) {
446
+ if (!n) return null;
447
+ if (tagOf(n) === 'body') return n;
448
+ for (const k of kids(n)) {
449
+ if (k.nodeType !== 1) continue;
450
+ const t = tagOf(k);
451
+ if (t === 'body') return k;
452
+ if (t === 'html') {
453
+ const b = findBody(k);
454
+ if (b) return b;
455
+ }
456
+ }
457
+ return null;
458
+ }
459
+
460
+ function fontKey(r) {
461
+ if (r.code || r.mono) return r.b ? 'monobold' : 'mono';
462
+ return r.b && r.i ? 'bolditalic' : r.b ? 'bold' : r.i ? 'italic' : 'regular';
463
+ }
464
+
465
+ class Layout {
466
+ constructor(opts) {
467
+ const size = Array.isArray(opts.page) ? opts.page : PAGE_SIZES[opts.page] || PAGE_SIZES.a4;
468
+ this.w = size[0];
469
+ this.h = size[1];
470
+ const m = typeof opts.margin === 'number' ? { top: opts.margin, right: opts.margin, bottom: opts.margin, left: opts.margin } : { top: 56, right: 56, bottom: 56, left: 56, ...(opts.margin || {}) };
471
+ this.m = m;
472
+ this.left = m.left;
473
+ this.width = Math.max(72, this.w - m.left - m.right);
474
+ this.top = this.h - m.top;
475
+ this.bottom = Math.min(this.top - 72, m.bottom);
476
+ this.base = opts.base || 11;
477
+ this.lead = opts.lineHeight || 1.45;
478
+ this.measureFn = opts.measure || ((t, f, s) => t.length * s * (f.startsWith('mono') ? 0.6 : 0.5));
479
+ this.canEncode = opts.canEncode || (() => true);
480
+ this.imageFn = opts.image || (() => null);
481
+ this.pages = [];
482
+ this.skipped = 0;
483
+ this.links = 0;
484
+ this.newPage();
485
+ }
486
+
487
+ newPage() {
488
+ this.ops = [];
489
+ this.pages.push({ ops: this.ops });
490
+ this.y = this.top;
491
+ }
492
+
493
+ get atTop() {
494
+ return this.y >= this.top - 0.01;
495
+ }
496
+
497
+ measure(text, font, size) {
498
+ try {
499
+ const w = this.measureFn(text, font, size);
500
+ return Number.isFinite(w) ? w : text.length * size * 0.5;
501
+ } catch (e) {
502
+ return text.length * size * 0.55;
503
+ }
504
+ }
505
+
506
+ pick(r, text) {
507
+ let f = fontKey(r);
508
+ if ((f === 'mono' || f === 'monobold') && !this.canEncode(f, text)) f = r.b ? 'bold' : 'regular';
509
+ if (!this.canEncode(f, text)) f = r.b ? 'bold' : 'regular';
510
+ return f;
511
+ }
512
+
513
+ lines(runs, width, size) {
514
+ const words = [];
515
+ let word = null;
516
+ for (const r of runs) {
517
+ if (r.br) {
518
+ words.push({ br: true });
519
+ word = null;
520
+ continue;
521
+ }
522
+ if (!r.text) continue;
523
+ const s = size * (r.scale || 1) * (r.sup || r.sub ? 0.7 : 1) * (r.code ? 0.9 : 1);
524
+ for (const piece of r.text.split(/( +)/)) {
525
+ if (!piece) continue;
526
+ const font = this.pick(r, piece);
527
+ const part = { text: piece, font, size: s, r, w: this.measure(piece, font, s) };
528
+ if (/^ +$/.test(piece)) {
529
+ const sp = { ...part, text: ' ', w: this.measure(' ', font, s) };
530
+ if (word) word.space = sp;
531
+ else if (words.length && !words[words.length - 1].br) words[words.length - 1].space = sp;
532
+ word = null;
533
+ } else {
534
+ if (!word) {
535
+ word = { parts: [], w: 0, space: null };
536
+ words.push(word);
537
+ }
538
+ word.parts.push(part);
539
+ word.w += part.w;
540
+ }
541
+ }
542
+ }
543
+ const lines = [];
544
+ let line = { parts: [], w: 0, spaces: 0 };
545
+ let pending = null;
546
+ const end = (hard) => {
547
+ line.hard = hard;
548
+ lines.push(line);
549
+ line = { parts: [], w: 0, spaces: 0 };
550
+ pending = null;
551
+ };
552
+ for (const wd of words) {
553
+ if (wd.br) {
554
+ end(true);
555
+ continue;
556
+ }
557
+ const gap = line.parts.length && pending ? pending.w : 0;
558
+ if (line.parts.length && line.w + gap + wd.w > width + 0.01) end(false);
559
+ if (!line.parts.length && wd.w > width) {
560
+ for (const p of wd.parts) {
561
+ let chunk = '';
562
+ for (const ch of p.text) {
563
+ const test = chunk + ch;
564
+ if (line.w + this.measure(test, p.font, p.size) > width && (chunk || line.parts.length)) {
565
+ if (chunk) {
566
+ const w = this.measure(chunk, p.font, p.size);
567
+ line.parts.push({ ...p, text: chunk, w });
568
+ line.w += w;
569
+ }
570
+ end(false);
571
+ chunk = ch;
572
+ } else chunk = test;
573
+ }
574
+ if (chunk) {
575
+ const w = this.measure(chunk, p.font, p.size);
576
+ line.parts.push({ ...p, text: chunk, w });
577
+ line.w += w;
578
+ }
579
+ }
580
+ } else {
581
+ if (line.parts.length && pending) {
582
+ line.parts.push({ ...pending, space: true });
583
+ line.w += pending.w;
584
+ line.spaces++;
585
+ }
586
+ for (const p of wd.parts) line.parts.push(p);
587
+ line.w += wd.w;
588
+ }
589
+ pending = wd.space;
590
+ }
591
+ if (line.parts.length || !lines.length) end(true);
592
+ return lines.map((l) => {
593
+ const max = l.parts.reduce((m, p) => Math.max(m, p.r.sup || p.r.sub ? p.size / 0.7 : p.r.code ? p.size / 0.9 : p.size), 0) || size;
594
+ return { ...l, h: max * this.lead, base: max * 1.08, size: max };
595
+ });
596
+ }
597
+
598
+ drawLine(ops, line, x, yTop, width, align, last) {
599
+ const base = yTop - line.base;
600
+ let extra = 0;
601
+ let cx = x;
602
+ if (align === 'center') cx = x + Math.max(0, (width - line.w) / 2);
603
+ else if (align === 'right') cx = x + Math.max(0, width - line.w);
604
+ else if (align === 'justify' && !last && !line.hard && line.spaces) extra = Math.max(0, (width - line.w) / line.spaces);
605
+ const same = (a, b) => a.font === b.font && a.size === b.size && a.r.link === b.r.link && !!a.r.u === !!b.r.u && !!a.r.s === !!b.r.s && !!a.r.sup === !!b.r.sup && !!a.r.sub === !!b.r.sub && !!a.r.code === !!b.r.code && String(a.r.color) === String(b.r.color) && String(a.r.bg) === String(b.r.bg);
606
+ const groups = [];
607
+ for (const p of line.parts) {
608
+ const g = groups[groups.length - 1];
609
+ if (g && !extra && same(g, p)) {
610
+ g.text += p.text;
611
+ g.w += p.w;
612
+ } else groups.push({ ...p });
613
+ }
614
+ for (const p of groups) {
615
+ if (p.space && extra) {
616
+ cx += p.w + extra;
617
+ continue;
618
+ }
619
+ const y = base + (p.r.sup ? line.size * 0.35 : p.r.sub ? -line.size * 0.12 : 0);
620
+ const col = p.r.color || INK;
621
+ const bg = p.r.code ? p.r.bg || CODE_BG : p.r.bg;
622
+ if (bg && p.text.trim()) ops.push({ op: 'rect', x: cx - (p.r.code ? 1.5 : 0.5), y: y - p.size * 0.28, w: p.w + (p.r.code ? 3 : 1), h: p.size * 1.18, fill: bg });
623
+ if (p.text.trim()) ops.push({ op: 'text', x: cx, y, text: p.text, font: p.font, size: p.size, color: col });
624
+ const t = Math.max(0.5, p.size * 0.055);
625
+ if (p.r.u || p.r.link) ops.push({ op: 'line', x1: cx, y1: y - p.size * 0.12, x2: cx + p.w, y2: y - p.size * 0.12, w: t, color: col });
626
+ if (p.r.s) ops.push({ op: 'line', x1: cx, y1: y + p.size * 0.3, x2: cx + p.w, y2: y + p.size * 0.3, w: t, color: col });
627
+ if (p.r.link && p.text.trim()) {
628
+ ops.push({ op: 'link', x: cx, y: y - p.size * 0.25, w: p.w, h: p.size * 1.15, url: p.r.link });
629
+ this.links++;
630
+ }
631
+ cx += p.w;
632
+ }
633
+ }
634
+
635
+ marker(ops, mark, line, x, yTop, size) {
636
+ const base = yTop - line.base;
637
+ if (mark.task !== null && mark.task !== undefined) {
638
+ const s = size * 0.78;
639
+ const bx = x - s - 6;
640
+ const by = base - size * 0.08;
641
+ ops.push({ op: 'rect', x: bx, y: by, w: s, h: s, stroke: INK, sw: 0.8 });
642
+ if (mark.task) ops.push({ op: 'path', points: [[bx + s * 0.2, by + s * 0.5], [bx + s * 0.42, by + s * 0.25], [bx + s * 0.82, by + s * 0.8]], w: 1.2, color: INK });
643
+ return;
644
+ }
645
+ if (mark.ordered) {
646
+ const label = markerLabel(mark);
647
+ if (!label) return;
648
+ const w = this.measure(label, 'regular', size);
649
+ ops.push({ op: 'text', x: x - w - 5, y: base, text: label, font: 'regular', size, color: INK });
650
+ return;
651
+ }
652
+ if (mark.kind === 'none') return;
653
+ const cx = x - 10;
654
+ const cy = base + size * 0.32;
655
+ const r = size * 0.17;
656
+ const kind = mark.kind || ['disc', 'circle', 'square'][mark.level % 3];
657
+ if (kind === 'circle') ops.push({ op: 'circle', x: cx, y: cy, r, stroke: INK, sw: size * 0.07 });
658
+ else if (kind === 'square') ops.push({ op: 'rect', x: cx - r, y: cy - r, w: r * 2, h: r * 2, fill: INK });
659
+ else ops.push({ op: 'circle', x: cx, y: cy, r, fill: INK });
660
+ }
661
+
662
+ imageSize(b, width) {
663
+ const nat = this.imageFn(b.src);
664
+ if (!nat || !(nat.w > 0) || !(nat.h > 0)) return null;
665
+ let w = b.width ? b.width * 0.75 : nat.w * 0.75;
666
+ let h = b.height ? b.height * 0.75 : b.width ? (w * nat.h) / nat.w : nat.h * 0.75;
667
+ if (b.height && !b.width) w = (h * nat.w) / nat.h;
668
+ const maxH = (this.top - this.bottom) * 0.92;
669
+ const k = Math.min(1, width / w, maxH / h);
670
+ return { w: w * k, h: h * k };
671
+ }
672
+
673
+ items(blocks, width, compact) {
674
+ const out = [];
675
+ const size0 = compact ? this.base * 0.92 : this.base;
676
+ for (let i = 0; i < blocks.length; i++) {
677
+ const b = blocks[i];
678
+ const bars = b.bars || [];
679
+ const indent = b.indent || 0;
680
+ const w = Math.max(24, width - indent);
681
+ const prev = out.length ? out[out.length - 1] : null;
682
+ if (b.type === 'break') {
683
+ out.push({ pagebreak: true });
684
+ continue;
685
+ }
686
+ if (b.type === 'rule') {
687
+ out.push({ space: size0 * 0.5, bars });
688
+ out.push({ h: 2, bars, draw: (ops, x, yTop) => ops.push({ op: 'line', x1: x + indent, y1: yTop - 1, x2: x + indent + w, y2: yTop - 1, w: 0.8, color: RULE }) });
689
+ out.push({ space: size0 * 0.9, bars });
690
+ continue;
691
+ }
692
+ if (b.type === 'image') {
693
+ const sz = this.imageSize(b, w);
694
+ if (!sz) {
695
+ this.skipped++;
696
+ if (b.alt) {
697
+ const lines = this.lines([{ text: b.alt, i: true, color: MUTED }], w, size0);
698
+ for (const line of lines) out.push({ h: line.h, bars, draw: (ops, x, yTop) => this.drawLine(ops, line, x + indent, yTop, w, b.align, true) });
699
+ out.push({ space: size0 * 0.6, bars });
700
+ }
701
+ continue;
702
+ }
703
+ const off = b.align === 'center' ? (w - sz.w) / 2 : b.align === 'right' ? w - sz.w : 0;
704
+ out.push({ h: sz.h, bars, draw: (ops, x, yTop) => ops.push({ op: 'image', src: b.src, x: x + indent + off, y: yTop - sz.h, w: sz.w, h: sz.h }) });
705
+ out.push({ space: compact ? 4 : size0 * 0.75, bars });
706
+ continue;
707
+ }
708
+ if (b.type === 'code') {
709
+ const size = size0 * 0.86;
710
+ const pad = compact ? 4 : 8;
711
+ const lh = size * 1.38;
712
+ const raw = b.text.split('\n');
713
+ const avail = w - pad * 2;
714
+ const rows = [];
715
+ for (const ln of raw) {
716
+ if (!ln) {
717
+ rows.push('');
718
+ continue;
719
+ }
720
+ let rest = ln;
721
+ while (rest.length) {
722
+ let cut = rest.length;
723
+ const font = this.canEncode('mono', rest) ? 'mono' : 'regular';
724
+ if (this.measure(rest, font, size) > avail) {
725
+ let lo = 1;
726
+ let hi = rest.length;
727
+ while (lo < hi) {
728
+ const mid = Math.ceil((lo + hi) / 2);
729
+ if (this.measure(rest.slice(0, mid), font, size) <= avail) lo = mid;
730
+ else hi = mid - 1;
731
+ }
732
+ cut = Math.max(1, lo);
733
+ }
734
+ rows.push(rest.slice(0, cut));
735
+ rest = rest.slice(cut);
736
+ }
737
+ }
738
+ if (!compact && prev && !prev.space) out.push({ space: size0 * 0.2, bars });
739
+ const bg = (ops, x, yTop, h) => ops.push({ op: 'rect', x: x + indent, y: yTop - h, w, h: h + 0.2, fill: CODE_BG });
740
+ out.push({ h: pad, bars, draw: (ops, x, yTop) => bg(ops, x, yTop, pad) });
741
+ for (const row of rows) {
742
+ out.push({
743
+ h: lh,
744
+ bars,
745
+ draw: (ops, x, yTop) => {
746
+ bg(ops, x, yTop, lh);
747
+ if (row.trim()) {
748
+ const font = this.canEncode('mono', row) ? 'mono' : 'regular';
749
+ ops.push({ op: 'text', x: x + indent + pad, y: yTop - size * 1.05, text: row, font, size, color: INK });
750
+ }
751
+ },
752
+ });
753
+ }
754
+ out.push({ h: pad, bars, draw: (ops, x, yTop) => bg(ops, x, yTop, pad) });
755
+ out.push({ space: compact ? 4 : size0 * 0.85, bars });
756
+ continue;
757
+ }
758
+ if (b.type === 'table') {
759
+ if (prev && !prev.space && !compact) out.push({ space: size0 * 0.3, bars });
760
+ out.push({ table: b, indent, width: w, bars });
761
+ out.push({ space: size0 * 0.9, bars });
762
+ continue;
763
+ }
764
+ const head = HEAD[b.style];
765
+ const size = head ? size0 * head[0] : size0;
766
+ const runs = head ? b.runs.map((r) => ({ ...r, b: true, i: r.i || b.style === 'h6' })) : b.runs;
767
+ if (head && !compact && out.length) {
768
+ while (out.length && out[out.length - 1].space) out.pop();
769
+ if (out.length) out.push({ space: size0 * head[1], bars });
770
+ }
771
+ const lines = this.lines(runs, w, size);
772
+ const after = head ? size0 * head[2] + (b.style === 'h1' || b.style === 'h2' ? 6 : 0) : b.mark ? size0 * 0.22 : compact ? 3 : size0 * 0.7;
773
+ lines.forEach((line, n) => {
774
+ const last = n === lines.length - 1;
775
+ const item = { h: line.h, bars, draw: (ops, x, yTop) => this.drawLine(ops, line, x + indent, yTop, w, b.align, last) };
776
+ if (n === 0 && b.mark) {
777
+ const inner = item.draw;
778
+ const mk = b.mark;
779
+ item.draw = (ops, x, yTop) => {
780
+ inner(ops, x, yTop);
781
+ this.marker(ops, mk, line, x + indent, yTop, size);
782
+ };
783
+ }
784
+ if (head) item.keep = true;
785
+ out.push(item);
786
+ });
787
+ if (head && (b.style === 'h1' || b.style === 'h2') && !compact) {
788
+ out.push({ h: 4, keep: true, bars, draw: (ops, x, yTop) => ops.push({ op: 'line', x1: x + indent, y1: yTop - 2, x2: x + indent + w, y2: yTop - 2, w: 0.6, color: RULE }) });
789
+ }
790
+ const next = blocks[i + 1];
791
+ const tightList = b.mark && next && next.mark && next.type === 'para';
792
+ out.push({ space: tightList ? size0 * 0.22 : after, bars });
793
+ }
794
+ while (out.length && out[out.length - 1].space) out.pop();
795
+ for (let i = 0; i < out.length; i++) {
796
+ if (!out[i].space) continue;
797
+ let a = i - 1;
798
+ while (a >= 0 && out[a].space) a--;
799
+ let z = i + 1;
800
+ while (z < out.length && out[z].space) z++;
801
+ const pa = a >= 0 ? out[a].bars || [] : [];
802
+ const pz = z < out.length ? out[z].bars || [] : [];
803
+ const common = [];
804
+ for (let k = 0; k < Math.min(pa.length, pz.length) && pa[k] === pz[k]; k++) common.push(pa[k]);
805
+ out[i].bars = common;
806
+ }
807
+ return out;
808
+ }
809
+
810
+ bars(item, yTop, h) {
811
+ for (const bx of item.bars || []) this.ops.push({ op: 'rect', x: this.left + bx, y: yTop - h, w: 2.5, h, fill: BAR });
812
+ }
813
+
814
+ flow(blocks) {
815
+ const items = this.items(blocks, this.width, false);
816
+ for (let i = 0; i < items.length; i++) {
817
+ const it = items[i];
818
+ if (it.pagebreak) {
819
+ if (!this.atTop) this.newPage();
820
+ continue;
821
+ }
822
+ if (it.space) {
823
+ if (!this.atTop) {
824
+ const h = Math.min(it.space, this.y - this.bottom);
825
+ this.bars(it, this.y, h);
826
+ this.y -= it.space;
827
+ }
828
+ if (this.y < this.bottom) this.newPage();
829
+ continue;
830
+ }
831
+ if (it.table) {
832
+ this.table(it);
833
+ continue;
834
+ }
835
+ let need = it.h;
836
+ if (it.keep) {
837
+ let j = i + 1;
838
+ let body = 0;
839
+ while (j < items.length && body < 2 && !items[j].pagebreak && !items[j].table) {
840
+ const nx = items[j];
841
+ if (nx.space) need += nx.space;
842
+ else {
843
+ need += nx.h;
844
+ if (!nx.keep) body++;
845
+ }
846
+ j++;
847
+ }
848
+ if (j < items.length && items[j].table && body === 0) need += 60;
849
+ }
850
+ if (this.y - (need > this.top - this.bottom ? it.h : need) < this.bottom - 0.01 && !this.atTop) this.newPage();
851
+ this.bars(it, this.y, it.h);
852
+ it.draw(this.ops, this.left, this.y);
853
+ this.y -= it.h;
854
+ }
855
+ }
856
+
857
+ natural(blocks) {
858
+ let min = 0;
859
+ let max = 0;
860
+ for (const b of blocks) {
861
+ if (b.type === 'image') {
862
+ const sz = this.imageSize(b, 10000);
863
+ min = Math.max(min, sz ? Math.min(sz.w, 60) : 0);
864
+ max = Math.max(max, sz ? sz.w : 0);
865
+ continue;
866
+ }
867
+ if (b.type === 'code') {
868
+ for (const ln of b.text.split('\n')) max = Math.max(max, this.measure(ln, 'mono', this.base * 0.8) + 8);
869
+ min = Math.max(min, 60);
870
+ continue;
871
+ }
872
+ if (b.type === 'table') {
873
+ min = Math.max(min, 80);
874
+ max = Math.max(max, 240);
875
+ continue;
876
+ }
877
+ if (!b.runs) continue;
878
+ const size = HEAD[b.style] ? this.base * 0.92 * HEAD[b.style][0] : this.base * 0.92;
879
+ let line = 0;
880
+ let word = 0;
881
+ for (const r of b.runs) {
882
+ if (r.br) {
883
+ max = Math.max(max, line + (b.indent || 0));
884
+ line = 0;
885
+ continue;
886
+ }
887
+ if (!r.text) continue;
888
+ const f = this.pick(r, r.text);
889
+ const s = size * (r.scale || 1);
890
+ line += this.measure(r.text, f, s);
891
+ for (const wd of r.text.split(/ +/)) if (wd) word = Math.max(word, this.measure(wd, f, s));
892
+ }
893
+ max = Math.max(max, line + (b.indent || 0));
894
+ min = Math.max(min, Math.min(word, 150) + (b.indent || 0));
895
+ }
896
+ return { min: min + 2 * CELL_PAD, max: max + 2 * CELL_PAD + 1 };
897
+ }
898
+
899
+ columns(rows, W) {
900
+ const grid = [];
901
+ const placed = rows.map((row, ri) => {
902
+ grid[ri] = grid[ri] || [];
903
+ let col = 0;
904
+ return row.cells.map((c) => {
905
+ while (grid[ri][col]) col++;
906
+ for (let dr = 0; dr < c.rowspan; dr++) {
907
+ grid[ri + dr] = grid[ri + dr] || [];
908
+ for (let dc = 0; dc < c.colspan; dc++) grid[ri + dr][col + dc] = dr ? 'span' : 'cell';
909
+ }
910
+ const p = { ...c, col, row: ri };
911
+ col += c.colspan;
912
+ return p;
913
+ });
914
+ });
915
+ const count = Math.max(1, ...grid.slice(0, rows.length).map((r) => (r ? r.length : 0)));
916
+ const min = new Array(count).fill(2 * CELL_PAD + 12);
917
+ const max = new Array(count).fill(2 * CELL_PAD + 12);
918
+ for (const cells of placed)
919
+ for (const c of cells) {
920
+ if (c.colspan !== 1) continue;
921
+ const n = this.natural(c.blocks);
922
+ min[c.col] = Math.max(min[c.col], n.min);
923
+ max[c.col] = Math.max(max[c.col], n.max);
924
+ }
925
+ const sMin = min.reduce((a, b) => a + b, 0);
926
+ const sMax = max.reduce((a, b) => a + b, 0);
927
+ let widths;
928
+ if (sMax <= W) widths = max.map((m) => m + ((W - sMax) * m) / sMax);
929
+ else if (sMin <= W) {
930
+ const extra = sMax - sMin;
931
+ widths = min.map((m, i) => m + (extra ? ((W - sMin) * (max[i] - min[i])) / extra : (W - sMin) / count));
932
+ } else widths = min.map((m) => (W * m) / sMin);
933
+ return { placed, grid, widths };
934
+ }
935
+
936
+ table(it) {
937
+ const t = it.table;
938
+ if (!t.rows.length) return;
939
+ const W = it.width;
940
+ const { placed, grid, widths } = this.columns(t.rows, W);
941
+ const x0 = this.left + it.indent;
942
+ const xs = [x0];
943
+ for (const w of widths) xs.push(xs[xs.length - 1] + w);
944
+ const prepared = placed.map((cells, ri) =>
945
+ cells.map((c) => {
946
+ const w = xs[c.col + c.colspan] - xs[c.col];
947
+ const items = this.items(c.blocks, w - 2 * CELL_PAD, true).filter((x) => !x.table && !x.pagebreak);
948
+ return { x: xs[c.col], w, items, head: c.head, bg: c.bg, rowspan: ri + c.rowspan > placed.length ? 1 : c.rowspan };
949
+ })
950
+ );
951
+ const headRows = [];
952
+ for (let ri = 0; ri < t.rows.length && t.rows[ri].head; ri++) headRows.push(ri);
953
+ let started = false;
954
+ for (let ri = 0; ri < placed.length; ri++) {
955
+ const spans = (grid[ri] || []).map((g, col) => (g === 'span' ? { col, last: !(grid[ri + 1] && grid[ri + 1][col] === 'span') || ri + 1 >= placed.length } : null)).filter(Boolean);
956
+ const isHead = headRows.includes(ri);
957
+ const repeat = started && !isHead && headRows.length ? headRows : [];
958
+ this.row(prepared[ri], spans, xs, it, repeat.map((h) => prepared[h]));
959
+ started = true;
960
+ }
961
+ }
962
+
963
+ rowHeight(cells) {
964
+ const height = (items) => items.reduce((s, x) => s + (x.space || x.h || 0), 0);
965
+ return Math.max(...cells.map((c) => height(c.items))) + 2 * CELL_PAD;
966
+ }
967
+
968
+ row(cells, spans, xs, it, heads) {
969
+ const height = (items) => items.reduce((s, x) => s + (x.space || x.h || 0), 0);
970
+ let pending = cells.map((c) => c.items.slice());
971
+ let first = true;
972
+ let guard = 0;
973
+ for (;;) {
974
+ if (++guard > 500) break;
975
+ const need = Math.max(...pending.map((its) => height(its))) + 2 * CELL_PAD;
976
+ const avail = this.y - this.bottom;
977
+ const full = this.top - this.bottom;
978
+ if (need > avail && !this.atTop && (need <= full || avail < full * 0.25)) {
979
+ this.newPage();
980
+ for (const h of heads) this.simpleRow(h, xs);
981
+ continue;
982
+ }
983
+ const chunk = Math.min(need, avail);
984
+ const top = this.y;
985
+ const rest = [];
986
+ const fills = [];
987
+ const texts = [];
988
+ cells.forEach((c, i) => {
989
+ if (c.head || c.bg) fills.push({ op: 'rect', x: c.x, y: top - chunk, w: c.w, h: chunk, fill: c.bg || HEAD_BG });
990
+ let y = top - CELL_PAD;
991
+ const its = pending[i];
992
+ let k = 0;
993
+ while (k < its.length) {
994
+ const x = its[k];
995
+ const h = x.space || x.h || 0;
996
+ if (y - h < top - chunk + CELL_PAD - 0.01 && k > 0) break;
997
+ if (x.draw) x.draw(texts, c.x + CELL_PAD, y);
998
+ y -= h;
999
+ k++;
1000
+ }
1001
+ rest.push(its.slice(k));
1002
+ });
1003
+ this.ops.push(...fills, ...texts);
1004
+ const bottomY = top - chunk;
1005
+ const done = rest.every((p) => !p.some((x) => x.draw));
1006
+ const line = (x1, y1, x2, y2) => this.ops.push({ op: 'line', x1, y1, x2, y2, w: 0.6, color: RULE });
1007
+ for (const c of cells) {
1008
+ if (first || c.rowspan === 1) line(c.x, top, c.x + c.w, top);
1009
+ if (c.rowspan === 1 || !done) line(c.x, bottomY, c.x + c.w, bottomY);
1010
+ line(c.x, top, c.x, bottomY);
1011
+ line(c.x + c.w, top, c.x + c.w, bottomY);
1012
+ }
1013
+ for (const s of spans) {
1014
+ line(xs[s.col], top, xs[s.col], bottomY);
1015
+ line(xs[s.col + 1], top, xs[s.col + 1], bottomY);
1016
+ if (s.last || !done) line(xs[s.col], bottomY, xs[s.col + 1], bottomY);
1017
+ }
1018
+ this.bars(it, top, chunk);
1019
+ this.y = bottomY;
1020
+ pending = rest;
1021
+ first = false;
1022
+ if (done) break;
1023
+ this.newPage();
1024
+ for (const h of heads) this.simpleRow(h, xs);
1025
+ first = true;
1026
+ }
1027
+ }
1028
+
1029
+ simpleRow(cells, xs) {
1030
+ const h = this.rowHeight(cells);
1031
+ const top = this.y;
1032
+ for (const c of cells) {
1033
+ this.ops.push({ op: 'rect', x: c.x, y: top - h, w: c.w, h, fill: c.bg || HEAD_BG });
1034
+ let y = top - CELL_PAD;
1035
+ for (const x of c.items) {
1036
+ if (x.draw) x.draw(this.ops, c.x + CELL_PAD, y);
1037
+ y -= x.space || x.h || 0;
1038
+ }
1039
+ const line = (x1, y1, x2, y2) => this.ops.push({ op: 'line', x1, y1, x2, y2, w: 0.6, color: RULE });
1040
+ line(c.x, top, c.x + c.w, top);
1041
+ line(c.x, top - h, c.x + c.w, top - h);
1042
+ line(c.x, top, c.x, top - h);
1043
+ line(c.x + c.w, top, c.x + c.w, top - h);
1044
+ }
1045
+ this.y = top - h;
1046
+ }
1047
+
1048
+ numbers() {
1049
+ const n = this.pages.length;
1050
+ this.pages.forEach((p, i) => {
1051
+ const label = i + 1 + ' / ' + n;
1052
+ const size = 8.5;
1053
+ const w = this.measure(label, 'regular', size);
1054
+ p.ops.push({ op: 'text', x: (this.w - w) / 2, y: Math.max(14, this.m.bottom / 2 - size / 2), text: label, font: 'regular', size, color: MUTED });
1055
+ });
1056
+ }
1057
+ }
1058
+
1059
+ export function layoutDocument(blocks, opts = {}) {
1060
+ const L = new Layout(opts);
1061
+ L.flow(blocks);
1062
+ while (L.pages.length > 1 && !L.pages[L.pages.length - 1].ops.length) L.pages.pop();
1063
+ if (opts.numbers) L.numbers();
1064
+ const fonts = new Set();
1065
+ for (const p of L.pages) for (const o of p.ops) if (o.op === 'text') fonts.add(o.font);
1066
+ return { width: L.w, height: L.h, pages: L.pages, skipped: L.skipped, links: L.links, fonts: [...fonts] };
1067
+ }
1068
+
1069
+ export function cssRules(text) {
1070
+ const src = String(text || '').replace(/\/\*[\s\S]*?\*\//g, '');
1071
+ const out = [];
1072
+ let i = 0;
1073
+ const block = (from) => {
1074
+ let depth = 0;
1075
+ for (let k = from; k < src.length; k++) {
1076
+ if (src[k] === '{') depth++;
1077
+ else if (src[k] === '}' && !--depth) return k;
1078
+ }
1079
+ return src.length;
1080
+ };
1081
+ while (i < src.length) {
1082
+ const open = src.indexOf('{', i);
1083
+ if (open < 0) break;
1084
+ const head = src.slice(i, open).trim();
1085
+ const close = block(open);
1086
+ const body = src.slice(open + 1, close);
1087
+ if (head.startsWith('@')) {
1088
+ if (/^@media\b/i.test(head) && /\b(print|all)\b/i.test(head) && !/\bnot\b/i.test(head)) out.push(...cssRules(body));
1089
+ } else if (head) {
1090
+ const decls = parseStyle(body);
1091
+ for (const sel of head.split(',')) {
1092
+ const s = sel.trim();
1093
+ if (s) out.push({ selector: s, decls });
1094
+ }
1095
+ }
1096
+ i = close + 1;
1097
+ }
1098
+ return out;
1099
+ }
1100
+
1101
+ export function specificity(sel) {
1102
+ const s = sel.replace(/::?[a-z-]+(\([^)]*\))?/gi, (m) => (m.startsWith('::') ? ' ' : '.p'));
1103
+ const ids = (s.match(/#[\w-]+/g) || []).length;
1104
+ const cls = (s.match(/\.[\w-]+|\[[^\]]*\]/g) || []).length;
1105
+ const tags = (s.replace(/#[\w-]+|\.[\w-]+|\[[^\]]*\]/g, ' ').match(/(^|[\s>+~])[a-z][\w-]*/gi) || []).length;
1106
+ return ids * 10000 + cls * 100 + tags;
1107
+ }
1108
+
1109
+ function compoundMatch(n, c) {
1110
+ if (!n || n.nodeType !== 1) return false;
1111
+ const m = /^([a-z][\w-]*|\*)?((?:[.#][\w-]+)*)$/i.exec(c);
1112
+ if (!m) return null;
1113
+ if (m[1] && m[1] !== '*' && tagOf(n) !== m[1].toLowerCase()) return false;
1114
+ const classes = String(attr(n, 'class') || '').split(/\s+/);
1115
+ for (const part of m[2].match(/[.#][\w-]+/g) || []) {
1116
+ if (part[0] === '#' && attr(n, 'id') !== part.slice(1)) return false;
1117
+ if (part[0] === '.' && !classes.includes(part.slice(1))) return false;
1118
+ }
1119
+ return true;
1120
+ }
1121
+
1122
+ export function simpleSelect(root, selector) {
1123
+ const parts = selector.replace(/\s*>\s*/g, ' ').trim().split(/\s+/);
1124
+ if (parts.some((p) => compoundMatch({ nodeType: 1, tagName: 'x', attrs: {} }, p) === null)) return [];
1125
+ const found = [];
1126
+ const walk = (n) => {
1127
+ for (const k of kids(n)) {
1128
+ if (k.nodeType !== 1) continue;
1129
+ if (compoundMatch(k, parts[parts.length - 1])) {
1130
+ let ok = true;
1131
+ let up = k.parentNode;
1132
+ for (let i = parts.length - 2; i >= 0 && ok; i--) {
1133
+ while (up && !compoundMatch(up, parts[i])) up = up.parentNode;
1134
+ if (!up) ok = false;
1135
+ else up = up.parentNode;
1136
+ }
1137
+ if (ok) found.push(k);
1138
+ }
1139
+ walk(k);
1140
+ }
1141
+ };
1142
+ walk(root);
1143
+ return found;
1144
+ }
1145
+
1146
+ export function applyStyleSheets(root, select = simpleSelect) {
1147
+ let css = '';
1148
+ const collect = (n) => {
1149
+ for (const k of kids(n)) {
1150
+ if (k.nodeType !== 1) continue;
1151
+ if (tagOf(k) === 'style') css += kids(k).map((t) => t.nodeValue || t.data || '').join('') + '\n';
1152
+ else collect(k);
1153
+ }
1154
+ };
1155
+ collect(root);
1156
+ if (!css.trim()) return 0;
1157
+ const hits = [];
1158
+ cssRules(css).forEach((r, order) => {
1159
+ let nodes = [];
1160
+ try {
1161
+ nodes = Array.from(select(root, r.selector) || []);
1162
+ } catch (e) {
1163
+ nodes = [];
1164
+ }
1165
+ const spec = specificity(r.selector);
1166
+ for (const n of nodes) hits.push({ n, spec, order, decls: r.decls });
1167
+ });
1168
+ hits.sort((a, b) => a.spec - b.spec || a.order - b.order);
1169
+ const acc = new Map();
1170
+ for (const h of hits) acc.set(h.n, { ...(acc.get(h.n) || {}), ...h.decls });
1171
+ for (const [n, decls] of acc) {
1172
+ const merged = { ...decls, ...parseStyle(attr(n, 'style')) };
1173
+ const text = Object.entries(merged).map(([k, v]) => k + ':' + v).join(';');
1174
+ if (typeof n.setAttribute === 'function') n.setAttribute('style', text);
1175
+ else n.attrs = { ...(n.attrs || {}), style: text };
1176
+ }
1177
+ return acc.size;
1178
+ }
1179
+
1180
+ export function blocksText(blocks) {
1181
+ const out = [];
1182
+ for (const b of blocks) {
1183
+ if (b.runs) out.push(b.runs.map((r) => (r.br ? '\n' : r.text || '')).join(''));
1184
+ else if (b.type === 'code') out.push(b.text);
1185
+ else if (b.type === 'table') for (const r of b.rows) for (const c of r.cells) out.push(blocksText(c.blocks));
1186
+ }
1187
+ return out.join('\n');
1188
+ }