@reteps/tree-sitter-htmlmustache 0.9.0 → 0.9.1
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/browser/out/browser/index.mjs +201 -67
- package/browser/out/browser/index.mjs.map +3 -3
- package/browser/out/core/selectorMatcher.d.ts +19 -6
- package/browser/out/core/selectorMatcher.d.ts.map +1 -1
- package/cli/out/main.js +200 -66
- package/package.json +1 -1
- package/src/core/selectorMatcher.ts +272 -72
|
@@ -10,14 +10,21 @@
|
|
|
10
10
|
* Supported user-facing syntax:
|
|
11
11
|
* - Tag names (`div`), universal (`*`), classes (`.foo`), ids (`#foo`)
|
|
12
12
|
* - Attributes: `[attr]`, `[attr=v]`, `[attr^=v]`, `[attr*=v]`, `[attr$=v]`, `[attr~=v]`
|
|
13
|
-
* - Descendant (space)
|
|
13
|
+
* - Descendant (space), child (`>`), adjacent-sibling (`+`), and
|
|
14
|
+
* general-sibling (`~`) combinators. Sibling combinators skip over text /
|
|
15
|
+
* whitespace nodes (CSS semantics) and work across HTML and Mustache
|
|
16
|
+
* constructs: e.g. `label + input`, `{{foo}} + p`, `h2 ~ {{#items}}`.
|
|
14
17
|
* - Mustache variables: `{{path}}` and `{{{path}}}` (raw)
|
|
15
18
|
* - Mustache sections: `{{#name}}` and `{{^name}}` (inverted)
|
|
16
19
|
* - Mustache comments: `{{!content}}`
|
|
17
20
|
* - Mustache partials: `{{>name}}`
|
|
18
21
|
* - Glob wildcard `*` inside the argument: `{{options.*}}`, `{{*.deprecated}}`, `{{*}}`
|
|
19
22
|
* - `:has(selector)` — element has a matching descendant
|
|
20
|
-
* - `:not(...)`
|
|
23
|
+
* - `:not(...)` — negation. Accepts attributes/class/id/`:has` (folded into
|
|
24
|
+
* the outer compound), plus any other selector (including Mustache
|
|
25
|
+
* literals and type selectors) as a whole-selector check against the
|
|
26
|
+
* node itself. Example: `{{*}}:not({{internal.*}})` matches any
|
|
27
|
+
* interpolation whose path does not start with `internal.`.
|
|
21
28
|
* - `:root` — the tree-sitter fragment root (the whole document). Unlike
|
|
22
29
|
* browser CSS where `:root` matches `<html>`, this matches the parse-tree
|
|
23
30
|
* root so it works on partials/fragments too. Useful as a document-scoped
|
|
@@ -26,15 +33,19 @@
|
|
|
26
33
|
* `pl-answer-panel` is. Cannot combine with tag/class/id/attribute in the
|
|
27
34
|
* same compound (only with `:has` / `:not(:has(...))`). Inside `:has(...)`,
|
|
28
35
|
* `:root` refers to the element being checked, not the document.
|
|
36
|
+
* - `:is(a, b, ...)` — matches if any alternative matches. Expanded at parse
|
|
37
|
+
* time into the Cartesian product of alternatives, so `:is(a, b) :is(c, d)`
|
|
38
|
+
* is equivalent to `a c, a d, b c, b d`. Alternatives inside `:is` that
|
|
39
|
+
* contain combinators are only allowed when the `:is(...)` stands alone in
|
|
40
|
+
* its compound (e.g. `:is(div > span, p)` works; `x:is(div > span, p)`
|
|
41
|
+
* does not, since a combinator can't be merged into another compound).
|
|
29
42
|
* - Comma-separated alternatives
|
|
30
43
|
*
|
|
31
44
|
* Unsupported (parseSelector returns null, rule is skipped):
|
|
32
|
-
* - Sibling combinators (`+`, `~`)
|
|
33
45
|
* - `[attr|=v]`, case-insensitive `i` flag
|
|
34
46
|
* - Mixed HTML + Mustache kinds in one compound (e.g. `img{{foo}}`)
|
|
35
47
|
* - `{{/end}}` (end tags aren't standalone nodes)
|
|
36
48
|
* - `{{=<% %>=}}` (delimiter changes aren't grammar-tracked)
|
|
37
|
-
* - Mustache literals inside `:not(...)` (only attribute/class/id/:has)
|
|
38
49
|
*/
|
|
39
50
|
|
|
40
51
|
import { parse as parselParse, type AST, type Token, type AttributeToken, type ClassToken, type IdToken } from 'parsel-js';
|
|
@@ -73,6 +84,8 @@ export interface DescendantCheck {
|
|
|
73
84
|
negated: boolean; // true for :not(:has(...))
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
export type Combinator = 'descendant' | 'child' | 'adjacent-sibling' | 'general-sibling';
|
|
88
|
+
|
|
76
89
|
export interface Segment {
|
|
77
90
|
kind: SegmentKind;
|
|
78
91
|
rootOnly: boolean; // true for `:root` — matches only the tree-sitter fragment root
|
|
@@ -80,7 +93,8 @@ export interface Segment {
|
|
|
80
93
|
pathRegex?: RegExp; // compiled glob when `name` contains `*`
|
|
81
94
|
attributes: AttributeConstraint[];
|
|
82
95
|
descendantChecks: DescendantCheck[];
|
|
83
|
-
|
|
96
|
+
selfNegations: ParsedSelector[]; // :not(X) where X is a full sub-selector tested against the node itself
|
|
97
|
+
combinator: Combinator;
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
/** A parsed selector is a list of alternatives (from comma-separated parts). */
|
|
@@ -208,17 +222,101 @@ export function parseSelector(raw: string): ParsedSelector | null {
|
|
|
208
222
|
const tops = ast.type === 'list' ? ast.list : [ast];
|
|
209
223
|
const alts: Segment[][] = [];
|
|
210
224
|
for (const top of tops) {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
225
|
+
const expanded = expandIs(top);
|
|
226
|
+
if (expanded === null) return null;
|
|
227
|
+
for (const exp of expanded) {
|
|
228
|
+
const segments: Segment[] = [];
|
|
229
|
+
if (!collectSegments(exp, 'descendant', segments)) return null;
|
|
230
|
+
if (segments.length === 0) return null;
|
|
231
|
+
alts.push(segments);
|
|
232
|
+
}
|
|
215
233
|
}
|
|
216
234
|
return alts.length > 0 ? alts : null;
|
|
217
235
|
}
|
|
218
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Expand `:is(...)` pseudo-classes into explicit alternatives. Returns an
|
|
239
|
+
* array of equivalent ASTs with every `:is` removed (Cartesian product over
|
|
240
|
+
* alternatives), or `null` if the expansion contains an unsupported shape
|
|
241
|
+
* (e.g. complex alternatives inside a mixed compound).
|
|
242
|
+
*/
|
|
243
|
+
function expandIs(ast: AST): AST[] | null {
|
|
244
|
+
switch (ast.type) {
|
|
245
|
+
case 'list': {
|
|
246
|
+
const out: AST[] = [];
|
|
247
|
+
for (const alt of ast.list) {
|
|
248
|
+
const expanded = expandIs(alt);
|
|
249
|
+
if (expanded === null) return null;
|
|
250
|
+
out.push(...expanded);
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
case 'complex': {
|
|
255
|
+
const lefts = expandIs(ast.left);
|
|
256
|
+
if (lefts === null) return null;
|
|
257
|
+
const rights = expandIs(ast.right);
|
|
258
|
+
if (rights === null) return null;
|
|
259
|
+
const out: AST[] = [];
|
|
260
|
+
for (const l of lefts) for (const r of rights) {
|
|
261
|
+
out.push({ ...ast, left: l, right: r });
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
case 'compound': {
|
|
266
|
+
// A compound whose sole token is `:is(...)` can be replaced by any
|
|
267
|
+
// shape (including complex alternatives). Mixed compounds require each
|
|
268
|
+
// alternative to be mergeable token-by-token.
|
|
269
|
+
if (ast.list.length === 1) {
|
|
270
|
+
const tok = ast.list[0];
|
|
271
|
+
if (tok.type === 'pseudo-class' && tok.name === 'is') {
|
|
272
|
+
if (!tok.subtree) return null;
|
|
273
|
+
return expandIs(tok.subtree);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return expandCompoundWithIs(ast.list);
|
|
277
|
+
}
|
|
278
|
+
default:
|
|
279
|
+
if (ast.type === 'pseudo-class' && ast.name === 'is') {
|
|
280
|
+
if (!ast.subtree) return null;
|
|
281
|
+
return expandIs(ast.subtree);
|
|
282
|
+
}
|
|
283
|
+
return [ast];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function expandCompoundWithIs(tokens: Token[]): AST[] | null {
|
|
288
|
+
let variants: Token[][] = [[]];
|
|
289
|
+
for (const tok of tokens) {
|
|
290
|
+
if (tok.type === 'pseudo-class' && tok.name === 'is') {
|
|
291
|
+
if (!tok.subtree) return null;
|
|
292
|
+
const alts = expandIs(tok.subtree);
|
|
293
|
+
if (alts === null) return null;
|
|
294
|
+
const next: Token[][] = [];
|
|
295
|
+
for (const base of variants) {
|
|
296
|
+
for (const alt of alts) {
|
|
297
|
+
if (alt.type === 'compound') {
|
|
298
|
+
next.push([...base, ...alt.list]);
|
|
299
|
+
} else if (alt.type === 'complex' || alt.type === 'list' || alt.type === 'relative') {
|
|
300
|
+
// Can't splice a combinator-bearing selector into a compound.
|
|
301
|
+
return null;
|
|
302
|
+
} else {
|
|
303
|
+
next.push([...base, alt]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
variants = next;
|
|
308
|
+
} else {
|
|
309
|
+
variants = variants.map(v => [...v, tok]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return variants.map(list =>
|
|
313
|
+
list.length === 1 ? (list[0] as AST) : ({ type: 'compound', list } as AST),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
219
317
|
function collectSegments(
|
|
220
318
|
ast: AST,
|
|
221
|
-
combinator:
|
|
319
|
+
combinator: Combinator,
|
|
222
320
|
out: Segment[],
|
|
223
321
|
): boolean {
|
|
224
322
|
if (ast.type === 'complex') {
|
|
@@ -236,10 +334,12 @@ function collectSegments(
|
|
|
236
334
|
return true;
|
|
237
335
|
}
|
|
238
336
|
|
|
239
|
-
function mapCombinator(c: string):
|
|
337
|
+
function mapCombinator(c: string): Combinator | null {
|
|
240
338
|
const trimmed = c.trim();
|
|
241
339
|
if (trimmed === '') return 'descendant';
|
|
242
340
|
if (trimmed === '>') return 'child';
|
|
341
|
+
if (trimmed === '+') return 'adjacent-sibling';
|
|
342
|
+
if (trimmed === '~') return 'general-sibling';
|
|
243
343
|
return null;
|
|
244
344
|
}
|
|
245
345
|
|
|
@@ -252,6 +352,7 @@ function segmentFromCompound(ast: AST): Segment | null {
|
|
|
252
352
|
let rootOnly = false;
|
|
253
353
|
const attributes: AttributeConstraint[] = [];
|
|
254
354
|
const descendantChecks: DescendantCheck[] = [];
|
|
355
|
+
const selfNegations: ParsedSelector[] = [];
|
|
255
356
|
|
|
256
357
|
// Once a Mustache kind is picked, no other kind tokens may appear.
|
|
257
358
|
const forbidChange = (requested: SegmentKind): boolean => {
|
|
@@ -310,7 +411,7 @@ function segmentFromCompound(ast: AST): Segment | null {
|
|
|
310
411
|
break;
|
|
311
412
|
}
|
|
312
413
|
if (token.name === 'not') {
|
|
313
|
-
if (!applyNegatedSubtree(token.subtree, attributes, descendantChecks)) return null;
|
|
414
|
+
if (!applyNegatedSubtree(token.subtree, attributes, descendantChecks, selfNegations)) return null;
|
|
314
415
|
break;
|
|
315
416
|
}
|
|
316
417
|
if (token.name === 'root') {
|
|
@@ -338,7 +439,7 @@ function segmentFromCompound(ast: AST): Segment | null {
|
|
|
338
439
|
|
|
339
440
|
const isHtml = kind === 'html';
|
|
340
441
|
const finalAttrs = isHtml ? attributes : [];
|
|
341
|
-
return { kind, rootOnly, name, pathRegex, attributes: finalAttrs, descendantChecks, combinator: 'descendant' };
|
|
442
|
+
return { kind, rootOnly, name, pathRegex, attributes: finalAttrs, descendantChecks, selfNegations, combinator: 'descendant' };
|
|
342
443
|
}
|
|
343
444
|
|
|
344
445
|
function mustacheKindFromMarker(name: string): SegmentKind | null {
|
|
@@ -401,6 +502,7 @@ function applyNegatedSubtree(
|
|
|
401
502
|
subtree: AST | undefined,
|
|
402
503
|
attributes: AttributeConstraint[],
|
|
403
504
|
descendantChecks: DescendantCheck[],
|
|
505
|
+
selfNegations: ParsedSelector[],
|
|
404
506
|
): boolean {
|
|
405
507
|
if (!subtree) return false;
|
|
406
508
|
if (subtree.type === 'attribute') {
|
|
@@ -423,6 +525,13 @@ function applyNegatedSubtree(
|
|
|
423
525
|
descendantChecks.push({ selector: sel, negated: true });
|
|
424
526
|
return true;
|
|
425
527
|
}
|
|
528
|
+
// Fall back to a whole-selector negation: `:not(X)` where X is a Mustache
|
|
529
|
+
// literal or any other parseable selector, tested against the node itself.
|
|
530
|
+
const sel = subtreeToSelector(subtree);
|
|
531
|
+
if (sel) {
|
|
532
|
+
selfNegations.push(sel);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
426
535
|
return false;
|
|
427
536
|
}
|
|
428
537
|
|
|
@@ -455,6 +564,8 @@ interface AncestorEntry {
|
|
|
455
564
|
kind: AncestorKind;
|
|
456
565
|
name: string; // lowercased
|
|
457
566
|
node: BalanceNode;
|
|
567
|
+
siblings: BalanceNode[]; // the node's parent's children; empty for root
|
|
568
|
+
indexInSiblings: number; // 0 for root
|
|
458
569
|
}
|
|
459
570
|
|
|
460
571
|
type AncestorKind = 'html' | 'section' | 'inverted' | 'root';
|
|
@@ -534,12 +645,25 @@ function checkDescendants(node: BalanceNode, checks: DescendantCheck[]): boolean
|
|
|
534
645
|
}
|
|
535
646
|
|
|
536
647
|
function hasDescendantMatch(node: BalanceNode, selector: ParsedSelector): boolean {
|
|
537
|
-
for (
|
|
538
|
-
if (matchSelector(
|
|
648
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
649
|
+
if (matchSelector(node.children[i], selector, node.children, i).length > 0) return true;
|
|
539
650
|
}
|
|
540
651
|
return false;
|
|
541
652
|
}
|
|
542
653
|
|
|
654
|
+
function checkSelfNegations(node: BalanceNode, negations: ParsedSelector[], rootNode: BalanceNode): boolean {
|
|
655
|
+
for (const sel of negations) {
|
|
656
|
+
for (const alt of sel) {
|
|
657
|
+
// A selfNegation selector is a single-segment check against the node itself.
|
|
658
|
+
// Multi-segment alternatives (e.g. `:not(a b)`) aren't sensibly testable
|
|
659
|
+
// against a single node, so they're treated as never matching => pass.
|
|
660
|
+
if (alt.length !== 1) continue;
|
|
661
|
+
if (nodeMatchesSegment(node, alt[0], rootNode)) return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
|
|
543
667
|
function matchesName(actual: string | null, segment: Segment): boolean {
|
|
544
668
|
if (segment.name === null) return true; // wildcard
|
|
545
669
|
if (actual === null) return false;
|
|
@@ -550,59 +674,92 @@ function matchesName(actual: string | null, segment: Segment): boolean {
|
|
|
550
674
|
function nodeMatchesSegment(node: BalanceNode, segment: Segment, rootNode: BalanceNode): boolean {
|
|
551
675
|
if (segment.rootOnly) {
|
|
552
676
|
if (node !== rootNode) return false;
|
|
553
|
-
return checkDescendants(node, segment.descendantChecks)
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (
|
|
677
|
+
return checkDescendants(node, segment.descendantChecks)
|
|
678
|
+
&& checkSelfNegations(node, segment.selfNegations, rootNode);
|
|
679
|
+
}
|
|
680
|
+
const baseMatches = (() => {
|
|
681
|
+
switch (segment.kind) {
|
|
682
|
+
case 'html': {
|
|
683
|
+
if (!HTML_ELEMENT_TYPES.has(node.type)) return false;
|
|
684
|
+
if (segment.name !== null) {
|
|
685
|
+
const tagName = getTagName(node)?.toLowerCase();
|
|
686
|
+
if (tagName !== segment.name) return false;
|
|
687
|
+
}
|
|
688
|
+
return checkAttributes(node, segment.attributes) && checkDescendants(node, segment.descendantChecks);
|
|
561
689
|
}
|
|
562
|
-
|
|
690
|
+
case 'section':
|
|
691
|
+
if (node.type !== 'mustache_section') return false;
|
|
692
|
+
if (!matchesName(getSectionName(node)?.toLowerCase() ?? null, segment)) return false;
|
|
693
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
694
|
+
case 'inverted':
|
|
695
|
+
if (node.type !== 'mustache_inverted_section') return false;
|
|
696
|
+
if (!matchesName(getSectionName(node)?.toLowerCase() ?? null, segment)) return false;
|
|
697
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
698
|
+
case 'variable':
|
|
699
|
+
if (node.type !== 'mustache_interpolation') return false;
|
|
700
|
+
if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
|
|
701
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
702
|
+
case 'raw':
|
|
703
|
+
if (node.type !== 'mustache_triple') return false;
|
|
704
|
+
if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
|
|
705
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
706
|
+
case 'comment':
|
|
707
|
+
if (node.type !== 'mustache_comment') return false;
|
|
708
|
+
if (!matchesName(getCommentContent(node)?.toLowerCase() ?? null, segment)) return false;
|
|
709
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
710
|
+
case 'partial':
|
|
711
|
+
if (node.type !== 'mustache_partial') return false;
|
|
712
|
+
if (!matchesName(getPartialName(node)?.toLowerCase() ?? null, segment)) return false;
|
|
713
|
+
return checkDescendants(node, segment.descendantChecks);
|
|
563
714
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
|
|
579
|
-
return checkDescendants(node, segment.descendantChecks);
|
|
580
|
-
case 'comment':
|
|
581
|
-
if (node.type !== 'mustache_comment') return false;
|
|
582
|
-
if (!matchesName(getCommentContent(node)?.toLowerCase() ?? null, segment)) return false;
|
|
583
|
-
return checkDescendants(node, segment.descendantChecks);
|
|
584
|
-
case 'partial':
|
|
585
|
-
if (node.type !== 'mustache_partial') return false;
|
|
586
|
-
if (!matchesName(getPartialName(node)?.toLowerCase() ?? null, segment)) return false;
|
|
587
|
-
return checkDescendants(node, segment.descendantChecks);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/** Does the ancestor stack satisfy remaining segments? */
|
|
592
|
-
function checkAncestors(
|
|
593
|
-
ancestors: AncestorEntry[],
|
|
715
|
+
})();
|
|
716
|
+
if (!baseMatches) return false;
|
|
717
|
+
return checkSelfNegations(node, segment.selfNegations, rootNode);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
interface Cursor {
|
|
721
|
+
ancestors: AncestorEntry[]; // nodes on the path from root, excluding the match pointer itself
|
|
722
|
+
siblings: BalanceNode[]; // siblings of the current match pointer
|
|
723
|
+
indexInSiblings: number; // index of the current match pointer in its siblings
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Does the prefix of segments up to segIdx satisfy the path/sibling context? */
|
|
727
|
+
function checkPrefix(
|
|
728
|
+
cursor: Cursor,
|
|
594
729
|
segments: Segment[],
|
|
595
730
|
segIdx: number,
|
|
596
|
-
|
|
731
|
+
stepCombinator: Combinator,
|
|
732
|
+
rootNode: BalanceNode,
|
|
597
733
|
): boolean {
|
|
598
734
|
if (segIdx < 0) return true;
|
|
599
735
|
const segment = segments[segIdx];
|
|
736
|
+
|
|
737
|
+
if (stepCombinator === 'adjacent-sibling' || stepCombinator === 'general-sibling') {
|
|
738
|
+
for (let i = cursor.indexInSiblings - 1; i >= 0; i--) {
|
|
739
|
+
const sib = cursor.siblings[i];
|
|
740
|
+
if (!isMatchableNode(sib)) continue;
|
|
741
|
+
if (!nodeMatchesSegment(sib, segment, rootNode)) {
|
|
742
|
+
if (stepCombinator === 'adjacent-sibling') return false;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const newCursor: Cursor = {
|
|
746
|
+
ancestors: cursor.ancestors,
|
|
747
|
+
siblings: cursor.siblings,
|
|
748
|
+
indexInSiblings: i,
|
|
749
|
+
};
|
|
750
|
+
if (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
|
|
751
|
+
if (stepCombinator === 'adjacent-sibling') return false;
|
|
752
|
+
}
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// descendant or child — walk up the ancestor stack
|
|
600
757
|
const ancestorKind = ancestorKindForSegment(segment);
|
|
601
758
|
if (ancestorKind === null) return false; // variable/raw/comment/partial can't be ancestors
|
|
602
759
|
|
|
603
|
-
if (
|
|
604
|
-
for (let a = ancestors.length - 1; a >= 0; a--) {
|
|
605
|
-
const entry = ancestors[a];
|
|
760
|
+
if (stepCombinator === 'child') {
|
|
761
|
+
for (let a = cursor.ancestors.length - 1; a >= 0; a--) {
|
|
762
|
+
const entry = cursor.ancestors[a];
|
|
606
763
|
if (entry.kind !== ancestorKind) {
|
|
607
764
|
// For `:root > X` (ancestorKind='root'), any html ancestor between
|
|
608
765
|
// X and the document root breaks the direct-child relationship.
|
|
@@ -613,24 +770,45 @@ function checkAncestors(
|
|
|
613
770
|
if (!matchesName(entry.name, segment)) return false;
|
|
614
771
|
if (segment.kind === 'html' && !checkAttributes(entry.node, segment.attributes)) return false;
|
|
615
772
|
if (!checkDescendants(entry.node, segment.descendantChecks)) return false;
|
|
616
|
-
|
|
773
|
+
if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) return false;
|
|
774
|
+
const newCursor: Cursor = {
|
|
775
|
+
ancestors: cursor.ancestors.slice(0, a),
|
|
776
|
+
siblings: entry.siblings,
|
|
777
|
+
indexInSiblings: entry.indexInSiblings,
|
|
778
|
+
};
|
|
779
|
+
return checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode);
|
|
617
780
|
}
|
|
618
781
|
return false;
|
|
619
782
|
}
|
|
620
783
|
|
|
621
|
-
for (let a = ancestors.length - 1; a >= 0; a--) {
|
|
622
|
-
const entry = ancestors[a];
|
|
784
|
+
for (let a = cursor.ancestors.length - 1; a >= 0; a--) {
|
|
785
|
+
const entry = cursor.ancestors[a];
|
|
623
786
|
if (entry.kind !== ancestorKind) continue;
|
|
624
787
|
if (!matchesName(entry.name, segment)) continue;
|
|
625
788
|
if (segment.kind === 'html' && !checkAttributes(entry.node, segment.attributes)) continue;
|
|
626
789
|
if (!checkDescendants(entry.node, segment.descendantChecks)) continue;
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
|
|
790
|
+
if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) continue;
|
|
791
|
+
const newCursor: Cursor = {
|
|
792
|
+
ancestors: cursor.ancestors.slice(0, a),
|
|
793
|
+
siblings: entry.siblings,
|
|
794
|
+
indexInSiblings: entry.indexInSiblings,
|
|
795
|
+
};
|
|
796
|
+
if (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
|
|
630
797
|
}
|
|
631
798
|
return false;
|
|
632
799
|
}
|
|
633
800
|
|
|
801
|
+
/** True for nodes that a segment could ever match — used to skip text/whitespace when walking siblings. */
|
|
802
|
+
function isMatchableNode(node: BalanceNode): boolean {
|
|
803
|
+
return HTML_ELEMENT_TYPES.has(node.type)
|
|
804
|
+
|| node.type === 'mustache_section'
|
|
805
|
+
|| node.type === 'mustache_inverted_section'
|
|
806
|
+
|| node.type === 'mustache_interpolation'
|
|
807
|
+
|| node.type === 'mustache_triple'
|
|
808
|
+
|| node.type === 'mustache_comment'
|
|
809
|
+
|| node.type === 'mustache_partial';
|
|
810
|
+
}
|
|
811
|
+
|
|
634
812
|
function ancestorKindForSegment(segment: Segment): AncestorKind | null {
|
|
635
813
|
if (segment.rootOnly) return 'root';
|
|
636
814
|
if (segment.kind === 'html') return 'html';
|
|
@@ -669,15 +847,26 @@ function getReportNode(node: BalanceNode, rootNode?: BalanceNode): BalanceNode {
|
|
|
669
847
|
return node;
|
|
670
848
|
}
|
|
671
849
|
|
|
672
|
-
function matchAlternative(
|
|
850
|
+
function matchAlternative(
|
|
851
|
+
rootNode: BalanceNode,
|
|
852
|
+
segments: Segment[],
|
|
853
|
+
rootSiblings: BalanceNode[],
|
|
854
|
+
rootIndexInSiblings: number,
|
|
855
|
+
): BalanceNode[] {
|
|
673
856
|
const results: BalanceNode[] = [];
|
|
674
857
|
const lastSegment = segments[segments.length - 1];
|
|
675
858
|
|
|
676
|
-
function walk(
|
|
859
|
+
function walk(
|
|
860
|
+
node: BalanceNode,
|
|
861
|
+
ancestors: AncestorEntry[],
|
|
862
|
+
siblings: BalanceNode[],
|
|
863
|
+
indexInSiblings: number,
|
|
864
|
+
) {
|
|
677
865
|
if (nodeMatchesSegment(node, lastSegment, rootNode)) {
|
|
866
|
+
const cursor: Cursor = { ancestors, siblings, indexInSiblings };
|
|
678
867
|
if (
|
|
679
868
|
segments.length === 1 ||
|
|
680
|
-
|
|
869
|
+
checkPrefix(cursor, segments, segments.length - 2, lastSegment.combinator, rootNode)
|
|
681
870
|
) {
|
|
682
871
|
results.push(getReportNode(node, rootNode));
|
|
683
872
|
}
|
|
@@ -690,25 +879,36 @@ function matchAlternative(rootNode: BalanceNode, segments: Segment[]): BalanceNo
|
|
|
690
879
|
ancestorKind === 'html' ? getTagName(node)?.toLowerCase() :
|
|
691
880
|
getSectionName(node)?.toLowerCase();
|
|
692
881
|
if (name) {
|
|
693
|
-
newAncestors = [...ancestors, { kind: ancestorKind, name, node }];
|
|
882
|
+
newAncestors = [...ancestors, { kind: ancestorKind, name, node, siblings, indexInSiblings }];
|
|
694
883
|
}
|
|
695
884
|
}
|
|
696
885
|
|
|
697
|
-
for (
|
|
886
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
887
|
+
walk(node.children[i], newAncestors, node.children, i);
|
|
888
|
+
}
|
|
698
889
|
}
|
|
699
890
|
|
|
700
891
|
// Seed the ancestor stack with a root entry so `:root X` / `:root > X`
|
|
701
892
|
// can find the document root as an ancestor. The root node itself is
|
|
702
893
|
// never an html/section/inverted node, so it's otherwise never pushed.
|
|
703
|
-
|
|
894
|
+
const rootEntry: AncestorEntry = {
|
|
895
|
+
kind: 'root', name: '', node: rootNode,
|
|
896
|
+
siblings: rootSiblings, indexInSiblings: rootIndexInSiblings,
|
|
897
|
+
};
|
|
898
|
+
walk(rootNode, [rootEntry], rootSiblings, rootIndexInSiblings);
|
|
704
899
|
return results;
|
|
705
900
|
}
|
|
706
901
|
|
|
707
|
-
export function matchSelector(
|
|
902
|
+
export function matchSelector(
|
|
903
|
+
rootNode: BalanceNode,
|
|
904
|
+
selector: ParsedSelector,
|
|
905
|
+
siblings: BalanceNode[] = [],
|
|
906
|
+
indexInSiblings = 0,
|
|
907
|
+
): BalanceNode[] {
|
|
708
908
|
const allResults: BalanceNode[] = [];
|
|
709
909
|
const seen = new Set<BalanceNode>();
|
|
710
910
|
for (const alt of selector) {
|
|
711
|
-
for (const node of matchAlternative(rootNode, alt)) {
|
|
911
|
+
for (const node of matchAlternative(rootNode, alt, siblings, indexInSiblings)) {
|
|
712
912
|
if (!seen.has(node)) {
|
|
713
913
|
seen.add(node);
|
|
714
914
|
allResults.push(node);
|