Skip to content

Commit 0b7bbf1

Browse files
authored
Merge pull request #14 from Kovensky/unicode-identifiers
Support unicode identifiers
2 parents db76c76 + 17379f6 commit 0b7bbf1

File tree

4 files changed

+77
-11
lines changed

4 files changed

+77
-11
lines changed

β€Žlib/parse.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
var Parser = require("fastparse");
4+
var regexpu = require("regexpu-core");
45

56
function unescape(str) {
67
return str.replace(/\\(.)/g, "$1");
@@ -164,11 +165,19 @@ function bracketEnd(match) {
164165
this.token.content += match;
165166
}
166167

167-
var parser = new Parser({
168-
selector: {
169-
"/\\*([\\s\\S]*?)\\*/": commentMatch,
170-
"\\.((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": typeMatch("class"),
171-
"#((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": typeMatch("id"),
168+
function getSelectors() {
169+
// The assignment here is split to preserve the property enumeration order.
170+
var selectors = {
171+
"/\\*([\\s\\S]*?)\\*/": commentMatch
172+
};
173+
// https://www.w3.org/TR/CSS21/syndata.html#characters
174+
// 4.1.3: identifiers (...) can contain only the characters [a-zA-Z0-9] and
175+
// ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_)
176+
//
177+
// 10ffff is the maximum allowed in current Unicode
178+
selectors[regexpu("\\.((?:\\\\.|[A-Za-z_\\-\\u{00a0}-\\u{10ffff}])(?:\\\\.|[A-Za-z_\\-0-9\\u{00a0}-\\u{10ffff}])*)", "u")] = typeMatch("class");
179+
selectors[regexpu("#((?:\\\\.|[A-Za-z_\\-\\u{00a0}-\\u{10ffff}])(?:\\\\.|[A-Za-z_\\-0-9\\u{00a0}-\\u{10ffff}])*)", "u")] = typeMatch("id");
180+
var selectorsSecondHalf = {
172181
":(not|matches|has|local|global)\\((\\s*)": nestedPseudoClassStartMatch,
173182
":((?:\\\\.|[A-Za-z_\\-0-9])+)\\(": pseudoClassStartMatch,
174183
":((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-class"),
@@ -185,7 +194,18 @@ var parser = new Parser({
185194
"^\\s+": irrelevantSpacingStartMatch,
186195
"\\s+": spacingMatch,
187196
".": invalidMatch
188-
},
197+
};
198+
var selector;
199+
for (selector in selectorsSecondHalf) {
200+
if (Object.prototype.hasOwnProperty.call(selectorsSecondHalf, selector)) {
201+
selectors[selector] = selectorsSecondHalf[selector];
202+
}
203+
}
204+
return selectors;
205+
}
206+
207+
var parser = new Parser({
208+
selector: getSelectors(),
189209
inBrackets: {
190210
"/\\*[\\s\\S]*?\\*/": addToCurrent,
191211
"\"([^\\\\\"]|\\\\.)*\"": addToCurrent,

β€Žlib/stringify.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22

33
var stringify;
44

5-
function escape(str) {
5+
var regexpu = require("regexpu-core");
6+
var identifierEscapeRegexp = new RegExp(
7+
regexpu("(^[^A-Za-z_\\-\\u{00a0}-\\u{10ffff}]|^\\-\\-|[^A-Za-z_0-9\\-\\u{00a0}-\\u{10ffff}])", "ug"),
8+
"g"
9+
);
10+
11+
function escape(str, identifier) {
612
if(str === "*") {
713
return "*";
814
}
9-
return str.replace(/(^[^A-Za-z_\\-]|^\-\-|[^A-Za-z_0-9\\-])/g, "\\$1");
15+
if (identifier) {
16+
return str.replace(identifierEscapeRegexp, "\\$1");
17+
} else {
18+
return str.replace(/(^[^A-Za-z_\\-]|^\-\-|[^A-Za-z_0-9\\-])/g, "\\$1");
19+
}
1020
}
1121

1222
function stringifyWithoutBeforeAfter(tree) {
@@ -18,9 +28,9 @@ function stringifyWithoutBeforeAfter(tree) {
1828
case "element":
1929
return (typeof tree.namespace === "string" ? escape(tree.namespace) + "|" : "") + escape(tree.name);
2030
case "class":
21-
return "." + escape(tree.name);
31+
return "." + escape(tree.name, true);
2232
case "id":
23-
return "#" + escape(tree.name);
33+
return "#" + escape(tree.name, true);
2434
case "attribute":
2535
return "[" + tree.content + "]";
2636
case "spacing":

β€Žpackage.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"homepage": "https://github.com/css-modules/css-selector-tokenizer",
3333
"dependencies": {
3434
"cssesc": "^0.1.0",
35-
"fastparse": "^1.1.1"
35+
"fastparse": "^1.1.1",
36+
"regexpu-core": "^1.0.0"
3637
},
3738
"devDependencies": {
3839
"chokidar-cli": "^0.2.1",

β€Žtest/test-cases.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,27 @@ module.exports = {
6565
])
6666
],
6767

68+
"class name with high BMP character": [
69+
".ε­—",
70+
singleSelector([
71+
{ type: "class", name: "ε­—" }
72+
])
73+
],
74+
75+
"class name with emoji": [
76+
".πŸ€”",
77+
singleSelector([
78+
{ type: "class", name: "πŸ€”" }
79+
])
80+
],
81+
82+
"class name with multiple emoji": [
83+
".πŸ‘πŸ‘Œ",
84+
singleSelector([
85+
{ type: "class", name: "πŸ‘πŸ‘Œ" }
86+
])
87+
],
88+
6889
"id name": [
6990
"#idName",
7091
singleSelector([
@@ -79,6 +100,20 @@ module.exports = {
79100
])
80101
],
81102

103+
"id name with latin-1 character": [
104+
"#Β‘",
105+
singleSelector([
106+
{ type: "id", name: "Β‘" }
107+
])
108+
],
109+
110+
"id name with complex emoji": [
111+
".πŸ––πŸΌ",
112+
singleSelector([
113+
{ type: "class", name: "πŸ––πŸΌ" }
114+
])
115+
],
116+
82117
"pseudo class": [
83118
":before",
84119
singleSelector([

0 commit comments

Comments
Β (0)