ntk 7.7.0 → 8.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/README.md +9 -10
- package/lib/app.js +4 -0
- package/lib/fontconfig.js +185 -61
- package/lib/index.js +2 -29
- package/lib/renderingcontext_2d.js +583 -19
- package/lib/shadow.js +199 -0
- package/lib/text/fontsource.js +47 -3
- package/lib/text/layout.js +4 -4
- package/lib/widgets/svgview.js +14 -16
- package/package.json +4 -9
- package/lib/widgets/css.js +0 -673
- package/lib/widgets/highlight.js +0 -97
- package/lib/widgets/htmlview.js +0 -893
- package/lib/widgets/markdown.js +0 -148
- package/lib/widgets/markdownview.js +0 -492
- package/lib/widgets/tex.js +0 -679
- package/lib/yoga.js +0 -119
package/lib/widgets/css.js
DELETED
|
@@ -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
|
-
}
|