Skip to content

Commit d779e5d

Browse files
committed
Merge pull request #2 from css-modules/escaped-chars-in-classes
Escaped characters should be treated as valid
2 parents 169a822 + 5a5cd02 commit d779e5d

File tree

3 files changed

+81
-21
lines changed

3 files changed

+81
-21
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

+49
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,62 @@ module.exports = {
4848
])
4949
],
5050

51+
"complex class name": [
52+
".class\\.Name",
53+
singleSelector([
54+
{ type: "class", name: "class.Name" }
55+
])
56+
],
57+
58+
"class name starting with number": [
59+
".\\5\\#-\\.5",
60+
singleSelector([
61+
{ type: "class", name: "5#-.5" }
62+
])
63+
],
64+
5165
"id name": [
5266
"#idName",
5367
singleSelector([
5468
{ type: "id", name: "idName" }
5569
])
5670
],
5771

72+
"id name starting with number": [
73+
"#\\5\\#-\\.5",
74+
singleSelector([
75+
{ type: "id", name: "5#-.5" }
76+
])
77+
],
78+
5879
"pseudo class": [
5980
":before",
6081
singleSelector([
6182
{ type: "pseudo-class", name: "before" }
6283
])
6384
],
6485

86+
"pseudo class with escaping": [
87+
":be\\:fo\\#r\\.e",
88+
singleSelector([
89+
{ type: "pseudo-class", name: "be:fo#r.e" }
90+
])
91+
],
92+
6593
"pseudo class with content": [
6694
":abc(.className)",
6795
singleSelector([
6896
{ type: "pseudo-class", name: "abc", content: ".className" }
6997
])
7098
],
7199

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+
72107
"nested pseudo class with content": [
73108
":not(.className)",
74109
singleSelector([
@@ -90,6 +125,13 @@ module.exports = {
90125
])
91126
],
92127

128+
"pseudo element with escaping": [
129+
"::fir\\:\\:st\\.li\\#ne",
130+
singleSelector([
131+
{ type: "pseudo-element", name: "fir::st.li#ne" }
132+
])
133+
],
134+
93135
"universal": [
94136
"*",
95137
singleSelector([
@@ -104,6 +146,13 @@ module.exports = {
104146
])
105147
],
106148

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

0 commit comments

Comments
 (0)