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.
- package/LICENSE +21 -0
- package/README.md +142 -2
- package/bin/snowkit.js +19 -0
- package/package.json +45 -4
- package/src/cli.js +177 -0
- package/src/engine/colorkit.js +338 -0
- package/src/engine/cron.js +148 -0
- package/src/engine/devtools-kit.js +1964 -0
- package/src/engine/docpdf-layout.js +1188 -0
- package/src/engine/docpdf-render.js +106 -0
- package/src/engine/html.js +59 -0
- package/src/engine/jsonkit.js +1241 -0
- package/src/engine/md-render.js +596 -0
- package/src/engine/mdpdf.js +113 -0
- package/src/engine/qrkit.js +1205 -0
- package/src/engine/readability.js +100 -0
- package/src/engine/sqlformat.js +425 -0
- package/src/engine/texttools-kit.js +623 -0
- package/src/files.js +132 -0
- package/src/pdf.js +297 -0
- package/src/qr.js +100 -0
- package/src/server.js +50 -0
- package/src/text.js +189 -0
- package/src/tools.js +223 -0
|
@@ -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
|
+
}
|