Skip to content

Commit 5a5cd02

Browse files
committed
allow escaped names in classes, id, etc.
1 parent 0cada10 commit 5a5cd02

File tree

3 files changed

+70
-24
lines changed

3 files changed

+70
-24
lines changed

lib/parse.js

+18-14
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
var Parser = require("fastparse");
44

5+
function unescape(str) {
6+
return str.replace(/\\(.)/g, "$1");
7+
}
8+
59
function commentMatch(match, content) {
610
this.selector.nodes.push({
711
type: "comment",
@@ -13,15 +17,15 @@ function typeMatch(type) {
1317
return function(match, name) {
1418
this.selector.nodes.push({
1519
type: type,
16-
name: name
20+
name: unescape(name)
1721
});
1822
};
1923
}
2024

2125
function pseudoClassStartMatch(match, name) {
2226
var newToken = {
2327
type: "pseudo-class",
24-
name: name,
28+
name: unescape(name),
2529
content: ""
2630
};
2731
this.selector.nodes.push(newToken);
@@ -37,7 +41,7 @@ function nestedPseudoClassStartMatch(match, name, after) {
3741
};
3842
var newToken = {
3943
type: "nested-pseudo-class",
40-
name: name,
44+
name: unescape(name),
4145
nodes: [newSelector]
4246
};
4347
if(after) {
@@ -88,10 +92,10 @@ function spacingMatch(match) {
8892
function elementMatch(match, namespace, name) {
8993
var newToken = {
9094
type: "element",
91-
name: name
95+
name: unescape(name)
9296
};
9397
if(namespace) {
94-
newToken.namespace = namespace.substr(0, namespace.length - 1);
98+
newToken.namespace = unescape(namespace.substr(0, namespace.length - 1));
9599
}
96100
this.selector.nodes.push(newToken);
97101
}
@@ -101,7 +105,7 @@ function universalMatch(match, namespace) {
101105
type: "universal"
102106
};
103107
if(namespace) {
104-
newToken.namespace = namespace.substr(0, namespace.length - 1);
108+
newToken.namespace = unescape(namespace.substr(0, namespace.length - 1));
105109
}
106110
this.selector.nodes.push(newToken);
107111
}
@@ -162,16 +166,16 @@ function bracketEnd(match) {
162166
var parser = new Parser({
163167
selector: {
164168
"/\\*([\\s\\S]*?)\\*/": commentMatch,
165-
"\\.([A-Za-z_\\-0-9]+)": typeMatch("class"),
166-
"#([A-Za-z_\\-0-9]+)": typeMatch("id"),
169+
"\\.((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": typeMatch("class"),
170+
"#((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": typeMatch("id"),
167171
":(not|has|local|global)\\((\\s*)": nestedPseudoClassStartMatch,
168-
":([A-Za-z_\\-0-9]+)\\(": pseudoClassStartMatch,
169-
":([A-Za-z_\\-0-9]+)": typeMatch("pseudo-class"),
170-
"::([A-Za-z_\\-0-9]+)": typeMatch("pseudo-element"),
171-
"(\\*\\|)([A-Za-z_\\-0-9]+)": elementMatch,
172+
":((?:\\\\.|[A-Za-z_\\-0-9])+)\\(": pseudoClassStartMatch,
173+
":((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-class"),
174+
"::((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-element"),
175+
"(\\*\\|)((?:\\\\.|[A-Za-z_\\-0-9])+)": elementMatch,
172176
"(\\*\\|)\\*": universalMatch,
173-
"([A-Za-z_\\-0-9]*\\|)?\\*": universalMatch,
174-
"([A-Za-z_\\-0-9]*\\|)?([A-Za-z_\\-0-9]+)": elementMatch,
177+
"((?:\\\\.|[A-Za-z_\\-0-9])*\\|)?\\*": universalMatch,
178+
"((?:\\\\.|[A-Za-z_\\-0-9])*\\|)?((?:\\\\.|[A-Za-z_\\-0-9])+)": elementMatch,
175179
"\\[([^\\]]+)\\]": attributeMatch,
176180
"(\\s*)\\)": nestedEnd,
177181
"(\\s*)([>+~])(\\s*)": operatorMatch,

lib/stringify.js

+14-7
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,37 @@
22

33
var stringify;
44

5+
function escape(str) {
6+
if(str === "*") {
7+
return "*";
8+
}
9+
return str.replace(/(^[^A-Za-z_\\-]|[^A-Za-z_0-9\\-])/g, "\\$1");
10+
}
11+
512
function stringifyWithoutBeforeAfter(tree) {
613
switch(tree.type) {
714
case "selectors":
815
return tree.nodes.map(stringify).join(",");
916
case "selector":
1017
return tree.nodes.map(stringify).join("");
1118
case "element":
12-
return (typeof tree.namespace === "string" ? tree.namespace + "|" : "") + tree.name;
19+
return (typeof tree.namespace === "string" ? escape(tree.namespace) + "|" : "") + escape(tree.name);
1320
case "class":
14-
return "." + tree.name;
21+
return "." + escape(tree.name);
1522
case "id":
16-
return "#" + tree.name;
23+
return "#" + escape(tree.name);
1724
case "attribute":
1825
return "[" + tree.content + "]";
1926
case "spacing":
2027
return tree.value;
2128
case "pseudo-class":
22-
return ":" + tree.name + (typeof tree.content === "string" ? "(" + tree.content + ")" : "");
29+
return ":" + escape(tree.name) + (typeof tree.content === "string" ? "(" + tree.content + ")" : "");
2330
case "nested-pseudo-class":
24-
return ":" + tree.name + "(" + tree.nodes.map(stringify).join(",") + ")";
31+
return ":" + escape(tree.name) + "(" + tree.nodes.map(stringify).join(",") + ")";
2532
case "pseudo-element":
26-
return "::" + tree.name;
33+
return "::" + escape(tree.name);
2734
case "universal":
28-
return (typeof tree.namespace === "string" ? tree.namespace + "|" : "") + "*";
35+
return (typeof tree.namespace === "string" ? escape(tree.namespace) + "|" : "") + "*";
2936
case "operator":
3037
return tree.operator;
3138
case "comment":

test/test-cases.js

+38-3
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ module.exports = {
5151
"complex class name": [
5252
".class\\.Name",
5353
singleSelector([
54-
{ type: "class", name: "class\\.Name" }
54+
{ type: "class", name: "class.Name" }
5555
])
5656
],
5757

5858
"class name starting with number": [
59-
".\\5-5",
59+
".\\5\\#-\\.5",
6060
singleSelector([
61-
{ type: "class", name: "\\5-5" }
61+
{ type: "class", name: "5#-.5" }
6262
])
6363
],
6464

@@ -69,20 +69,41 @@ module.exports = {
6969
])
7070
],
7171

72+
"id name starting with number": [
73+
"#\\5\\#-\\.5",
74+
singleSelector([
75+
{ type: "id", name: "5#-.5" }
76+
])
77+
],
78+
7279
"pseudo class": [
7380
":before",
7481
singleSelector([
7582
{ type: "pseudo-class", name: "before" }
7683
])
7784
],
7885

86+
"pseudo class with escaping": [
87+
":be\\:fo\\#r\\.e",
88+
singleSelector([
89+
{ type: "pseudo-class", name: "be:fo#r.e" }
90+
])
91+
],
92+
7993
"pseudo class with content": [
8094
":abc(.className)",
8195
singleSelector([
8296
{ type: "pseudo-class", name: "abc", content: ".className" }
8397
])
8498
],
8599

100+
"pseudo class with content and escaping": [
101+
":a\\:b\\.c(.className)",
102+
singleSelector([
103+
{ type: "pseudo-class", name: "a:b.c", content: ".className" }
104+
])
105+
],
106+
86107
"nested pseudo class with content": [
87108
":not(.className)",
88109
singleSelector([
@@ -104,6 +125,13 @@ module.exports = {
104125
])
105126
],
106127

128+
"pseudo element with escaping": [
129+
"::fir\\:\\:st\\.li\\#ne",
130+
singleSelector([
131+
{ type: "pseudo-element", name: "fir::st.li#ne" }
132+
])
133+
],
134+
107135
"universal": [
108136
"*",
109137
singleSelector([
@@ -118,6 +146,13 @@ module.exports = {
118146
])
119147
],
120148

149+
"universal with namespace and escaping": [
150+
"f\\|o\\.o|*",
151+
singleSelector([
152+
{ type: "universal", namespace: "f|o.o" }
153+
])
154+
],
155+
121156
"universal with any namespace": [
122157
"*|*",
123158
singleSelector([

0 commit comments

Comments
 (0)