@miaskiewicz/turbo-dom 0.7.1 → 0.8.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/Cargo.lock +7 -7
- package/Cargo.toml +7 -5
- package/README.md +13 -8
- package/package.json +1 -1
- package/pkg/package.json +2 -2
- package/pkg/turbo_dom_parser_bg.wasm +0 -0
- package/pkg-web/package.json +2 -2
- package/pkg-web/turbo_dom_parser_bg.wasm +0 -0
- package/src/runtime/cascade.mjs +70 -54
- package/src/runtime/css-shorthand.mjs +1217 -734
- package/src/runtime/custom-elements.mjs +8 -2
- package/src/runtime/dom.mjs +148 -95
- package/src/runtime/events.mjs +106 -85
- package/src/runtime/html-serialize.mjs +28 -24
- package/src/runtime/selectors.mjs +353 -237
- package/src/runtime/stubs.mjs +9 -6
- package/turbo-dom-parser.darwin-arm64.node +0 -0
- package/turbo-dom-parser.linux-arm64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-musl.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
|
@@ -145,8 +145,34 @@ const ATTR_OP = { '~': 'includes', '|': 'dash', '^': 'prefix', $: 'suffix', '*':
|
|
|
145
145
|
// unterminated comment runs to the end. `closeAtEof` (the DOM selector APIs): an
|
|
146
146
|
// unterminated string and every open `[` / `(` are closed, as CSS does at EOF.
|
|
147
147
|
const nameLike = (c) => c !== undefined && (c >= '\x80' || c === '-' || c === '_' || c === '\\' || isLetter(c) || isDigit(c));
|
|
148
|
+
function needsNormalize(src, closeAtEof) {
|
|
149
|
+
return src.indexOf('/*') !== -1 || (closeAtEof && /[[("'\\]/.test(src));
|
|
150
|
+
}
|
|
151
|
+
// The index just past the comment opening at `i` (the input's end when unterminated).
|
|
152
|
+
function commentEnd(src, i) {
|
|
153
|
+
const end = src.indexOf('*/', i + 2);
|
|
154
|
+
return end === -1 ? src.length : end + 2;
|
|
155
|
+
}
|
|
156
|
+
// What a comment between `out` and `next` leaves behind: a separator between two name
|
|
157
|
+
// characters (a space inside `[…]`, the never-valid `!` elsewhere), else nothing.
|
|
158
|
+
function commentSeparator(out, next, open) {
|
|
159
|
+
const prev = out[out.length - 1];
|
|
160
|
+
if ((nameLike(prev) || prev === '#') && nameLike(next)) return open[open.length - 1] === ']' ? ' ' : '!';
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
function trackBracket(c, open) {
|
|
164
|
+
if (c === '[') open.push(']');
|
|
165
|
+
else if (c === '(') open.push(')');
|
|
166
|
+
else if (c === open[open.length - 1]) open.pop();
|
|
167
|
+
}
|
|
168
|
+
// Close an unterminated string and every open `[` / `(` at EOF.
|
|
169
|
+
function closeAtEnd(out, quote, open) {
|
|
170
|
+
if (quote) out += quote;
|
|
171
|
+
while (open.length) out += open.pop();
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
148
174
|
function normalizeSource(src, closeAtEof) {
|
|
149
|
-
if (src
|
|
175
|
+
if (!needsNormalize(src, closeAtEof)) return src;
|
|
150
176
|
let out = '', quote = null;
|
|
151
177
|
const open = [];
|
|
152
178
|
for (let i = 0; i < src.length; i++) {
|
|
@@ -157,24 +183,17 @@ function normalizeSource(src, closeAtEof) {
|
|
|
157
183
|
out += src.slice(i, i + 2); i++; continue;
|
|
158
184
|
}
|
|
159
185
|
if (quote) { if (c === quote) quote = null; out += c; continue; }
|
|
160
|
-
if (c === '
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
end = end === -1 ? src.length : end + 2;
|
|
164
|
-
const prev = out[out.length - 1];
|
|
165
|
-
if ((nameLike(prev) || prev === '#') && nameLike(src[end])) out += open[open.length - 1] === ']' ? ' ' : '!';
|
|
186
|
+
if (c === '/' && src[i + 1] === '*') {
|
|
187
|
+
const end = commentEnd(src, i);
|
|
188
|
+
out += commentSeparator(out, src[end], open);
|
|
166
189
|
i = end - 1;
|
|
167
190
|
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
else
|
|
191
|
+
}
|
|
192
|
+
if (c === '"' || c === "'") quote = c;
|
|
193
|
+
else trackBracket(c, open);
|
|
171
194
|
out += c;
|
|
172
195
|
}
|
|
173
|
-
|
|
174
|
-
if (quote) out += quote;
|
|
175
|
-
while (open.length) out += open.pop();
|
|
176
|
-
}
|
|
177
|
-
return out;
|
|
196
|
+
return closeAtEof ? closeAtEnd(out, quote, open) : out;
|
|
178
197
|
}
|
|
179
198
|
// A quoted string starting at `i` (src[i] is the quote): { value, end } (end past the
|
|
180
199
|
// closing quote; an unterminated string ends with the input, as in CSS — the attribute
|
|
@@ -201,11 +220,37 @@ function scanString(src, i) {
|
|
|
201
220
|
// { op, value }; `ns: 'any'` for `[*|a]`; `ci: true` for the `i` modifier. Throws on what
|
|
202
221
|
// Chrome 156 rejects, including the `s` modifier (so does the Rust engine since
|
|
203
222
|
// turbo-dom#24).
|
|
223
|
+
function skipWsAt(s, i) {
|
|
224
|
+
while (i < s.length && isWs(s[i])) i++;
|
|
225
|
+
return i;
|
|
226
|
+
}
|
|
227
|
+
function skipDigits(s, i) {
|
|
228
|
+
while (i < s.length && isDigit(s[i])) i++;
|
|
229
|
+
return i;
|
|
230
|
+
}
|
|
231
|
+
// The attribute value at `i` (a string or an identifier): { value, end }.
|
|
232
|
+
function scanAttrValue(s, i, src) {
|
|
233
|
+
if (s[i] === '"' || s[i] === "'") return scanString(s, i);
|
|
234
|
+
if (startsIdent(s, i)) return scanIdent(s, i);
|
|
235
|
+
throw syntax(src, 'attribute value');
|
|
236
|
+
}
|
|
237
|
+
// The operator at `i`: { op, end }.
|
|
238
|
+
function scanAttrOp(s, i, src) {
|
|
239
|
+
if (ATTR_OP[s[i]] !== undefined && s[i + 1] === '=') return { op: ATTR_OP[s[i]], end: i + 2 };
|
|
240
|
+
if (s[i] === '=') return { op: 'equals', end: i + 1 };
|
|
241
|
+
throw syntax(src, 'attribute operator');
|
|
242
|
+
}
|
|
243
|
+
// The optional modifier at `i` (only `i`), then the end of the selector.
|
|
244
|
+
function parseAttrModifier(attr, s, i, src) {
|
|
245
|
+
if (i >= s.length) return;
|
|
246
|
+
const m = s[i];
|
|
247
|
+
if (m === 'i' || m === 'I') attr.ci = true;
|
|
248
|
+
else throw syntax(src, 'attribute modifier');
|
|
249
|
+
if (skipWsAt(s, i + 1) < s.length) throw syntax(src, 'attribute selector');
|
|
250
|
+
}
|
|
204
251
|
function parseAttr(raw, src) {
|
|
205
252
|
const s = raw;
|
|
206
|
-
let i = 0;
|
|
207
|
-
const skip = () => { while (i < s.length && isWs(s[i])) i++; };
|
|
208
|
-
skip();
|
|
253
|
+
let i = skipWsAt(s, 0);
|
|
209
254
|
let ns = null;
|
|
210
255
|
if (s[i] === '*' && s[i + 1] === '|') { ns = 'any'; i += 2; }
|
|
211
256
|
else if (s[i] === '|' && s[i + 1] !== '=') { i += 1; }
|
|
@@ -217,33 +262,12 @@ function parseAttr(raw, src) {
|
|
|
217
262
|
if (ns) attr.ns = ns;
|
|
218
263
|
// the HTML-element lookup name, folded once (non-enumerable: the AST stays `{ name, match }`)
|
|
219
264
|
Object.defineProperty(attr, 'lname', { value: nameTok.value.toLowerCase() });
|
|
220
|
-
|
|
265
|
+
i = skipWsAt(s, i);
|
|
221
266
|
if (i === s.length) return attr;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
skip();
|
|
227
|
-
let value;
|
|
228
|
-
if (s[i] === '"' || s[i] === "'") {
|
|
229
|
-
const str = scanString(s, i);
|
|
230
|
-
value = str.value;
|
|
231
|
-
i = str.end;
|
|
232
|
-
} else if (startsIdent(s, i)) {
|
|
233
|
-
const v = scanIdent(s, i);
|
|
234
|
-
value = v.value;
|
|
235
|
-
i = v.end;
|
|
236
|
-
} else throw syntax(src, 'attribute value');
|
|
237
|
-
attr.match = { op, value };
|
|
238
|
-
skip();
|
|
239
|
-
if (i < s.length) {
|
|
240
|
-
const m = s[i];
|
|
241
|
-
if (m === 'i' || m === 'I') attr.ci = true;
|
|
242
|
-
else throw syntax(src, 'attribute modifier');
|
|
243
|
-
i++;
|
|
244
|
-
skip();
|
|
245
|
-
if (i < s.length) throw syntax(src, 'attribute selector');
|
|
246
|
-
}
|
|
267
|
+
const op = scanAttrOp(s, i, src);
|
|
268
|
+
const v = scanAttrValue(s, skipWsAt(s, op.end), src);
|
|
269
|
+
attr.match = { op: op.op, value: v.value };
|
|
270
|
+
parseAttrModifier(attr, s, skipWsAt(s, v.end), src);
|
|
247
271
|
return attr;
|
|
248
272
|
}
|
|
249
273
|
|
|
@@ -317,13 +341,15 @@ const PLAIN_AFTER = new Map([
|
|
|
317
341
|
]);
|
|
318
342
|
// May pseudo-class `p` follow pseudo-element `pe` ({ name, arg })? (Chrome 156; mirrors
|
|
319
343
|
// the Rust allowed_after_pseudo_element.)
|
|
344
|
+
// After an ELEMENT_LIKE pseudo-element.
|
|
345
|
+
function allowedAfterElementLike(p) {
|
|
346
|
+
if (p.arg !== null) return p.name === 'state' || p.name === 'lang' || p.name === 'dir';
|
|
347
|
+
return ELEMENT_LIKE_PLAIN.has(p.name) || (NEVER.has(p.name) && !NOT_AFTER_ELEMENT_LIKE.has(p.name));
|
|
348
|
+
}
|
|
320
349
|
function allowedAfter(pe, p) {
|
|
321
350
|
const name = pe.name;
|
|
322
351
|
if ((p.name === 'is' || p.name === 'where') && p.arg !== null) return name !== 'column' && name !== 'slotted';
|
|
323
|
-
if (ELEMENT_LIKE.has(name))
|
|
324
|
-
if (p.arg !== null) return p.name === 'state' || p.name === 'lang' || p.name === 'dir';
|
|
325
|
-
return ELEMENT_LIKE_PLAIN.has(p.name) || (NEVER.has(p.name) && !NOT_AFTER_ELEMENT_LIKE.has(p.name));
|
|
326
|
-
}
|
|
352
|
+
if (ELEMENT_LIKE.has(name)) return allowedAfterElementLike(p);
|
|
327
353
|
if (p.arg !== null) return false;
|
|
328
354
|
if (SCROLLBAR.has(name)) return AFTER_SCROLLBAR.has(p.name);
|
|
329
355
|
if (name.startsWith('-webkit-') || (name === 'cue' && pe.arg === null)) return USER_ACTION.has(p.name);
|
|
@@ -359,32 +385,33 @@ function parseAnB(arg) {
|
|
|
359
385
|
let sign = 1;
|
|
360
386
|
if (s[0] === '+' || s[0] === '-') { sign = s[0] === '-' ? -1 : 1; i = 1; }
|
|
361
387
|
const ds = i;
|
|
362
|
-
|
|
388
|
+
i = skipDigits(s, i);
|
|
363
389
|
const digits = s.slice(ds, i);
|
|
364
390
|
if (s[i] !== 'n') {
|
|
365
391
|
if (!digits || i !== s.length) return null;
|
|
366
392
|
return { a: 0, b: clamp32(clampInt(sign, digits)) };
|
|
367
393
|
}
|
|
368
394
|
const a = digits ? clamp32(clampInt(sign, digits)) : sign;
|
|
369
|
-
i
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
395
|
+
return parseAnBTail(s, i + 1, a);
|
|
396
|
+
}
|
|
397
|
+
// `n-123` is one ident / dimension unit: its integer (after the `n` at i - 1) is converted
|
|
398
|
+
// exactly and must fit.
|
|
399
|
+
function gluedIntegerFits(s, i) {
|
|
400
|
+
if (s[i] !== '-' || i + 1 >= s.length || !isDigit(s[i + 1])) return true;
|
|
401
|
+
return clampInt(-1, s.slice(i + 1, skipDigits(s, i + 1))) >= INT_MIN;
|
|
402
|
+
}
|
|
403
|
+
// The part after An's `n` (at i - 1): nothing, or a signed B.
|
|
404
|
+
function parseAnBTail(s, i, a) {
|
|
405
|
+
if (!gluedIntegerFits(s, i)) return null;
|
|
406
|
+
i = skipWsAt(s, i);
|
|
377
407
|
if (i === s.length) return { a, b: 0 };
|
|
378
408
|
if (s[i] !== '+' && s[i] !== '-') return null;
|
|
379
409
|
const bs = s[i] === '-' ? -1 : 1;
|
|
380
|
-
i
|
|
381
|
-
|
|
382
|
-
const bd = i;
|
|
383
|
-
while (i < s.length && isDigit(s[i])) i++;
|
|
410
|
+
const bd = skipWsAt(s, i + 1);
|
|
411
|
+
i = skipDigits(s, bd);
|
|
384
412
|
if (i === bd) return null;
|
|
385
413
|
const b = clamp32(clampInt(bs, s.slice(bd, i)));
|
|
386
|
-
|
|
387
|
-
return i === s.length ? { a, b } : null;
|
|
414
|
+
return skipWsAt(s, i) === s.length ? { a, b } : null;
|
|
388
415
|
}
|
|
389
416
|
// The `of` keyword of `:nth-child(An+B of S)`: lowercase, whitespace before it, a non-empty
|
|
390
417
|
// An+B part, and no name code point right after it. Index or -1.
|
|
@@ -407,6 +434,61 @@ function nthMatch(a, b, index) {
|
|
|
407
434
|
// ── lexer ───────────────────────────────────────────────────────────────────
|
|
408
435
|
// Token kinds: ws | comma | comb (v) | type (ns, name) | id (v) | class (v) | attr |
|
|
409
436
|
// pseudo (name, arg) | pelem (name, arg).
|
|
437
|
+
// The type / universal selector at `i` (src[i] is `*`, `|` or an identifier start), with
|
|
438
|
+
// an optional `*|` / `|` namespace prefix: pushes its token, returns the next index.
|
|
439
|
+
function scanTypeToken(src, i, tokens) {
|
|
440
|
+
const c = src[i];
|
|
441
|
+
let ns = null, name = null;
|
|
442
|
+
if (c === '*') i++;
|
|
443
|
+
else if (c !== '|') { const t = scanIdent(src, i); name = t.value; i = t.end; }
|
|
444
|
+
if (src[i] === '|' && src[i + 1] !== '=') {
|
|
445
|
+
if (name !== null) throw syntax(src, 'undeclared namespace prefix');
|
|
446
|
+
ns = c === '*' ? 'any' : 'none';
|
|
447
|
+
i++;
|
|
448
|
+
if (src[i] === '*') i++;
|
|
449
|
+
else if (startsIdent(src, i)) { const t = scanIdent(src, i); name = t.value; i = t.end; }
|
|
450
|
+
else throw syntax(src, 'type selector after a namespace prefix');
|
|
451
|
+
} else if (c === '|') throw syntax(src, `unexpected '|'`);
|
|
452
|
+
tokens.push({ k: 'type', ns, name: name === null ? null : name.toLowerCase() });
|
|
453
|
+
return i;
|
|
454
|
+
}
|
|
455
|
+
// The pseudo-class / pseudo-element at `i` (src[i] is `:`): pushes its token, returns the
|
|
456
|
+
// next index.
|
|
457
|
+
function scanPseudoToken(src, i, tokens) {
|
|
458
|
+
const n = src.length;
|
|
459
|
+
i++;
|
|
460
|
+
const element = src[i] === ':';
|
|
461
|
+
if (element) i++;
|
|
462
|
+
if (!startsIdent(src, i)) throw syntax(src, `unexpected ':'`);
|
|
463
|
+
const t = scanIdent(src, i);
|
|
464
|
+
const name = t.value.toLowerCase();
|
|
465
|
+
i = t.end;
|
|
466
|
+
let arg = null;
|
|
467
|
+
if (src[i] === '(') {
|
|
468
|
+
const { inner, end } = scanBalanced(src, i + 1, '(', ')');
|
|
469
|
+
arg = inner;
|
|
470
|
+
i = end < n ? end + 1 : n; // an unterminated argument closes at the end (CSS)
|
|
471
|
+
}
|
|
472
|
+
const legacy = !element && LEGACY_PSEUDO_ELEMENTS.has(name);
|
|
473
|
+
tokens.push({ k: element || legacy ? 'pelem' : 'pseudo', name, arg, legacy });
|
|
474
|
+
return i;
|
|
475
|
+
}
|
|
476
|
+
// The attribute selector at `i` (src[i] is `[`): pushes its token, returns the next index.
|
|
477
|
+
function scanAttrToken(src, i, tokens) {
|
|
478
|
+
const { inner, end } = scanBalanced(src, i + 1, '[', ']');
|
|
479
|
+
if (end >= src.length) throw new SyntaxError(`unterminated attribute selector: ${src.slice(i)}`);
|
|
480
|
+
tokens.push({ k: 'attr', attr: parseAttr(inner, src) });
|
|
481
|
+
return end + 1;
|
|
482
|
+
}
|
|
483
|
+
// The id / class selector at `i` (src[i] is `#` / `.`): pushes its token, returns the next
|
|
484
|
+
// index.
|
|
485
|
+
function scanHashOrClassToken(src, i, tokens) {
|
|
486
|
+
const c = src[i];
|
|
487
|
+
if (!startsIdent(src, i + 1)) throw syntax(src, `unexpected '${c}'`);
|
|
488
|
+
const t = scanIdent(src, i + 1);
|
|
489
|
+
tokens.push({ k: c === '#' ? 'id' : 'class', v: t.value });
|
|
490
|
+
return t.end;
|
|
491
|
+
}
|
|
410
492
|
function tokenize(src) {
|
|
411
493
|
const tokens = [];
|
|
412
494
|
const n = src.length;
|
|
@@ -414,53 +496,15 @@ function tokenize(src) {
|
|
|
414
496
|
while (i < n) {
|
|
415
497
|
const c = src[i];
|
|
416
498
|
if (isWs(c)) {
|
|
417
|
-
|
|
499
|
+
i = skipWsAt(src, i);
|
|
418
500
|
tokens.push({ k: 'ws' });
|
|
419
501
|
} else if (c === ',') { tokens.push({ k: 'comma' }); i++; }
|
|
420
502
|
else if (c === '>' || c === '+' || c === '~') { tokens.push({ k: 'comb', v: c }); i++; }
|
|
421
|
-
else if (c === '*' || c === '|' || startsIdent(src, i))
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (src[i] === '|' && src[i + 1] !== '=') {
|
|
427
|
-
if (name !== null) throw syntax(src, 'undeclared namespace prefix');
|
|
428
|
-
ns = c === '*' ? 'any' : 'none';
|
|
429
|
-
i++;
|
|
430
|
-
if (src[i] === '*') i++;
|
|
431
|
-
else if (startsIdent(src, i)) { const t = scanIdent(src, i); name = t.value; i = t.end; }
|
|
432
|
-
else throw syntax(src, 'type selector after a namespace prefix');
|
|
433
|
-
} else if (c === '|') throw syntax(src, `unexpected '|'`);
|
|
434
|
-
tokens.push({ k: 'type', ns, name: name === null ? null : name.toLowerCase() });
|
|
435
|
-
} else if (c === '#' || c === '.') {
|
|
436
|
-
if (!startsIdent(src, i + 1)) throw syntax(src, `unexpected '${c}'`);
|
|
437
|
-
const t = scanIdent(src, i + 1);
|
|
438
|
-
tokens.push({ k: c === '#' ? 'id' : 'class', v: t.value });
|
|
439
|
-
i = t.end;
|
|
440
|
-
} else if (c === '[') {
|
|
441
|
-
const { inner, end } = scanBalanced(src, i + 1, '[', ']');
|
|
442
|
-
if (end >= n) throw new SyntaxError(`unterminated attribute selector: ${src.slice(i)}`);
|
|
443
|
-
tokens.push({ k: 'attr', attr: parseAttr(inner, src) });
|
|
444
|
-
i = end + 1;
|
|
445
|
-
} else if (c === ':') {
|
|
446
|
-
i++;
|
|
447
|
-
const element = src[i] === ':';
|
|
448
|
-
if (element) i++;
|
|
449
|
-
if (!startsIdent(src, i)) throw syntax(src, `unexpected ':'`);
|
|
450
|
-
const t = scanIdent(src, i);
|
|
451
|
-
const name = t.value.toLowerCase();
|
|
452
|
-
i = t.end;
|
|
453
|
-
let arg = null;
|
|
454
|
-
if (src[i] === '(') {
|
|
455
|
-
const { inner, end } = scanBalanced(src, i + 1, '(', ')');
|
|
456
|
-
arg = inner;
|
|
457
|
-
i = end < n ? end + 1 : n; // an unterminated argument closes at the end (CSS)
|
|
458
|
-
}
|
|
459
|
-
const legacy = !element && LEGACY_PSEUDO_ELEMENTS.has(name);
|
|
460
|
-
tokens.push({ k: element || legacy ? 'pelem' : 'pseudo', name, arg, legacy });
|
|
461
|
-
} else {
|
|
462
|
-
throw new SyntaxError(`unexpected '${c}' in selector`);
|
|
463
|
-
}
|
|
503
|
+
else if (c === '*' || c === '|' || startsIdent(src, i)) i = scanTypeToken(src, i, tokens);
|
|
504
|
+
else if (c === '#' || c === '.') i = scanHashOrClassToken(src, i, tokens);
|
|
505
|
+
else if (c === '[') i = scanAttrToken(src, i, tokens);
|
|
506
|
+
else if (c === ':') i = scanPseudoToken(src, i, tokens);
|
|
507
|
+
else throw new SyntaxError(`unexpected '${c}' in selector`);
|
|
464
508
|
}
|
|
465
509
|
return tokens;
|
|
466
510
|
}
|
|
@@ -479,45 +523,46 @@ function parsePseudo(tok, src, opts) {
|
|
|
479
523
|
const { inHas, inCompound = false } = opts;
|
|
480
524
|
const { name, arg } = tok;
|
|
481
525
|
const p = { name, arg };
|
|
482
|
-
|
|
483
|
-
const functional = arg !== null;
|
|
484
|
-
if (PLAIN.has(name)) {
|
|
485
|
-
if (functional) throw syntax(src, `:${name}() takes no argument`);
|
|
486
|
-
} else if (NEVER.has(name) && !functional) {
|
|
487
|
-
parsed = { never: true };
|
|
488
|
-
} else if (NEVER_FUNCTIONAL.has(name) && functional) {
|
|
489
|
-
parsed = { never: true };
|
|
490
|
-
} else if (!functional) {
|
|
491
|
-
throw syntax(src, `unknown pseudo-class :${name}`);
|
|
492
|
-
} else if (name === 'not') {
|
|
493
|
-
parsed = { list: parseList(arg, { nested: true, forgiving: false, inHas, compound: inCompound, inCompound }) };
|
|
494
|
-
} else if (name === 'is' || name === 'where') {
|
|
495
|
-
parsed = { list: parseList(arg, { nested: true, forgiving: true, inHas, compound: inCompound, inCompound }) };
|
|
496
|
-
} else if (name === '-webkit-any') {
|
|
497
|
-
// unforgiving compound selectors, no :has() (a nested :is() drops items with one)
|
|
498
|
-
parsed = { list: parseList(arg, { nested: true, forgiving: false, inHas: true, compound: true, inCompound: true }) };
|
|
499
|
-
} else if (name === 'has') {
|
|
500
|
-
if (inHas) throw syntax(src, ':has() inside :has()');
|
|
501
|
-
parsed = { rel: parseRelative(arg, src) };
|
|
502
|
-
} else if (name === 'nth-child' || name === 'nth-last-child' || name === 'nth-of-type' || name === 'nth-last-of-type') {
|
|
503
|
-
const at = name.endsWith('child') ? nthOfPosition(arg) : -1;
|
|
504
|
-
const anb = parseAnB(at === -1 ? arg : arg.slice(0, at));
|
|
505
|
-
if (anb === null) throw syntax(src, `:${name}() argument`);
|
|
506
|
-
parsed = { a: anb.a, b: anb.b, of: at === -1 ? null : parseList(arg.slice(at + 2), { nested: true, forgiving: false, inHas, inCompound }) };
|
|
507
|
-
} else if (name === 'lang' || name === 'dir') {
|
|
508
|
-
const v = identValue(trimWs(arg));
|
|
509
|
-
if (v === null) throw syntax(src, `:${name}() takes one identifier`);
|
|
510
|
-
parsed = { v: v.toLowerCase() };
|
|
511
|
-
} else if (name === 'state') {
|
|
512
|
-
const v = identValue(trimWs(arg));
|
|
513
|
-
if (v === null) throw syntax(src, ':state() takes one identifier');
|
|
514
|
-
parsed = { v };
|
|
515
|
-
} else {
|
|
516
|
-
throw syntax(src, `unknown pseudo-class :${name}()`);
|
|
517
|
-
}
|
|
526
|
+
const parsed = arg === null ? parsePlainPseudo(name, src) : parseFunctionalPseudo(name, arg, src, inHas, inCompound);
|
|
518
527
|
if (parsed) Object.defineProperty(p, 'parsed', { value: parsed });
|
|
519
528
|
return p;
|
|
520
529
|
}
|
|
530
|
+
// An argument-less pseudo-class: its `parsed` (null for an implemented one).
|
|
531
|
+
function parsePlainPseudo(name, src) {
|
|
532
|
+
if (PLAIN.has(name)) return null;
|
|
533
|
+
if (NEVER.has(name)) return { never: true };
|
|
534
|
+
throw syntax(src, `unknown pseudo-class :${name}`);
|
|
535
|
+
}
|
|
536
|
+
const NTH = new Set(['nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type']);
|
|
537
|
+
// A functional pseudo-class: its `parsed`.
|
|
538
|
+
function parseFunctionalPseudo(name, arg, src, inHas, inCompound) {
|
|
539
|
+
if (PLAIN.has(name)) throw syntax(src, `:${name}() takes no argument`);
|
|
540
|
+
if (NEVER_FUNCTIONAL.has(name)) return { never: true };
|
|
541
|
+
if (name === 'not') return { list: parseList(arg, { nested: true, forgiving: false, inHas, compound: inCompound, inCompound }) };
|
|
542
|
+
if (name === 'is' || name === 'where') return { list: parseList(arg, { nested: true, forgiving: true, inHas, compound: inCompound, inCompound }) };
|
|
543
|
+
// unforgiving compound selectors, no :has() (a nested :is() drops items with one)
|
|
544
|
+
if (name === '-webkit-any') return { list: parseList(arg, { nested: true, forgiving: false, inHas: true, compound: true, inCompound: true }) };
|
|
545
|
+
if (name === 'has') {
|
|
546
|
+
if (inHas) throw syntax(src, ':has() inside :has()');
|
|
547
|
+
return { rel: parseRelative(arg, src) };
|
|
548
|
+
}
|
|
549
|
+
if (NTH.has(name)) return parseNthPseudo(name, arg, src, inHas, inCompound);
|
|
550
|
+
if (name === 'lang' || name === 'dir') return { v: identArg(name, arg, src).toLowerCase() };
|
|
551
|
+
if (name === 'state') return { v: identArg(name, arg, src) };
|
|
552
|
+
throw syntax(src, `unknown pseudo-class :${name}()`);
|
|
553
|
+
}
|
|
554
|
+
// The one identifier argument of `:name()` (throws otherwise).
|
|
555
|
+
function identArg(name, arg, src) {
|
|
556
|
+
const v = identValue(trimWs(arg));
|
|
557
|
+
if (v === null) throw syntax(src, `:${name}() takes one identifier`);
|
|
558
|
+
return v;
|
|
559
|
+
}
|
|
560
|
+
function parseNthPseudo(name, arg, src, inHas, inCompound) {
|
|
561
|
+
const at = name.endsWith('child') ? nthOfPosition(arg) : -1;
|
|
562
|
+
const anb = parseAnB(at === -1 ? arg : arg.slice(0, at));
|
|
563
|
+
if (anb === null) throw syntax(src, `:${name}() argument`);
|
|
564
|
+
return { a: anb.a, b: anb.b, of: at === -1 ? null : parseList(arg.slice(at + 2), { nested: true, forgiving: false, inHas, inCompound }) };
|
|
565
|
+
}
|
|
521
566
|
|
|
522
567
|
// A view-transition pseudo-element argument (<pt-name-and-class-selector>): `*` or a name,
|
|
523
568
|
// then `.class` parts (no space after the dot), or only class parts; whitespace may
|
|
@@ -592,10 +637,75 @@ function parsePseudoElement(tok, src) {
|
|
|
592
637
|
// Parse tokens[lo..hi) (one complex selector, commas already split out). Throws on an empty
|
|
593
638
|
// item, a dangling combinator, a type selector after another simple selector, and a
|
|
594
639
|
// pseudo-element anywhere but at the end of the last compound.
|
|
640
|
+
// The last pseudo-element of `pe`'s chain (`pe` itself without sub-pseudo-elements).
|
|
641
|
+
function lastPseudoElement(pe) {
|
|
642
|
+
return pe.chain ? pe.chain[pe.chain.length - 1] : pe;
|
|
643
|
+
}
|
|
644
|
+
// A pseudo-element token `p` of `compound`: the compound's pseudo-element, or a
|
|
645
|
+
// sub-pseudo-element (`::slotted(li)::marker`: valid, never matched here) of it.
|
|
646
|
+
function addPseudoElement(compound, p, src, opts) {
|
|
647
|
+
if (opts.nested) throw syntax(src, 'pseudo-element inside a pseudo-class argument');
|
|
648
|
+
const next = parsePseudoElement(p, src);
|
|
649
|
+
const pe = compound.pe;
|
|
650
|
+
if (pe === null) { compound.pe = next; return; }
|
|
651
|
+
const last = lastPseudoElement(pe);
|
|
652
|
+
if (!allowedSub(last.name, next.name)) throw syntax(src, `::${next.name} after ::${last.name}`);
|
|
653
|
+
if (!pe.chain) Object.defineProperty(pe, 'chain', { value: [] });
|
|
654
|
+
pe.chain.push(next);
|
|
655
|
+
}
|
|
656
|
+
// A simple-selector token `p` after the compound's pseudo-element: only the pseudo-classes
|
|
657
|
+
// Chrome allows there (the selector never matches an element either way) and further
|
|
658
|
+
// pseudo-elements.
|
|
659
|
+
function addAfterPseudoElement(compound, p, src, opts) {
|
|
660
|
+
if (p.k === 'pelem') { addPseudoElement(compound, p, src, opts); return; }
|
|
661
|
+
if (p.k !== 'pseudo') throw syntax(src, 'simple selector after a pseudo-element');
|
|
662
|
+
const last = lastPseudoElement(compound.pe);
|
|
663
|
+
if (!allowedAfter(last, p)) throw syntax(src, `:${p.name} after ::${last.name}`);
|
|
664
|
+
compound.pseudos.push({ name: p.name, arg: p.arg });
|
|
665
|
+
}
|
|
666
|
+
// Add simple-selector token `p` (the `parts`-th of its compound) to `compound`.
|
|
667
|
+
function addSimple(compound, p, parts, src, opts) {
|
|
668
|
+
if (compound.pe !== null) { addAfterPseudoElement(compound, p, src, opts); return; }
|
|
669
|
+
if (p.k === 'type') {
|
|
670
|
+
if (parts) throw syntax(src, 'type selector after another simple selector');
|
|
671
|
+
compound.tag = p.name === null ? '*' : p.name;
|
|
672
|
+
compound.ns = p.ns;
|
|
673
|
+
} else if (p.k === 'id') compound.id = p.v;
|
|
674
|
+
else if (p.k === 'class') compound.classes.push(p.v);
|
|
675
|
+
else if (p.k === 'attr') compound.attrs.push(p.attr);
|
|
676
|
+
else if (p.k === 'pelem') addPseudoElement(compound, p, src, opts);
|
|
677
|
+
else compound.pseudos.push(parsePseudo(p, src, opts));
|
|
678
|
+
}
|
|
679
|
+
// Fill `compound` from tokens[pos..end) up to the next whitespace / combinator; returns
|
|
680
|
+
// the index of that token (or `end`).
|
|
681
|
+
function parseCompound(tokens, pos, end, src, opts, compound) {
|
|
682
|
+
const start = pos;
|
|
683
|
+
for (; pos < end; pos++) {
|
|
684
|
+
const p = tokens[pos];
|
|
685
|
+
if (p.k === 'ws' || p.k === 'comb') break;
|
|
686
|
+
addSimple(compound, p, pos - start, src, opts);
|
|
687
|
+
}
|
|
688
|
+
return pos;
|
|
689
|
+
}
|
|
690
|
+
function firstNonWs(tokens, lo, hi) {
|
|
691
|
+
while (lo < hi && tokens[lo].k === 'ws') lo++;
|
|
692
|
+
return lo;
|
|
693
|
+
}
|
|
694
|
+
function endNonWs(tokens, lo, hi) {
|
|
695
|
+
while (hi > lo && tokens[hi - 1].k === 'ws') hi--;
|
|
696
|
+
return hi;
|
|
697
|
+
}
|
|
698
|
+
// Whitespace after a compound is the descendant combinator unless another follows.
|
|
699
|
+
function afterWhitespace(combinator, first) {
|
|
700
|
+
return combinator === null && first !== null ? ' ' : combinator;
|
|
701
|
+
}
|
|
702
|
+
// A pending combinator other than the descendant one has no compound after it.
|
|
703
|
+
function isDangling(combinator) {
|
|
704
|
+
return combinator !== null && combinator !== ' ';
|
|
705
|
+
}
|
|
595
706
|
function parseComplexTokens(tokens, lo, hi, src, opts) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
while (end > start && tokens[end - 1].k === 'ws') end--;
|
|
707
|
+
const start = firstNonWs(tokens, lo, hi);
|
|
708
|
+
const end = endNonWs(tokens, start, hi);
|
|
599
709
|
if (start >= end) throw syntax(src, 'empty selector');
|
|
600
710
|
let first = null;
|
|
601
711
|
const rest = [];
|
|
@@ -604,9 +714,9 @@ function parseComplexTokens(tokens, lo, hi, src, opts) {
|
|
|
604
714
|
let sawPe = false;
|
|
605
715
|
while (pos < end) {
|
|
606
716
|
const t = tokens[pos];
|
|
607
|
-
if (t.k === 'ws') { pos++;
|
|
717
|
+
if (t.k === 'ws') { pos++; combinator = afterWhitespace(combinator, first); continue; }
|
|
608
718
|
if (t.k === 'comb') {
|
|
609
|
-
if (first === null || (combinator
|
|
719
|
+
if (first === null || isDangling(combinator)) throw syntax(src, 'dangling combinator');
|
|
610
720
|
combinator = t.v;
|
|
611
721
|
pos++;
|
|
612
722
|
continue;
|
|
@@ -616,47 +726,13 @@ function parseComplexTokens(tokens, lo, hi, src, opts) {
|
|
|
616
726
|
// one object shape for every compound (monomorphic matching): `ns` is null / 'any' /
|
|
617
727
|
// 'none', `pe` the pseudo-element or null
|
|
618
728
|
const compound = { tag: null, ns: null, id: null, classes: [], attrs: [], pseudos: [], pe: null };
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
let last = null; // the last pseudo-element of the chain
|
|
622
|
-
for (; pos < end; pos++) {
|
|
623
|
-
const p = tokens[pos];
|
|
624
|
-
if (p.k === 'ws' || p.k === 'comb') break;
|
|
625
|
-
if (pe !== null && p.k !== 'pseudo' && p.k !== 'pelem') throw syntax(src, 'simple selector after a pseudo-element');
|
|
626
|
-
if (p.k === 'type') {
|
|
627
|
-
if (parts) throw syntax(src, 'type selector after another simple selector');
|
|
628
|
-
compound.tag = p.name === null ? '*' : p.name;
|
|
629
|
-
compound.ns = p.ns;
|
|
630
|
-
} else if (p.k === 'id') compound.id = p.v;
|
|
631
|
-
else if (p.k === 'class') compound.classes.push(p.v);
|
|
632
|
-
else if (p.k === 'attr') compound.attrs.push(p.attr);
|
|
633
|
-
else if (p.k === 'pelem') {
|
|
634
|
-
if (opts.nested) throw syntax(src, 'pseudo-element inside a pseudo-class argument');
|
|
635
|
-
const next = parsePseudoElement(p, src);
|
|
636
|
-
if (pe === null) pe = next;
|
|
637
|
-
else {
|
|
638
|
-
// a sub-pseudo-element (`::slotted(li)::marker`): valid, never matched here
|
|
639
|
-
if (!allowedSub(last.name, next.name)) throw syntax(src, `::${next.name} after ::${last.name}`);
|
|
640
|
-
if (!pe.chain) Object.defineProperty(pe, 'chain', { value: [] });
|
|
641
|
-
pe.chain.push(next);
|
|
642
|
-
}
|
|
643
|
-
last = next;
|
|
644
|
-
} else if (pe !== null) {
|
|
645
|
-
// after a pseudo-element only the pseudo-classes Chrome allows there (the selector
|
|
646
|
-
// never matches an element either way)
|
|
647
|
-
if (!allowedAfter(last, p)) throw syntax(src, `:${p.name} after ::${last.name}`);
|
|
648
|
-
compound.pseudos.push({ name: p.name, arg: p.arg });
|
|
649
|
-
} else {
|
|
650
|
-
compound.pseudos.push(parsePseudo(p, src, opts));
|
|
651
|
-
}
|
|
652
|
-
parts++;
|
|
653
|
-
}
|
|
654
|
-
if (pe !== null) { compound.pe = pe; sawPe = true; }
|
|
729
|
+
pos = parseCompound(tokens, pos, end, src, opts, compound);
|
|
730
|
+
if (compound.pe !== null) sawPe = true;
|
|
655
731
|
if (first === null) first = compound;
|
|
656
732
|
else rest.push({ combinator: combinator === null ? ' ' : combinator, compound });
|
|
657
733
|
combinator = null;
|
|
658
734
|
}
|
|
659
|
-
if (combinator
|
|
735
|
+
if (isDangling(combinator)) throw syntax(src, 'dangling combinator');
|
|
660
736
|
if (opts.compound && rest.length) throw syntax(src, 'combinator inside :-webkit-any()');
|
|
661
737
|
return { first, rest };
|
|
662
738
|
}
|
|
@@ -860,19 +936,25 @@ function disabledState(el) {
|
|
|
860
936
|
return actuallyDisabled(el);
|
|
861
937
|
case 'optgroup':
|
|
862
938
|
return boolAttr(el, 'disabled', 'disabled');
|
|
863
|
-
case 'option':
|
|
864
|
-
|
|
865
|
-
let p = el.parentNode;
|
|
866
|
-
if (p && p.nodeType === 1 && p.localName === 'optgroup') {
|
|
867
|
-
if (p.hasAttribute('disabled')) return true;
|
|
868
|
-
p = p.parentNode;
|
|
869
|
-
}
|
|
870
|
-
return !!(p && p.nodeType === 1 && p.localName === 'select' && actuallyDisabled(p));
|
|
871
|
-
}
|
|
939
|
+
case 'option':
|
|
940
|
+
return optionDisabled(el);
|
|
872
941
|
default:
|
|
873
942
|
return undefined;
|
|
874
943
|
}
|
|
875
944
|
}
|
|
945
|
+
function isElementNamed(n, localName) {
|
|
946
|
+
return !!n && n.nodeType === 1 && n.localName === localName;
|
|
947
|
+
}
|
|
948
|
+
// An option is disabled by its own attribute, its optgroup's or a disabled select.
|
|
949
|
+
function optionDisabled(el) {
|
|
950
|
+
if (el.hasAttribute('disabled')) return true;
|
|
951
|
+
let p = el.parentNode;
|
|
952
|
+
if (isElementNamed(p, 'optgroup')) {
|
|
953
|
+
if (p.hasAttribute('disabled')) return true;
|
|
954
|
+
p = p.parentNode;
|
|
955
|
+
}
|
|
956
|
+
return isElementNamed(p, 'select') && actuallyDisabled(p);
|
|
957
|
+
}
|
|
876
958
|
function isEditable(el) {
|
|
877
959
|
for (let n = el; n && n.nodeType === 1; n = n.parentNode) {
|
|
878
960
|
if (!isHtml(n)) continue;
|
|
@@ -1014,12 +1096,11 @@ function matchAttr(el, a) {
|
|
|
1014
1096
|
// The element `:scope` matches during the current match (null = stylesheet: `:root`).
|
|
1015
1097
|
let currentScope = null;
|
|
1016
1098
|
|
|
1099
|
+
// matchPseudo dispatches by family: tree-structural here, then the selector-argument and
|
|
1100
|
+
// language ones (matchLogicalPseudo), then element state (matchStatePseudo).
|
|
1017
1101
|
function matchPseudo(el, p) {
|
|
1018
1102
|
const parsed = p.parsed;
|
|
1019
1103
|
switch (p.name) {
|
|
1020
|
-
case 'not': return !parsed.list.some((cx) => matchComplex(el, cx));
|
|
1021
|
-
case 'is': case 'where': case '-webkit-any': return parsed.list.some((cx) => matchComplex(el, cx));
|
|
1022
|
-
case 'has': return parsed.rel.some((rel) => hasMatch(el, rel));
|
|
1023
1104
|
// O(1) sibling steps (a parentless element is its own only sibling)
|
|
1024
1105
|
case 'first-child': return previousElement(el) === null;
|
|
1025
1106
|
case 'last-child': return nextElement(el) === null;
|
|
@@ -1027,41 +1108,64 @@ function matchPseudo(el, p) {
|
|
|
1027
1108
|
case 'first-of-type': return position(el, true)[0] === 1;
|
|
1028
1109
|
case 'last-of-type': { const [i, n] = position(el, true); return i === n; }
|
|
1029
1110
|
case 'only-of-type': return position(el, true)[1] === 1;
|
|
1030
|
-
case 'nth-child':
|
|
1031
|
-
|
|
1032
|
-
if (parsed.of) {
|
|
1033
|
-
if (!parsed.of.some((cx) => matchComplex(el, cx))) return false;
|
|
1034
|
-
return nthMatch(parsed.a, parsed.b, positionOf(el, parsed.of, last));
|
|
1035
|
-
}
|
|
1036
|
-
const [i, n] = position(el, false);
|
|
1037
|
-
return nthMatch(parsed.a, parsed.b, last ? n - i + 1 : i);
|
|
1038
|
-
}
|
|
1111
|
+
case 'nth-child': return matchNthChild(el, parsed, false);
|
|
1112
|
+
case 'nth-last-child': return matchNthChild(el, parsed, true);
|
|
1039
1113
|
case 'nth-of-type': case 'nth-last-of-type': {
|
|
1040
1114
|
const [i, n] = position(el, true);
|
|
1041
1115
|
return nthMatch(parsed.a, parsed.b, p.name === 'nth-last-of-type' ? n - i + 1 : i);
|
|
1042
1116
|
}
|
|
1117
|
+
case 'scope': return currentScope === null ? isRootElement(el) : el === currentScope;
|
|
1118
|
+
default: return matchLogicalPseudo(el, p);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function isRootElement(el) {
|
|
1122
|
+
return !!el.parentNode && el.parentNode.nodeType === 9;
|
|
1123
|
+
}
|
|
1124
|
+
function matchNthChild(el, parsed, last) {
|
|
1125
|
+
if (parsed.of) {
|
|
1126
|
+
if (!parsed.of.some((cx) => matchComplex(el, cx))) return false;
|
|
1127
|
+
return nthMatch(parsed.a, parsed.b, positionOf(el, parsed.of, last));
|
|
1128
|
+
}
|
|
1129
|
+
const [i, n] = position(el, false);
|
|
1130
|
+
return nthMatch(parsed.a, parsed.b, last ? n - i + 1 : i);
|
|
1131
|
+
}
|
|
1132
|
+
function matchLogicalPseudo(el, p) {
|
|
1133
|
+
const parsed = p.parsed;
|
|
1134
|
+
switch (p.name) {
|
|
1135
|
+
case 'not': return !parsed.list.some((cx) => matchComplex(el, cx));
|
|
1136
|
+
case 'is': case 'where': case '-webkit-any': return parsed.list.some((cx) => matchComplex(el, cx));
|
|
1137
|
+
case 'has': return parsed.rel.some((rel) => hasMatch(el, rel));
|
|
1138
|
+
case 'link': case 'any-link': return isLink(el);
|
|
1139
|
+
case 'defined': return isDefinedElement(el);
|
|
1140
|
+
case 'lang': return langMatches(el, parsed.v);
|
|
1141
|
+
case 'dir': return (parsed.v === 'rtl' || parsed.v === 'ltr') && isRtl(el) === (parsed.v === 'rtl');
|
|
1142
|
+
default: return matchStatePseudo(el, p);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// element state; form state reads the live PROPERTY first (what React sets), then the attribute
|
|
1146
|
+
function matchStatePseudo(el, p) {
|
|
1147
|
+
switch (p.name) {
|
|
1043
1148
|
case 'empty': return isEmpty(el);
|
|
1044
|
-
case 'root': return
|
|
1045
|
-
case '
|
|
1046
|
-
// form state reads the live PROPERTY first (what React sets), then the attribute
|
|
1047
|
-
case 'checked':
|
|
1048
|
-
if (el.localName === 'option') return !!el.selected;
|
|
1049
|
-
return el.localName === 'input' && (inputType(el) === 'checkbox' || inputType(el) === 'radio') && boolAttr(el, 'checked', 'checked');
|
|
1149
|
+
case 'root': return isRootElement(el);
|
|
1150
|
+
case 'checked': return isChecked(el);
|
|
1050
1151
|
case 'disabled': return disabledState(el) === true;
|
|
1051
1152
|
case 'enabled': return disabledState(el) === false;
|
|
1052
1153
|
case 'required': return boolAttr(el, 'required', 'required');
|
|
1053
|
-
case 'optional': return (el
|
|
1154
|
+
case 'optional': return isOptional(el);
|
|
1054
1155
|
case 'read-only': return isHtml(el) && !readWrite(el);
|
|
1055
1156
|
case 'read-write': return isHtml(el) && readWrite(el);
|
|
1056
|
-
case 'link': case 'any-link': return isLink(el);
|
|
1057
|
-
case 'defined': return isDefinedElement(el);
|
|
1058
|
-
case 'lang': return langMatches(el, parsed.v);
|
|
1059
|
-
case 'dir': return (parsed.v === 'rtl' || parsed.v === 'ltr') && isRtl(el) === (parsed.v === 'rtl');
|
|
1060
1157
|
// `:state()` parses; this runtime has no ElementInternals custom states, so it never
|
|
1061
1158
|
// matches (falls through to the never-matching default)
|
|
1062
1159
|
default: return false; // the valid never-matching pseudo-classes (:hover, :focus, …)
|
|
1063
1160
|
}
|
|
1064
1161
|
}
|
|
1162
|
+
function isChecked(el) {
|
|
1163
|
+
if (el.localName === 'option') return !!el.selected;
|
|
1164
|
+
return el.localName === 'input' && (inputType(el) === 'checkbox' || inputType(el) === 'radio') && boolAttr(el, 'checked', 'checked');
|
|
1165
|
+
}
|
|
1166
|
+
function isOptional(el) {
|
|
1167
|
+
return (el.localName === 'input' || el.localName === 'select' || el.localName === 'textarea') && !boolAttr(el, 'required', 'required');
|
|
1168
|
+
}
|
|
1065
1169
|
|
|
1066
1170
|
// allocation-free "does the class attribute contain this whole-word class"
|
|
1067
1171
|
function hasClass(cn, cls) {
|
|
@@ -1077,24 +1181,29 @@ function hasClass(cn, cls) {
|
|
|
1077
1181
|
return false;
|
|
1078
1182
|
}
|
|
1079
1183
|
|
|
1184
|
+
// The type selector `tag` (null = none, '*' = universal) against `el`.
|
|
1185
|
+
function tagMatches(el, tag) {
|
|
1186
|
+
if (tag === null || tag === '*') return true;
|
|
1187
|
+
const ln = el.localName;
|
|
1188
|
+
// type selectors compare ASCII case-insensitively (as the Rust engine); HTML local
|
|
1189
|
+
// names are already lowercase, so only foreign elements pay for the fold
|
|
1190
|
+
return ln === tag || (!!el.__ns && ln.toLowerCase() === tag);
|
|
1191
|
+
}
|
|
1192
|
+
// classes: read the class attribute ONCE and test membership without allocating
|
|
1193
|
+
function classesMatch(el, classes) {
|
|
1194
|
+
const cn = plainAttr(el, 'class');
|
|
1195
|
+
if (!cn) return false;
|
|
1196
|
+
for (let i = 0; i < classes.length; i++) if (!hasClass(cn, classes[i])) return false;
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1080
1200
|
function matchCompound(el, compound) {
|
|
1081
1201
|
if (!el || el.nodeType !== 1) return false;
|
|
1082
1202
|
if (compound.pe) return false; // a pseudo-element is never an element
|
|
1083
1203
|
if (compound.ns === 'none' && el.namespaceURI !== null) return false;
|
|
1084
|
-
|
|
1085
|
-
if (tag !== null && tag !== '*') {
|
|
1086
|
-
const ln = el.localName;
|
|
1087
|
-
// type selectors compare ASCII case-insensitively (as the Rust engine); HTML local
|
|
1088
|
-
// names are already lowercase, so only foreign elements pay for the fold
|
|
1089
|
-
if (ln !== tag && (!el.__ns || ln.toLowerCase() !== tag)) return false;
|
|
1090
|
-
}
|
|
1204
|
+
if (!tagMatches(el, compound.tag)) return false;
|
|
1091
1205
|
if (compound.id !== null && plainAttr(el, 'id') !== compound.id) return false;
|
|
1092
|
-
|
|
1093
|
-
if (compound.classes.length) {
|
|
1094
|
-
const cn = plainAttr(el, 'class');
|
|
1095
|
-
if (!cn) return false;
|
|
1096
|
-
for (const cls of compound.classes) if (!hasClass(cn, cls)) return false;
|
|
1097
|
-
}
|
|
1206
|
+
if (compound.classes.length && !classesMatch(el, compound.classes)) return false;
|
|
1098
1207
|
for (const a of compound.attrs) if (!matchAttr(el, a)) return false;
|
|
1099
1208
|
for (const p of compound.pseudos) if (!matchPseudo(el, p)) return false;
|
|
1100
1209
|
return true;
|
|
@@ -1113,7 +1222,8 @@ function matchComplex(el, cx) {
|
|
|
1113
1222
|
// Match compounds 0..=k of `cx` ending at `el`, recursing leftward. Descendant (' ') and
|
|
1114
1223
|
// general-sibling ('~') combinators try EVERY candidate ancestor/sibling and backtrack.
|
|
1115
1224
|
// `scope` supports `:has()`: `{ comb, node }` anchors the leftmost compound to the subject.
|
|
1116
|
-
|
|
1225
|
+
// (Every caller passes `scope`: null outside `:has()`.)
|
|
1226
|
+
function matchChain(el, cx, k, scope) {
|
|
1117
1227
|
if (!matchCompound(el, compoundAt(cx, k))) return false;
|
|
1118
1228
|
if (k === 0) return scope === null ? true : relatesToScope(el, scope.comb, scope.node);
|
|
1119
1229
|
const comb = cx.rest[k - 1].combinator;
|
|
@@ -1125,14 +1235,20 @@ function matchChain(el, cx, k, scope = null) {
|
|
|
1125
1235
|
const prev = previousElement(el);
|
|
1126
1236
|
return !!prev && matchChain(prev, cx, k - 1, scope);
|
|
1127
1237
|
}
|
|
1128
|
-
if (comb === ' ')
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1238
|
+
if (comb === ' ') return someAncestorMatches(el, cx, k - 1, scope);
|
|
1239
|
+
return somePrecedingSiblingMatches(el, cx, k - 1, scope);
|
|
1240
|
+
}
|
|
1241
|
+
// The descendant combinator: compounds 0..=k end at SOME element ancestor of `el`.
|
|
1242
|
+
function someAncestorMatches(el, cx, k, scope) {
|
|
1243
|
+
for (let anc = el.parentNode; anc && anc.nodeType === 1; anc = anc.parentNode) {
|
|
1244
|
+
if (matchChain(anc, cx, k, scope)) return true;
|
|
1133
1245
|
}
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
// The general-sibling combinator: compounds 0..=k end at SOME preceding element sibling.
|
|
1249
|
+
function somePrecedingSiblingMatches(el, cx, k, scope) {
|
|
1134
1250
|
for (let prev = previousElement(el); prev; prev = previousElement(prev)) {
|
|
1135
|
-
if (matchChain(prev, cx, k
|
|
1251
|
+
if (matchChain(prev, cx, k, scope)) return true;
|
|
1136
1252
|
}
|
|
1137
1253
|
return false;
|
|
1138
1254
|
}
|