Skip to content

Commit d3fc608

Browse files
authored
feat: support nth of, fix :enabled (#1754)
1 parent b77508c commit d3fc608

6 files changed

Lines changed: 192 additions & 134 deletions

File tree

src/helpers/options.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { InternalOptions } from "../types.js";
2+
3+
/**
4+
* Create a copy of options, omitting `context` and `rootFunc`.
5+
*
6+
* This is used when compiling nested selectors (e.g. inside `:is`, `:not`,
7+
* `:nth-child(… of S)`) so that the parent compilation state doesn't leak.
8+
*/
9+
export function copyOptions<Node, ElementNode extends Node>(
10+
options: InternalOptions<Node, ElementNode>,
11+
): InternalOptions<Node, ElementNode> {
12+
// Omit context and rootFunc so parent compilation state doesn't leak.
13+
const { context: _, rootFunc: __, ...copied } = options;
14+
return copied;
15+
}

src/pseudo-selectors/aliases.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export const aliases: Record<string, string> = {
2424
optgroup[disabled] > option,
2525
fieldset[disabled]:not(fieldset[disabled] legend:first-of-type *)
2626
)`,
27-
enabled: ":not(:disabled)",
27+
enabled:
28+
":is(button, input, select, textarea, optgroup, option, fieldset):not(:disabled)",
2829
checked:
2930
":is(:is(input[type=radio], input[type=checkbox])[checked], :selected)",
3031
required: ":is(input, select, textarea)[required]",

src/pseudo-selectors/filters.ts

Lines changed: 75 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as boolbase from "boolbase";
2+
import { parse } from "css-what";
23
import getNCheck from "nth-check";
34
import { cacheParentResults } from "../helpers/cache.js";
5+
import { copyOptions } from "../helpers/options.js";
46
import { getElementParent } from "../helpers/querying.js";
5-
import type { CompiledQuery, InternalOptions } from "../types.js";
7+
import type { CompiledQuery, CompileToken, InternalOptions } from "../types.js";
68

79
/**
810
* RFC 4647 extended filtering with pre-split subtags.
@@ -29,150 +31,107 @@ function extendedFilter(tag: string[], range: string[]): boolean {
2931
return true;
3032
}
3133

32-
type Filter = <Node, ElementNode extends Node>(
34+
/** @see {@link https://www.w3.org/TR/selectors-4/#the-nth-child-pseudo} */
35+
const nthOfRegex = /^(.+?)\s+of\s+(.+)$/is;
36+
37+
/** A pre-compiled pseudo filter. */
38+
export type Filter = <Node, ElementNode extends Node>(
3339
next: CompiledQuery<ElementNode>,
3440
text: string,
3541
options: InternalOptions<Node, ElementNode>,
3642
context?: Node[],
43+
compileToken?: CompileToken<Node, ElementNode>,
3744
) => CompiledQuery<ElementNode>;
3845

39-
/**
40-
* Pre-compiled pseudo filters.
41-
*/
42-
export const filters: Record<string, Filter> = {
43-
contains(next, text, options) {
44-
const { getText } = options.adapter;
46+
function compileNth(reverse: boolean, ofType: boolean): Filter {
47+
return function nth(next, rule, options, context, compileToken) {
48+
const { adapter, equals } = options;
49+
const ofMatch = ofType ? null : rule.match(nthOfRegex);
50+
const nthCheck = getNCheck(ofMatch ? ofMatch[1].trim() : rule);
4551

46-
return cacheParentResults(next, options, (element) =>
47-
getText(element).includes(text),
48-
);
49-
},
50-
icontains(next, text, options) {
51-
const itext = text.toLowerCase();
52-
const { getText } = options.adapter;
52+
if (nthCheck === boolbase.falseFunc) return boolbase.falseFunc;
5353

54-
return cacheParentResults(next, options, (element) =>
55-
getText(element).toLowerCase().includes(itext),
56-
);
57-
},
54+
const ofSelector =
55+
ofMatch && compileToken
56+
? compileToken(
57+
parse(ofMatch[2].trim()),
58+
copyOptions(options),
59+
context,
60+
)
61+
: undefined;
5862

59-
// Location specific methods
60-
"nth-child"(next, rule, { adapter, equals }) {
61-
const nthCheck = getNCheck(rule);
63+
if (ofSelector === boolbase.falseFunc) return boolbase.falseFunc;
6264

63-
if (nthCheck === boolbase.falseFunc) {
64-
return boolbase.falseFunc;
65-
}
66-
if (nthCheck === boolbase.trueFunc) {
65+
if (nthCheck === boolbase.trueFunc && !ofSelector) {
6766
return (element) =>
6867
getElementParent(element, adapter) !== null && next(element);
6968
}
7069

71-
return function nthChild(element) {
72-
const siblings = adapter.getSiblings(element);
73-
let pos = 0;
74-
75-
for (const sibling of siblings) {
76-
if (equals(element, sibling)) {
77-
break;
78-
}
79-
if (adapter.isTag(sibling)) {
80-
pos++;
70+
type ElementNode = Parameters<typeof next>[0];
71+
72+
const shouldCount = ofSelector
73+
? (_element: ElementNode, sibling: ElementNode) =>
74+
ofSelector(sibling)
75+
: ofType
76+
? (element: ElementNode, sibling: ElementNode) =>
77+
adapter.getName(sibling) === adapter.getName(element)
78+
: boolbase.trueFunc;
79+
80+
if (reverse) {
81+
return function nthLast(element) {
82+
if (ofSelector && !ofSelector(element)) return false;
83+
const siblings = adapter.getSiblings(element);
84+
let pos = 0;
85+
for (let index = siblings.length - 1; index >= 0; index--) {
86+
const sibling = siblings[index];
87+
if (equals(element, sibling)) break;
88+
if (adapter.isTag(sibling) && shouldCount(element, sibling))
89+
pos++;
8190
}
82-
}
83-
84-
return nthCheck(pos) && next(element);
85-
};
86-
},
87-
"nth-last-child"(next, rule, { adapter, equals }) {
88-
const nthCheck = getNCheck(rule);
89-
90-
if (nthCheck === boolbase.falseFunc) {
91-
return boolbase.falseFunc;
92-
}
93-
if (nthCheck === boolbase.trueFunc) {
94-
return (element) =>
95-
getElementParent(element, adapter) !== null && next(element);
91+
return nthCheck(pos) && next(element);
92+
};
9693
}
9794

98-
return function nthLastChild(element) {
95+
return function nth(element) {
96+
if (ofSelector && !ofSelector(element)) return false;
9997
const siblings = adapter.getSiblings(element);
10098
let pos = 0;
101-
102-
for (let index = siblings.length - 1; index >= 0; index--) {
103-
if (equals(element, siblings[index])) {
104-
break;
105-
}
106-
if (adapter.isTag(siblings[index])) {
99+
for (const sibling of siblings) {
100+
if (equals(element, sibling)) break;
101+
if (adapter.isTag(sibling) && shouldCount(element, sibling))
107102
pos++;
108-
}
109103
}
110-
111104
return nthCheck(pos) && next(element);
112105
};
113-
},
114-
"nth-of-type"(next, rule, { adapter, equals }) {
115-
const nthCheck = getNCheck(rule);
116-
117-
if (nthCheck === boolbase.falseFunc) {
118-
return boolbase.falseFunc;
119-
}
120-
if (nthCheck === boolbase.trueFunc) {
121-
return (element) =>
122-
getElementParent(element, adapter) !== null && next(element);
123-
}
124-
125-
return function nthOfType(element) {
126-
const siblings = adapter.getSiblings(element);
127-
let pos = 0;
106+
};
107+
}
128108

129-
for (const currentSibling of siblings) {
130-
if (equals(element, currentSibling)) {
131-
break;
132-
}
133-
if (
134-
adapter.isTag(currentSibling) &&
135-
adapter.getName(currentSibling) === adapter.getName(element)
136-
) {
137-
pos++;
138-
}
139-
}
109+
/**
110+
* Pre-compiled pseudo filters.
111+
*/
112+
export const filters: Record<string, Filter> = {
113+
contains(next, text, options) {
114+
const { getText } = options.adapter;
140115

141-
return nthCheck(pos) && next(element);
142-
};
116+
return cacheParentResults(next, options, (element) =>
117+
getText(element).includes(text),
118+
);
143119
},
144-
"nth-last-of-type"(next, rule, { adapter, equals }) {
145-
const nthCheck = getNCheck(rule);
146-
147-
if (nthCheck === boolbase.falseFunc) {
148-
return boolbase.falseFunc;
149-
}
150-
if (nthCheck === boolbase.trueFunc) {
151-
return (element) =>
152-
getElementParent(element, adapter) !== null && next(element);
153-
}
154-
155-
return function nthLastOfType(element) {
156-
const siblings = adapter.getSiblings(element);
157-
let pos = 0;
158-
159-
for (let index = siblings.length - 1; index >= 0; index--) {
160-
const currentSibling = siblings[index];
161-
if (equals(element, currentSibling)) {
162-
break;
163-
}
164-
if (
165-
adapter.isTag(currentSibling) &&
166-
adapter.getName(currentSibling) === adapter.getName(element)
167-
) {
168-
pos++;
169-
}
170-
}
120+
icontains(next, text, options) {
121+
const itext = text.toLowerCase();
122+
const { getText } = options.adapter;
171123

172-
return nthCheck(pos) && next(element);
173-
};
124+
return cacheParentResults(next, options, (element) =>
125+
getText(element).toLowerCase().includes(itext),
126+
);
174127
},
175128

129+
// Location specific methods
130+
"nth-child": compileNth(false, false),
131+
"nth-last-child": compileNth(true, false),
132+
"nth-of-type": compileNth(false, true),
133+
"nth-last-of-type": compileNth(true, true),
134+
176135
// TODO determine the actual root element
177136
root(next, _rule, { adapter }) {
178137
return (element) =>

src/pseudo-selectors/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,13 @@ export function compilePseudoSelector<Node, ElementNode extends Node>(
6767
}
6868

6969
if (name in filters) {
70-
return filters[name](next, data as string, options, context);
70+
return filters[name](
71+
next,
72+
data as string,
73+
options,
74+
context,
75+
compileToken,
76+
);
7177
}
7278

7379
if (name in pseudos) {

src/pseudo-selectors/subselects.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as boolbase from "boolbase";
22
import type { Selector } from "css-what";
33
import { cacheParentResults } from "../helpers/cache.js";
4+
import { copyOptions } from "../helpers/options.js";
45
import { findOne, getNextSiblings } from "../helpers/querying.js";
56
import { includesScopePseudo, isTraversal } from "../helpers/selectors.js";
67
import type { CompiledQuery, CompileToken, InternalOptions } from "../types.js";
@@ -33,22 +34,6 @@ function hasDependsOnCurrentElement(selector: Selector[][]) {
3334
);
3435
}
3536

36-
function copyOptions<Node, ElementNode extends Node>(
37-
options: InternalOptions<Node, ElementNode>,
38-
): InternalOptions<Node, ElementNode> {
39-
// Not copied: context, rootFunc
40-
return {
41-
xmlMode: !!options.xmlMode,
42-
lowerCaseAttributeNames: !!options.lowerCaseAttributeNames,
43-
lowerCaseTags: !!options.lowerCaseTags,
44-
quirksMode: !!options.quirksMode,
45-
cacheResults: !!options.cacheResults,
46-
pseudos: options.pseudos,
47-
adapter: options.adapter,
48-
equals: options.equals,
49-
};
50-
}
51-
5237
const is: Subselect = (next, token, options, context, compileToken) => {
5338
const compiledToken = compileToken(token, copyOptions(options), context);
5439

0 commit comments

Comments
 (0)