Skip to content

Commit bd7909c

Browse files
committed
added support for nested pseudo classes
:not and :has
1 parent b69d598 commit bd7909c

File tree

4 files changed

+177
-22
lines changed

4 files changed

+177
-22
lines changed

README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Parses and stringifies CSS selectors.
55
``` js
66
import Tokenizer from "css-selector-tokenizer";
77

8-
let input = "a#content.active > div::first-line [data-content], a:visited";
8+
let input = "a#content.active > div::first-line [data-content], a:not(:visited)";
99

1010
Tokenizer.parse(input); // === expected
1111
let expected = {
@@ -28,7 +28,14 @@ let expected = {
2828
type: "selector",
2929
nodes: [
3030
{ type: "element", name: "a" },
31-
{ type: "pseudo-class", name: "visited" }
31+
{ type: "nested-pseudo-class", name: "not", nodes: [
32+
{
33+
type: "selector",
34+
nodes: [
35+
{ type: "pseudo-class", name: "visited" }
36+
]
37+
}
38+
] }
3239
],
3340
before: " "
3441
}

lib/parse.js

+69-14
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,40 @@ function pseudoClassStartMatch(match, name) {
3030
return "inBrackets";
3131
}
3232

33+
function nestedPseudoClassStartMatch(match, name, after) {
34+
var newSelector = {
35+
type: "selector",
36+
nodes: []
37+
};
38+
var newToken = {
39+
type: "nested-pseudo-class",
40+
name: name,
41+
nodes: [newSelector]
42+
};
43+
if(after) {
44+
newSelector.before = after;
45+
}
46+
this.selector.nodes.push(newToken);
47+
this.stack.push(this.root);
48+
this.root = newToken;
49+
this.selector = newSelector;
50+
}
51+
52+
function nestedEnd(match, before) {
53+
if(this.stack.length > 0) {
54+
if(before) {
55+
this.selector.after = before;
56+
}
57+
this.root = this.stack.pop();
58+
this.selector = this.root.nodes[this.root.nodes.length - 1];
59+
} else {
60+
this.selector.nodes.push({
61+
type: "invalid",
62+
value: match
63+
});
64+
}
65+
}
66+
3367
function operatorMatch(match, before, operator, after) {
3468
var token = {
3569
type: "operator",
@@ -51,10 +85,25 @@ function spacingMatch(match) {
5185
});
5286
}
5387

54-
function allMatch() {
55-
this.selector.nodes.push({
56-
type: "all"
57-
});
88+
function elementMatch(match, namespace, name) {
89+
var newToken = {
90+
type: "element",
91+
name: name
92+
};
93+
if(namespace) {
94+
newToken.namespace = namespace.substr(0, namespace.length - 1);
95+
}
96+
this.selector.nodes.push(newToken);
97+
}
98+
99+
function universalMatch(match, namespace) {
100+
var newToken = {
101+
type: "universal"
102+
};
103+
if(namespace) {
104+
newToken.namespace = namespace.substr(0, namespace.length - 1);
105+
}
106+
this.selector.nodes.push(newToken);
58107
}
59108

60109
function attributeMatch(match, content) {
@@ -108,12 +157,16 @@ var parser = new Parser({
108157
"/\\*([\\s\\S]*?)\\*/": commentMatch,
109158
"\\.([A-Za-z_\\-0-9]+)": typeMatch("class"),
110159
"#([A-Za-z_\\-0-9]+)": typeMatch("id"),
160+
":(not|has)\\((\\s*)": nestedPseudoClassStartMatch,
111161
":([A-Za-z_\\-0-9]+)\\(": pseudoClassStartMatch,
112162
":([A-Za-z_\\-0-9]+)": typeMatch("pseudo-class"),
113163
"::([A-Za-z_\\-0-9]+)": typeMatch("pseudo-element"),
114-
"([A-Za-z_\\-0-9]+)": typeMatch("element"),
164+
"(\\*\\|)([A-Za-z_\\-0-9]+)": elementMatch,
165+
"(\\*\\|)\\*": universalMatch,
166+
"([A-Za-z_\\-0-9]*\\|)?\\*": universalMatch,
167+
"([A-Za-z_\\-0-9]*\\|)?([A-Za-z_\\-0-9]+)": elementMatch,
115168
"\\[([^\\]]+)\\]": attributeMatch,
116-
"\\*": allMatch,
169+
"(\\s*)\\)": nestedEnd,
117170
"(\\s*)([>+~])(\\s*)": operatorMatch,
118171
"(\\s*),(\\s*)": nextSelectorMatch,
119172
"\\s+$": irrelevantSpacingEndMatch,
@@ -136,16 +189,18 @@ function parse(str) {
136189
type: "selector",
137190
nodes: []
138191
};
139-
var result = parser.parse("selector", str, {
140-
root: {
141-
type: "selectors",
142-
nodes: [
143-
selectorNode
144-
]
145-
},
192+
var rootNode = {
193+
type: "selectors",
194+
nodes: [
195+
selectorNode
196+
]
197+
};
198+
parser.parse("selector", str, {
199+
stack: [],
200+
root: rootNode,
146201
selector: selectorNode
147202
});
148-
return result.root;
203+
return rootNode;
149204
}
150205

151206
module.exports = parse;

lib/stringify.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function stringifyWithoutBeforeAfter(tree) {
99
case "selector":
1010
return tree.nodes.map(stringify).join("");
1111
case "element":
12-
return tree.name;
12+
return (typeof tree.namespace === "string" ? tree.namespace + "|" : "") + tree.name;
1313
case "class":
1414
return "." + tree.name;
1515
case "id":
@@ -20,14 +20,18 @@ function stringifyWithoutBeforeAfter(tree) {
2020
return tree.value;
2121
case "pseudo-class":
2222
return ":" + tree.name + (typeof tree.content === "string" ? "(" + tree.content + ")" : "");
23+
case "nested-pseudo-class":
24+
return ":" + tree.name + "(" + tree.nodes.map(stringify).join(",") + ")";
2325
case "pseudo-element":
2426
return "::" + tree.name;
25-
case "all":
26-
return "*";
27+
case "universal":
28+
return (typeof tree.namespace === "string" ? tree.namespace + "|" : "") + "*";
2729
case "operator":
2830
return tree.operator;
2931
case "comment":
3032
return "/*" + tree.content + "*/";
33+
case "invalid":
34+
return tree.value;
3135
}
3236
}
3337

test/test-cases.js

+92-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,27 @@ module.exports = {
2020
])
2121
],
2222

23+
"element with namespace": [
24+
"foo|h1",
25+
singleSelector([
26+
{ type: "element", name: "h1", namespace: "foo" }
27+
])
28+
],
29+
30+
"element with any namespace": [
31+
"*|h1",
32+
singleSelector([
33+
{ type: "element", name: "h1", namespace: "*" }
34+
])
35+
],
36+
37+
"element without namespace": [
38+
"|h1",
39+
singleSelector([
40+
{ type: "element", name: "h1", namespace: "" }
41+
])
42+
],
43+
2344
"class name": [
2445
".className",
2546
singleSelector([
@@ -42,9 +63,23 @@ module.exports = {
4263
],
4364

4465
"pseudo class with content": [
66+
":abc(.className)",
67+
singleSelector([
68+
{ type: "pseudo-class", name: "abc", content: ".className" }
69+
])
70+
],
71+
72+
"nested pseudo class with content": [
4573
":not(.className)",
4674
singleSelector([
47-
{ type: "pseudo-class", name: "not", content: ".className" }
75+
{ type: "nested-pseudo-class", name: "not", nodes: [
76+
{
77+
type: "selector",
78+
nodes: [
79+
{ type: "class", name: "className" }
80+
]
81+
}
82+
] }
4883
])
4984
],
5085

@@ -55,10 +90,31 @@ module.exports = {
5590
])
5691
],
5792

58-
"all": [
93+
"universal": [
5994
"*",
6095
singleSelector([
61-
{ type: "all" }
96+
{ type: "universal" }
97+
])
98+
],
99+
100+
"universal with namespace": [
101+
"foo|*",
102+
singleSelector([
103+
{ type: "universal", namespace: "foo" }
104+
])
105+
],
106+
107+
"universal with any namespace": [
108+
"*|*",
109+
singleSelector([
110+
{ type: "universal", namespace: "*" }
111+
])
112+
],
113+
114+
"universal without namespace": [
115+
"|*",
116+
singleSelector([
117+
{ type: "universal", namespace: "" }
62118
])
63119
],
64120

@@ -157,5 +213,38 @@ module.exports = {
157213
singleSelector([
158214
{ type: "pseudo-class", name: "import", content: "\"./module.css\"" }
159215
])
216+
],
217+
218+
"nested pseudo class with multiple selectors": [
219+
":has( h1, h2 )",
220+
singleSelector([
221+
{ type: "nested-pseudo-class", name: "has", nodes: [
222+
{
223+
type: "selector",
224+
nodes: [
225+
{ type: "element", name: "h1" }
226+
],
227+
before: " "
228+
},
229+
{
230+
type: "selector",
231+
nodes: [
232+
{ type: "element", name: "h2" }
233+
],
234+
before: " ",
235+
after: " "
236+
}
237+
] }
238+
])
239+
],
240+
241+
"invalid nesting": [
242+
"a ) b",
243+
singleSelector([
244+
{ type: "element", name: "a" },
245+
{ type: "invalid", value: " )" },
246+
{ type: "spacing", value: " " },
247+
{ type: "element", name: "b" }
248+
])
160249
]
161250
};

0 commit comments

Comments
 (0)