Skip to content

Commit 633aea4

Browse files
committed
Add minimal support of CSS custom properties
1 parent f049d87 commit 633aea4

File tree

7 files changed

+205
-78
lines changed

7 files changed

+205
-78
lines changed

.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"bitwise": true,
33
"curly": true,
44
"eqeqeq": true,
5+
"eqnull": true,
56
"evil": true,
67
"forin": true,
78
"freeze": true,

src/css/Parser.js

+21-8
Original file line numberDiff line numberDiff line change
@@ -1077,10 +1077,10 @@ Parser.prototype = function() {
10771077
* ;
10781078
*/
10791079

1080-
var tokenStream = this._tokenStream,
1081-
value = null,
1082-
hack = null,
1083-
tokenValue,
1080+
var tokenStream = this._tokenStream,
1081+
value = null,
1082+
hack = null,
1083+
propertyName = "",
10841084
token,
10851085
line,
10861086
col;
@@ -1094,18 +1094,31 @@ Parser.prototype = function() {
10941094
col = token.startCol;
10951095
}
10961096

1097+
// consume a single hyphen before finding the identifier, to support custom properties
1098+
if (tokenStream.peek() === Tokens.MINUS) {
1099+
tokenStream.get();
1100+
token = tokenStream.token();
1101+
propertyName = token.value;
1102+
line = token.startLine;
1103+
col = token.startCol;
1104+
}
1105+
10971106
if (tokenStream.match(Tokens.IDENT)) {
10981107
token = tokenStream.token();
1099-
tokenValue = token.value;
1108+
propertyName += token.value;
11001109

11011110
// check for underscore hack - no error if not allowed because it's valid CSS syntax
1102-
if (tokenValue.charAt(0) === "_" && this.options.underscoreHack) {
1111+
if (propertyName.charAt(0) === "_" && this.options.underscoreHack) {
11031112
hack = "_";
1104-
tokenValue = tokenValue.substring(1);
1113+
propertyName = propertyName.substring(1);
11051114
}
11061115

1107-
value = new PropertyName(tokenValue, hack, line || token.startLine, col || token.startCol);
1116+
value = new PropertyName(propertyName, hack, line || token.startLine, col || token.startCol);
11081117
this._readWhitespace();
1118+
} else if (tokenStream.peek() === Tokens.RBRACE) {
1119+
// Encountered when there are no more properties.
1120+
} else {
1121+
this._unexpectedToken(tokenStream.LT(1));
11091122
}
11101123

11111124
return value;

src/css/TokenStream.js

+93-52
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,31 @@ var h = /^[0-9a-fA-F]$/,
1818

1919

2020
function isHexDigit(c) {
21-
return c !== null && h.test(c);
21+
return c != null && h.test(c);
2222
}
2323

2424
function isDigit(c) {
25-
return c !== null && /\d/.test(c);
25+
return c != null && /\d/.test(c);
2626
}
2727

2828
function isWhitespace(c) {
29-
return c !== null && whitespace.test(c);
29+
return c != null && whitespace.test(c);
3030
}
3131

3232
function isNewLine(c) {
33-
return c !== null && nl.test(c);
33+
return c != null && nl.test(c);
3434
}
3535

3636
function isNameStart(c) {
37-
return c !== null && /[a-z_\u00A0-\uFFFF\\]/i.test(c);
37+
return c != null && /[a-z_\u00A0-\uFFFF\\]/i.test(c);
3838
}
3939

4040
function isNameChar(c) {
41-
return c !== null && (isNameStart(c) || /[0-9\-\\]/.test(c));
41+
return c != null && (isNameStart(c) || /[0-9\-\\]/.test(c));
4242
}
4343

4444
function isIdentStart(c) {
45-
return c !== null && (isNameStart(c) || /-\\/.test(c));
45+
return c != null && (isNameStart(c) || /-\\/.test(c));
4646
}
4747

4848
function mix(receiver, supplier) {
@@ -54,6 +54,16 @@ function mix(receiver, supplier) {
5454
return receiver;
5555
}
5656

57+
function wouldStartIdent(twoCodePoints) {
58+
return typeof twoCodePoints === "string" &&
59+
(twoCodePoints[0] === "-" && isNameStart(twoCodePoints[1]) || isNameStart(twoCodePoints[0]));
60+
}
61+
62+
function wouldStartUnsignedNumber(twoCodePoints) {
63+
return typeof twoCodePoints === "string" &&
64+
(isDigit(twoCodePoints[0]) || (twoCodePoints[0] === "." && isDigit(twoCodePoints[1])));
65+
}
66+
5767
//-----------------------------------------------------------------------------
5868
// CSS Token Stream
5969
//-----------------------------------------------------------------------------
@@ -89,7 +99,6 @@ TokenStream.prototype = mix(new TokenStreamBase(), {
8999

90100
c = reader.read();
91101

92-
93102
while (c) {
94103
switch (c) {
95104

@@ -170,16 +179,31 @@ TokenStream.prototype = mix(new TokenStreamBase(), {
170179
/*
171180
* Potential tokens:
172181
* - CDC
173-
* - MINUS
174182
* - NUMBER
175183
* - DIMENSION
176184
* - PERCENTAGE
185+
* - IDENT
186+
* - MINUS
177187
*/
178188
case "-":
179-
if (reader.peek() === "-") { // could be closing HTML-style comment
189+
if (wouldStartUnsignedNumber(reader.peekCount(2))) {
190+
token = this.numberToken(c, startLine, startCol);
191+
break;
192+
} else if (reader.peekCount(2) === "->") {
180193
token = this.htmlCommentEndToken(c, startLine, startCol);
181-
} else if (isNameStart(reader.peek())) {
182-
token = this.identOrFunctionToken(c, startLine, startCol);
194+
} else {
195+
token = this._getDefaultToken(c, startLine, startCol);
196+
}
197+
break;
198+
199+
/*
200+
* Potential tokens:
201+
* - NUMBER
202+
* - PLUS
203+
*/
204+
case "+":
205+
if (wouldStartUnsignedNumber(reader.peekCount(2))) {
206+
token = this.numberToken(c, startLine, startCol);
183207
} else {
184208
token = this.charToken(c, startLine, startCol);
185209
}
@@ -242,48 +266,13 @@ TokenStream.prototype = mix(new TokenStreamBase(), {
242266
case "u":
243267
if (reader.peek() === "+") {
244268
token = this.unicodeRangeToken(c, startLine, startCol);
245-
break;
246-
}
247-
/* falls through */
248-
default:
249-
250-
/*
251-
* Potential tokens:
252-
* - NUMBER
253-
* - DIMENSION
254-
* - LENGTH
255-
* - FREQ
256-
* - TIME
257-
* - EMS
258-
* - EXS
259-
* - ANGLE
260-
*/
261-
if (isDigit(c)) {
262-
token = this.numberToken(c, startLine, startCol);
263-
} else
264-
265-
/*
266-
* Potential tokens:
267-
* - S
268-
*/
269-
if (isWhitespace(c)) {
270-
token = this.whitespaceToken(c, startLine, startCol);
271-
} else
272-
273-
/*
274-
* Potential tokens:
275-
* - IDENT
276-
*/
277-
if (isIdentStart(c)) {
278-
token = this.identOrFunctionToken(c, startLine, startCol);
279269
} else {
280-
/*
281-
* Potential tokens:
282-
* - CHAR
283-
* - PLUS
284-
*/
285-
token = this.charToken(c, startLine, startCol);
270+
token = this._getDefaultToken(c, startLine, startCol);
286271
}
272+
break;
273+
274+
default:
275+
token = this._getDefaultToken(c, startLine, startCol);
287276

288277
}
289278

@@ -299,6 +288,58 @@ TokenStream.prototype = mix(new TokenStreamBase(), {
299288
return token;
300289
},
301290

291+
/**
292+
* Produces a token based on the given character and location in the
293+
* stream, when no other case applies.
294+
* Potential tokens:
295+
* - NUMBER
296+
* - DIMENSION
297+
* - LENGTH
298+
* - FREQ
299+
* - TIME
300+
* - EMS
301+
* - EXS
302+
* - ANGLE
303+
* @param {String} c The character for the token.
304+
* @param {int} startLine The beginning line for the character.
305+
* @param {int} startCol The beginning column for the character.
306+
* @return {Object} A token object.
307+
* @method _getDefaultToken
308+
*/
309+
_getDefaultToken: function(c, startLine, startCol) {
310+
var reader = this._reader,
311+
token = null;
312+
313+
if (isDigit(c)) {
314+
token = this.numberToken(c, startLine, startCol);
315+
} else
316+
317+
/*
318+
* Potential tokens:
319+
* - S
320+
*/
321+
if (isWhitespace(c)) {
322+
token = this.whitespaceToken(c, startLine, startCol);
323+
} else
324+
325+
/*
326+
* Potential tokens:
327+
* - IDENT
328+
*/
329+
if (wouldStartIdent(c + reader.peekCount(1))) {
330+
token = this.identOrFunctionToken(c, startLine, startCol);
331+
} else {
332+
/*
333+
* Potential tokens:
334+
* - CHAR
335+
* - PLUS
336+
*/
337+
token = this.charToken(c, startLine, startCol);
338+
}
339+
340+
return token;
341+
},
342+
302343
//-------------------------------------------------------------------------
303344
// Methods to create tokens
304345
//-------------------------------------------------------------------------

src/util/StringReader.js

+12
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,18 @@ StringReader.prototype = {
164164
// Advanced reading
165165
//-------------------------------------------------------------------------
166166

167+
/**
168+
* Reads a given number of characters without advancing the cursor.
169+
* @param {int} count How many characters to look ahead (default is 1).
170+
* @return {String} The characters as a string, or an empty string if
171+
* there is no next character.
172+
* @method peek
173+
*/
174+
peekCount: function(count) {
175+
count = typeof count === "undefined" ? 1 : Math.max(count, 0);
176+
return this._input.substring(this._cursor, this._cursor + count);
177+
},
178+
167179
/**
168180
* Reads up to and including the given string. Throws an error if that
169181
* string is not found.

tests/css/Parser.js

+42-1
Original file line numberDiff line numberDiff line change
@@ -2138,6 +2138,21 @@ var YUITest = require("yuitest"),
21382138
parser.parse(".foo {\n color: #fff;\n}");
21392139
},
21402140

2141+
"Test rule with custom property": function() {
2142+
var parser = new Parser({ strict: true });
2143+
parser.addListener("property", function(event) {
2144+
Assert.areEqual("--color-Foo_BAR", event.property.toString());
2145+
Assert.areEqual("#fff", event.value.toString());
2146+
Assert.areEqual(3, event.property.col, "Property column should be 3.");
2147+
Assert.areEqual(2, event.property.line, "Property line should be 2.");
2148+
Assert.areEqual(3, event.col, "Event column should be 3.");
2149+
Assert.areEqual(2, event.line, "Event line should be 2.");
2150+
Assert.areEqual(20, event.value.parts[0].col, "First part column should be 20.");
2151+
Assert.areEqual(2, event.value.parts[0].line, "First part line should be 2.");
2152+
});
2153+
parser.parse(".foo {\n --color-Foo_BAR: #fff;\n}");
2154+
},
2155+
21412156
"Test rule with star hack property": function() {
21422157
var parser = new Parser({
21432158
strict: true,
@@ -2289,7 +2304,33 @@ var YUITest = require("yuitest"),
22892304

22902305
name: "Invalid CSS Parsing Tests",
22912306

2292-
"Test parsing invalid celector": function() {
2307+
"Test parsing custom property typo": function() {
2308+
var error;
2309+
var parser = new Parser();
2310+
parser.addListener("error", function(e) {
2311+
error = e;
2312+
});
2313+
parser.parse("a:hover{\ncolor:red;\n==myFont:Helvetica;/*dropped*/;\nborder:0\n}");
2314+
2315+
Assert.areEqual("error", error.type);
2316+
Assert.areEqual(3, error.line);
2317+
Assert.areEqual(1, error.col);
2318+
},
2319+
2320+
"Test parsing invalid property": function() {
2321+
var error;
2322+
var parser = new Parser();
2323+
parser.addListener("error", function(e) {
2324+
error = e;
2325+
});
2326+
parser.parse("a:hover{\ncolor:red;\nfont::Helvetica;/*dropped*/;\nborder:0\n}");
2327+
2328+
Assert.areEqual("error", error.type);
2329+
Assert.areEqual(3, error.line);
2330+
Assert.areEqual(6, error.col);
2331+
},
2332+
2333+
"Test parsing invalid selector": function() {
22932334
var error;
22942335
var parser = new Parser();
22952336
parser.addListener("error", function(e) {

0 commit comments

Comments
 (0)