ntk 7.6.1 → 8.0.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.
@@ -1,673 +0,0 @@
1
- // CSS support for HtmlView: stylesheet parsing (postcss), selector matching
2
- // (css-select), cascade (specificity + order + !important), inheritance and
3
- // computed-value resolution for the property subset the widget renders.
4
- import { selectAll } from 'css-select';
5
- import postcss from 'postcss';
6
-
7
- // Parse a CSS color to premultiplied [r, g, b, a] floats in 0..1, or null.
8
- // These land on `ctx.fillStyle` as arrays (see htmlview's paint), which is
9
- // the already-premultiplied form, so the premultiplying happens in color.js.
10
- // Imported as well as re-exported: the cascade below calls it too.
11
- import { cssColor } from '../color.js';
12
-
13
- export { cssColor };
14
-
15
- const FONT_SIZE_KEYWORDS = {
16
- 'xx-small': 9,
17
- 'x-small': 10,
18
- small: 13,
19
- medium: 16,
20
- large: 18,
21
- 'x-large': 24,
22
- 'xx-large': 32
23
- };
24
-
25
- /**
26
- * Parse a CSS length into `{ px }`, `{ pct }` or `'auto'`; null if
27
- * unsupported. `em` resolves against `emBase` (the element's font size).
28
- */
29
- export function cssLength(value, emBase = 16, rootSize = 16) {
30
- if (value == null) return null;
31
- value = String(value).trim();
32
- if (value === 'auto') return 'auto';
33
- if (value === '0') return { px: 0 };
34
- const m = /^(-?\d*\.?\d+)(px|pt|em|rem|%)$/.exec(value);
35
- if (!m) return null;
36
- const n = parseFloat(m[1]);
37
- switch (m[2]) {
38
- case 'px':
39
- return { px: n };
40
- case 'pt':
41
- return { px: (n * 4) / 3 };
42
- case 'em':
43
- return { px: n * emBase };
44
- case 'rem':
45
- return { px: n * rootSize };
46
- case '%':
47
- return { pct: n };
48
- }
49
- return null;
50
- }
51
-
52
- // ---------------------------------------------------------------------------
53
- // specificity
54
-
55
- // (ids, classes/attributes/pseudo-classes, types/pseudo-elements) packed into
56
- // one comparable number. Computed textually from the selector — robust enough
57
- // for the supported subset.
58
- export function specificity(selector) {
59
- let s = selector
60
- .replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, 'v') // string literals
61
- .replace(/\\./g, 'c'); // escaped chars
62
- let b = 0;
63
- // attribute selectors (may contain anything except ])
64
- s = s.replace(/\[[^\]]*\]/g, () => ((b += 1), ' '));
65
- let a = 0;
66
- s = s.replace(/#[\w-]+/g, () => ((a += 1), ' '));
67
- s = s.replace(/\.[\w-]+/g, () => ((b += 1), ' '));
68
- let c = 0;
69
- s = s.replace(/::[\w-]+/g, () => ((c += 1), ' '));
70
- // :not() adds the specificity of its argument but not itself
71
- s = s.replace(/:not\(/g, '(');
72
- s = s.replace(/:[\w-]+(\([^)]*\))?/g, () => ((b += 1), ' '));
73
- s.replace(/(^|[\s>+~(,])[a-zA-Z][\w-]*/g, () => ((c += 1), ' '));
74
- return a * 1_000_000 + b * 1_000 + c;
75
- }
76
-
77
- // ---------------------------------------------------------------------------
78
- // stylesheet parsing
79
-
80
- // media queries are not evaluated: rules under a plain `screen`/`all` @media
81
- // apply, anything conditional is skipped
82
- const APPLIED_MEDIA = /^\s*(all|screen)?\s*$/;
83
-
84
- /**
85
- * Parse a stylesheet into flat rule entries
86
- * `{ selector, specificity, decls: [{ prop, value, important }] }`.
87
- * Invalid CSS is dropped (postcss safe-ish: parse errors throw — callers
88
- * treat a throwing sheet as empty).
89
- */
90
- export function parseStylesheet(cssText) {
91
- let root;
92
- try {
93
- root = postcss.parse(cssText);
94
- } catch {
95
- return [];
96
- }
97
- const rules = [];
98
- root.walkRules((rule) => {
99
- if (rule.parent?.type === 'atrule') {
100
- const at = rule.parent;
101
- if (at.name !== 'media' || !APPLIED_MEDIA.test(at.params)) return;
102
- }
103
- const decls = [];
104
- for (const node of rule.nodes ?? []) {
105
- if (node.type !== 'decl') continue;
106
- decls.push({ prop: node.prop.toLowerCase(), value: node.value.trim(), important: !!node.important });
107
- }
108
- if (!decls.length) return;
109
- for (const selector of rule.selectors ?? [rule.selector]) {
110
- rules.push({ selector: selector.trim(), specificity: specificity(selector), decls });
111
- }
112
- });
113
- return rules;
114
- }
115
-
116
- /** parse an inline `style="..."` attribute into decl entries */
117
- export function parseInlineStyle(text) {
118
- const rules = parseStylesheet(`x{${text}}`);
119
- return rules.length ? rules[0].decls : [];
120
- }
121
-
122
- // ---------------------------------------------------------------------------
123
- // the UA stylesheet
124
-
125
- export const UA_STYLESHEET = `
126
- html, body, div, p, h1, h2, h3, h4, h5, h6, ul, ol, li, dl, dt, dd,
127
- blockquote, pre, hr, address, figure, figcaption, fieldset, form,
128
- table, caption, section, article, header, footer, nav, aside, main,
129
- details, summary { display: block }
130
- head, style, script, title, meta, link, template, noscript { display: none }
131
- body { margin: 8px }
132
- p, blockquote, ul, ol, dl, pre, table, figure { margin-top: 1em; margin-bottom: 1em }
133
- h1 { font-size: 2em; margin-top: 0.67em; margin-bottom: 0.67em; font-weight: bold }
134
- h2 { font-size: 1.5em; margin-top: 0.83em; margin-bottom: 0.83em; font-weight: bold }
135
- h3 { font-size: 1.17em; margin-top: 1em; margin-bottom: 1em; font-weight: bold }
136
- h4 { margin-top: 1.33em; margin-bottom: 1.33em; font-weight: bold }
137
- h5 { font-size: 0.83em; margin-top: 1.67em; margin-bottom: 1.67em; font-weight: bold }
138
- h6 { font-size: 0.75em; margin-top: 2.33em; margin-bottom: 2.33em; font-weight: bold }
139
- ul, ol { padding-left: 40px }
140
- ul { list-style-type: disc }
141
- ol { list-style-type: decimal }
142
- ul ul, ol ul { list-style-type: circle }
143
- li { display: list-item }
144
- b, strong { font-weight: bold }
145
- i, em, cite, var { font-style: italic }
146
- u, ins { text-decoration: underline }
147
- s, strike, del { text-decoration: line-through }
148
- code, pre, kbd, samp, tt { font-family: monospace; font-size: 0.9em }
149
- pre { white-space: pre }
150
- a { color: #0b61c9; text-decoration: underline }
151
- blockquote { margin-left: 40px; margin-right: 40px }
152
- figure { margin-left: 40px; margin-right: 40px }
153
- hr { border-bottom: 1px solid #999999; margin-top: 0.5em; margin-bottom: 0.5em }
154
- img { display: inline-block }
155
- table { display: flex; flex-direction: column }
156
- thead, tbody, tfoot { display: flex; flex-direction: column }
157
- tr { display: flex; flex-direction: row }
158
- td, th { display: flex; flex-direction: column; flex: 1; padding: 3px }
159
- th { font-weight: bold }
160
- center { display: block; text-align: center }
161
- big { font-size: 1.2em }
162
- small, sub, sup { font-size: 0.83em }
163
- dd { margin-left: 40px }
164
- dt { font-weight: bold }
165
- summary { font-weight: bold }
166
- `;
167
-
168
- // ---------------------------------------------------------------------------
169
- // computed styles
170
-
171
- const INHERITED = new Set([
172
- 'color',
173
- 'font-family',
174
- 'font-size',
175
- 'font-weight',
176
- 'font-style',
177
- 'line-height',
178
- 'text-align',
179
- 'white-space',
180
- 'list-style-type'
181
- ]);
182
-
183
- export const DEFAULT_STYLE = {
184
- display: 'inline',
185
- color: [0.133, 0.133, 0.133, 1],
186
- background: null,
187
- fontFamily: 'sans-serif',
188
- fontSize: 16,
189
- fontWeight: 400,
190
- fontStyle: 'normal',
191
- lineHeight: { mult: 1.25 },
192
- textAlign: 'left',
193
- textDecoration: 'none',
194
- whiteSpace: 'normal',
195
- listStyleType: 'disc',
196
- margin: ['auto0', 'auto0', 'auto0', 'auto0'], // see note below
197
- padding: [{ px: 0 }, { px: 0 }, { px: 0 }, { px: 0 }],
198
- borderWidth: [0, 0, 0, 0],
199
- borderColor: [null, null, null, null], // null -> currentColor
200
- borderStyle: ['none', 'none', 'none', 'none'],
201
- width: 'auto',
202
- height: 'auto',
203
- minWidth: null,
204
- maxWidth: null,
205
- minHeight: null,
206
- maxHeight: null,
207
- flexDirection: 'row',
208
- flexWrap: 'nowrap',
209
- justifyContent: 'flex-start',
210
- alignItems: 'stretch',
211
- alignSelf: 'auto',
212
- flexGrow: 0,
213
- flexShrink: 1,
214
- flexBasis: 'auto',
215
- rowGap: 0,
216
- columnGap: 0
217
- };
218
-
219
- const SIDES = ['top', 'right', 'bottom', 'left'];
220
-
221
- // expand 1-4 value shorthand into [top, right, bottom, left] value strings
222
- function expandBox(value) {
223
- const parts = value.split(/\s+/).filter(Boolean);
224
- if (parts.length === 0) return null;
225
- const [a, b = a, c = a, d = b] = parts;
226
- return [a, b, c, d];
227
- }
228
-
229
- function splitOutsideParens(value) {
230
- const out = [];
231
- let depth = 0;
232
- let cur = '';
233
- for (const ch of value) {
234
- if (ch === '(') depth++;
235
- if (ch === ')') depth--;
236
- if (/\s/.test(ch) && depth === 0) {
237
- if (cur) out.push(cur);
238
- cur = '';
239
- } else {
240
- cur += ch;
241
- }
242
- }
243
- if (cur) out.push(cur);
244
- return out;
245
- }
246
-
247
- // border shorthand: any of width / style / color in any order
248
- function parseBorderShorthand(value) {
249
- const out = { width: null, style: null, color: null };
250
- for (const part of splitOutsideParens(value)) {
251
- if (/^(none|hidden|solid|dashed|dotted|double|groove|ridge|inset|outset)$/.test(part)) {
252
- out.style = part === 'hidden' ? 'none' : part;
253
- } else if (/^(thin|medium|thick)$/.test(part)) {
254
- out.width = { thin: 1, medium: 3, thick: 5 }[part];
255
- } else if (cssLength(part) && cssLength(part) !== 'auto') {
256
- out.width = cssLength(part).px ?? null;
257
- } else if (cssColor(part)) {
258
- out.color = cssColor(part);
259
- }
260
- }
261
- return out;
262
- }
263
-
264
- const DISPLAY_VALUES = new Set([
265
- 'block',
266
- 'inline',
267
- 'inline-block',
268
- 'flex',
269
- 'inline-flex',
270
- 'list-item',
271
- 'none'
272
- ]);
273
-
274
- /**
275
- * Apply one declaration to a computed style object (mutates `st`).
276
- * `emBase` is the element's own font size (already resolved), used for
277
- * `em` lengths on non-font-size properties.
278
- */
279
- function applyDecl(st, prop, value, emBase, rootSize, parent) {
280
- const len = (v) => cssLength(v, emBase, rootSize);
281
- const px = (v) => {
282
- const l = len(v);
283
- return l && l !== 'auto' && l.px !== undefined ? l.px : null;
284
- };
285
- const sizeVal = (v) => {
286
- const l = len(v);
287
- if (l === 'auto') return 'auto';
288
- if (!l) return null;
289
- return l; // {px} or {pct}
290
- };
291
-
292
- switch (prop) {
293
- case 'display': {
294
- const v = value.toLowerCase();
295
- if (DISPLAY_VALUES.has(v)) st.display = v === 'inline-flex' ? 'flex' : v;
296
- break;
297
- }
298
- case 'color': {
299
- const c = cssColor(value);
300
- if (c) st.color = c;
301
- break;
302
- }
303
- case 'background':
304
- case 'background-color': {
305
- if (value === 'none') {
306
- st.background = null;
307
- break;
308
- }
309
- // background shorthand: take the first color-looking token
310
- for (const part of splitOutsideParens(value)) {
311
- const c = cssColor(part);
312
- if (c) {
313
- st.background = c;
314
- break;
315
- }
316
- }
317
- break;
318
- }
319
- case 'font-family':
320
- st.fontFamily = value.split(',')[0].trim().replace(/^["']|["']$/g, '') || st.fontFamily;
321
- break;
322
- case 'font-size': {
323
- const v = value.toLowerCase();
324
- if (FONT_SIZE_KEYWORDS[v] !== undefined) {
325
- st.fontSize = FONT_SIZE_KEYWORDS[v];
326
- } else {
327
- // em/% for font-size resolve against the parent font size
328
- const l = cssLength(v, parent.fontSize, rootSize);
329
- if (l && l !== 'auto') {
330
- st.fontSize = l.px !== undefined ? l.px : (l.pct / 100) * parent.fontSize;
331
- }
332
- }
333
- break;
334
- }
335
- case 'font-weight': {
336
- const v = value.toLowerCase();
337
- if (v === 'bold' || v === 'bolder') st.fontWeight = 700;
338
- else if (v === 'normal' || v === 'lighter') st.fontWeight = 400;
339
- else if (/^\d+$/.test(v)) st.fontWeight = parseInt(v, 10);
340
- break;
341
- }
342
- case 'font-style': {
343
- const v = value.toLowerCase();
344
- if (v === 'italic' || v === 'oblique') st.fontStyle = 'italic';
345
- else if (v === 'normal') st.fontStyle = 'normal';
346
- break;
347
- }
348
- case 'line-height': {
349
- const v = value.toLowerCase();
350
- if (v === 'normal') st.lineHeight = { mult: 1.25 };
351
- else if (/^\d*\.?\d+$/.test(v)) st.lineHeight = { mult: parseFloat(v) };
352
- else {
353
- const l = len(v);
354
- if (l && l.px !== undefined) st.lineHeight = { px: l.px };
355
- else if (l && l.pct !== undefined) st.lineHeight = { mult: l.pct / 100 };
356
- }
357
- break;
358
- }
359
- case 'text-align': {
360
- const v = value.toLowerCase();
361
- if (['left', 'right', 'center', 'justify', 'start', 'end'].includes(v)) {
362
- st.textAlign = v === 'justify' ? 'left' : v === 'start' ? 'left' : v === 'end' ? 'right' : v;
363
- }
364
- break;
365
- }
366
- case 'text-decoration':
367
- case 'text-decoration-line': {
368
- const v = value.toLowerCase();
369
- if (v.includes('underline')) st.textDecoration = 'underline';
370
- else if (v.includes('line-through')) st.textDecoration = 'line-through';
371
- else if (v.includes('none')) st.textDecoration = 'none';
372
- break;
373
- }
374
- case 'white-space': {
375
- const v = value.toLowerCase();
376
- if (['normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line'].includes(v)) st.whiteSpace = v;
377
- break;
378
- }
379
- case 'list-style':
380
- case 'list-style-type': {
381
- const v = value.toLowerCase();
382
- for (const part of v.split(/\s+/)) {
383
- if (['disc', 'circle', 'square', 'decimal', 'none', 'lower-alpha', 'upper-alpha', 'lower-roman', 'upper-roman'].includes(part)) {
384
- st.listStyleType = part;
385
- }
386
- }
387
- break;
388
- }
389
-
390
- case 'margin': {
391
- const box = expandBox(value);
392
- if (box) box.forEach((v, i) => {
393
- const l = sizeVal(v);
394
- if (l) st.margin[i] = l;
395
- });
396
- break;
397
- }
398
- case 'margin-top':
399
- case 'margin-right':
400
- case 'margin-bottom':
401
- case 'margin-left': {
402
- const l = sizeVal(value);
403
- if (l) st.margin[SIDES.indexOf(prop.slice(7))] = l;
404
- break;
405
- }
406
- case 'padding': {
407
- const box = expandBox(value);
408
- if (box) box.forEach((v, i) => {
409
- const l = sizeVal(v);
410
- if (l && l !== 'auto') st.padding[i] = l;
411
- });
412
- break;
413
- }
414
- case 'padding-top':
415
- case 'padding-right':
416
- case 'padding-bottom':
417
- case 'padding-left': {
418
- const l = sizeVal(value);
419
- if (l && l !== 'auto') st.padding[SIDES.indexOf(prop.slice(8))] = l;
420
- break;
421
- }
422
-
423
- case 'border': {
424
- const b = parseBorderShorthand(value);
425
- for (let i = 0; i < 4; i++) {
426
- if (b.style) st.borderStyle[i] = b.style;
427
- st.borderWidth[i] = b.style === 'none' ? 0 : (b.width ?? (b.style ? 3 : st.borderWidth[i]));
428
- if (b.color) st.borderColor[i] = b.color;
429
- }
430
- break;
431
- }
432
- case 'border-top':
433
- case 'border-right':
434
- case 'border-bottom':
435
- case 'border-left': {
436
- const i = SIDES.indexOf(prop.slice(7));
437
- const b = parseBorderShorthand(value);
438
- if (b.style) st.borderStyle[i] = b.style;
439
- st.borderWidth[i] = b.style === 'none' ? 0 : (b.width ?? (b.style ? 3 : st.borderWidth[i]));
440
- if (b.color) st.borderColor[i] = b.color;
441
- break;
442
- }
443
- case 'border-width': {
444
- const box = expandBox(value);
445
- if (box) box.forEach((v, i) => {
446
- const w = /^(thin|medium|thick)$/.test(v) ? { thin: 1, medium: 3, thick: 5 }[v] : px(v);
447
- if (w !== null) st.borderWidth[i] = w;
448
- });
449
- break;
450
- }
451
- case 'border-style': {
452
- const box = expandBox(value.toLowerCase());
453
- if (box) box.forEach((v, i) => {
454
- st.borderStyle[i] = v === 'none' || v === 'hidden' ? 'none' : 'solid';
455
- if (st.borderStyle[i] === 'none') st.borderWidth[i] = 0;
456
- });
457
- break;
458
- }
459
- case 'border-color': {
460
- const box = expandBox(value);
461
- if (box) box.forEach((v, i) => {
462
- const c = cssColor(v);
463
- if (c) st.borderColor[i] = c;
464
- });
465
- break;
466
- }
467
-
468
- case 'width':
469
- case 'height': {
470
- const l = sizeVal(value);
471
- if (l) st[prop] = l;
472
- break;
473
- }
474
- case 'min-width':
475
- case 'max-width':
476
- case 'min-height':
477
- case 'max-height': {
478
- const l = sizeVal(value);
479
- if (l && l !== 'auto') {
480
- st[prop.replace(/-(\w)/, (_, ch) => ch.toUpperCase())] = l;
481
- }
482
- break;
483
- }
484
-
485
- case 'flex-direction': {
486
- const v = value.toLowerCase();
487
- if (['row', 'column', 'row-reverse', 'column-reverse'].includes(v)) st.flexDirection = v;
488
- break;
489
- }
490
- case 'flex-wrap': {
491
- const v = value.toLowerCase();
492
- if (['nowrap', 'wrap', 'wrap-reverse'].includes(v)) st.flexWrap = v;
493
- break;
494
- }
495
- case 'justify-content': {
496
- const v = value.toLowerCase();
497
- if (['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly', 'start', 'end'].includes(v)) {
498
- st.justifyContent = v === 'start' ? 'flex-start' : v === 'end' ? 'flex-end' : v;
499
- }
500
- break;
501
- }
502
- case 'align-items':
503
- case 'align-self': {
504
- const v = value.toLowerCase();
505
- const key = prop === 'align-items' ? 'alignItems' : 'alignSelf';
506
- if (['flex-start', 'flex-end', 'center', 'stretch', 'baseline', 'auto', 'start', 'end'].includes(v)) {
507
- st[key] = v === 'start' ? 'flex-start' : v === 'end' ? 'flex-end' : v;
508
- }
509
- break;
510
- }
511
- case 'flex-grow':
512
- if (/^\d*\.?\d+$/.test(value)) st.flexGrow = parseFloat(value);
513
- break;
514
- case 'flex-shrink':
515
- if (/^\d*\.?\d+$/.test(value)) st.flexShrink = parseFloat(value);
516
- break;
517
- case 'flex-basis': {
518
- const l = sizeVal(value);
519
- if (l) st.flexBasis = l;
520
- break;
521
- }
522
- case 'flex': {
523
- const v = value.toLowerCase().trim();
524
- if (v === 'none') {
525
- st.flexGrow = 0;
526
- st.flexShrink = 0;
527
- st.flexBasis = 'auto';
528
- break;
529
- }
530
- const parts = v.split(/\s+/);
531
- if (/^\d*\.?\d+$/.test(parts[0])) {
532
- st.flexGrow = parseFloat(parts[0]);
533
- st.flexShrink = parts[1] && /^\d*\.?\d+$/.test(parts[1]) ? parseFloat(parts[1]) : 1;
534
- const basis = parts[2] ?? (parts[1] && !/^\d*\.?\d+$/.test(parts[1]) ? parts[1] : '0px');
535
- st.flexBasis = sizeVal(basis) ?? { px: 0 };
536
- }
537
- break;
538
- }
539
- case 'gap': {
540
- const parts = value.split(/\s+/).filter(Boolean);
541
- const row = px(parts[0]);
542
- if (row !== null) st.rowGap = row;
543
- const col = parts[1] ? px(parts[1]) : row;
544
- if (col !== null) st.columnGap = col;
545
- break;
546
- }
547
- case 'row-gap': {
548
- const v = px(value);
549
- if (v !== null) st.rowGap = v;
550
- break;
551
- }
552
- case 'column-gap': {
553
- const v = px(value);
554
- if (v !== null) st.columnGap = v;
555
- break;
556
- }
557
- }
558
- }
559
-
560
- /**
561
- * Compute styles for every element of a parsed document.
562
- *
563
- * @param {object} dom htmlparser2 document node
564
- * @param {string[]} authorSheets stylesheet texts, in cascade order (the UA
565
- * sheet is always applied first)
566
- * @param {object} [opts] { rootFontSize = 16, baseStyle } — `baseStyle`
567
- * overrides DEFAULT_STYLE fields for the root (theming: font, color...)
568
- * @returns {Map<element, computedStyle>}
569
- */
570
- export function computeStyles(dom, authorSheets = [], opts = {}) {
571
- const rootSize = opts.rootFontSize ?? 16;
572
-
573
- // parse all sheets into one ordered rule list (UA first: lowest priority
574
- // at equal specificity because of source order)
575
- const rules = [];
576
- for (const text of [UA_STYLESHEET, ...authorSheets]) {
577
- for (const rule of parseStylesheet(text)) rules.push(rule);
578
- }
579
-
580
- // match each rule once over the whole document
581
- const matches = new Map(); // element -> [{specificity, order, decls}]
582
- rules.forEach((rule, order) => {
583
- let matched;
584
- try {
585
- matched = selectAll(rule.selector, dom);
586
- } catch {
587
- return; // unsupported selector: skip the rule
588
- }
589
- for (const el of matched) {
590
- let list = matches.get(el);
591
- if (!list) matches.set(el, (list = []));
592
- list.push({ specificity: rule.specificity, order, decls: rule.decls });
593
- }
594
- });
595
-
596
- const computed = new Map();
597
- const defaultStyle = { ...DEFAULT_STYLE, ...(opts.baseStyle || {}) };
598
-
599
- const walk = (el, parentStyle) => {
600
- // htmlparser2 marks <style>/<script> with their own node types
601
- if (el.type !== 'tag' && el.type !== 'style' && el.type !== 'script') {
602
- // only elements get computed styles; recurse for containers
603
- for (const child of el.children ?? []) walk(child, parentStyle);
604
- return;
605
- }
606
-
607
- // start from inherited/default values
608
- const st = {
609
- ...defaultStyle,
610
- margin: ['auto0', 'auto0', 'auto0', 'auto0'],
611
- padding: [{ px: 0 }, { px: 0 }, { px: 0 }, { px: 0 }],
612
- borderWidth: [0, 0, 0, 0],
613
- borderColor: [null, null, null, null],
614
- borderStyle: ['none', 'none', 'none', 'none']
615
- };
616
- for (const prop of ['color', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'lineHeight', 'textAlign', 'whiteSpace', 'listStyleType']) {
617
- st[prop] = parentStyle[prop];
618
- }
619
- // text-decoration propagates to inline descendants (approximation of
620
- // CSS decoration propagation)
621
- st.textDecoration = 'none';
622
- st.display = 'inline';
623
- st.background = null;
624
- st.width = 'auto';
625
- st.height = 'auto';
626
- st.minWidth = st.maxWidth = st.minHeight = st.maxHeight = null;
627
- st.flexDirection = 'row';
628
- st.flexWrap = 'nowrap';
629
- st.justifyContent = 'flex-start';
630
- st.alignItems = 'stretch';
631
- st.alignSelf = 'auto';
632
- st.flexGrow = 0;
633
- st.flexShrink = 1;
634
- st.flexBasis = 'auto';
635
- st.rowGap = 0;
636
- st.columnGap = 0;
637
-
638
- // presentational attributes (lowest priority)
639
- const attribs = el.attribs || {};
640
- if (el.name === 'img') {
641
- if (attribs.width && /^\d+$/.test(attribs.width)) st.width = { px: parseInt(attribs.width, 10) };
642
- if (attribs.height && /^\d+$/.test(attribs.height)) st.height = { px: parseInt(attribs.height, 10) };
643
- }
644
-
645
- // cascade: sort matched decl blocks; then apply — font-size first so em
646
- // lengths in the same element resolve against the final font size
647
- const declBlocks = (matches.get(el) ?? [])
648
- .slice()
649
- .sort((x, y) => x.specificity - y.specificity || x.order - y.order);
650
- const decls = [];
651
- for (const block of declBlocks) for (const d of block.decls) decls.push(d);
652
- if (attribs.style) for (const d of parseInlineStyle(attribs.style)) decls.push(d);
653
- // stable partition: non-important then important
654
- const ordered = [...decls.filter((d) => !d.important), ...decls.filter((d) => d.important)];
655
-
656
- for (const d of ordered) {
657
- if (d.prop === 'font-size') applyDecl(st, d.prop, d.value, st.fontSize, rootSize, parentStyle);
658
- }
659
- for (const d of ordered) {
660
- if (d.prop !== 'font-size') applyDecl(st, d.prop, d.value, st.fontSize, rootSize, parentStyle);
661
- }
662
-
663
- // unset margins default to 0 (the 'auto0' marker distinguishes "not
664
- // set" from an explicit `margin: auto`)
665
- st.margin = st.margin.map((m) => (m === 'auto0' ? { px: 0 } : m));
666
-
667
- computed.set(el, st);
668
- for (const child of el.children ?? []) walk(child, st);
669
- };
670
-
671
- for (const child of dom.children ?? []) walk(child, defaultStyle);
672
- return computed;
673
- }