diff --git a/.jshintrc b/.jshintrc index 7c368cdc..df2e6666 100644 --- a/.jshintrc +++ b/.jshintrc @@ -2,6 +2,7 @@ "bitwise": true, "curly": true, "eqeqeq": true, + "eqnull": true, "evil": true, "forin": true, "freeze": true, diff --git a/src/css/Parser.js b/src/css/Parser.js index 65525378..990ef9a1 100644 --- a/src/css/Parser.js +++ b/src/css/Parser.js @@ -1077,10 +1077,10 @@ Parser.prototype = function() { * ; */ - var tokenStream = this._tokenStream, - value = null, - hack = null, - tokenValue, + var tokenStream = this._tokenStream, + value = null, + hack = null, + propertyName = "", token, line, col; @@ -1094,18 +1094,31 @@ Parser.prototype = function() { col = token.startCol; } + // consume a single hyphen before finding the identifier, to support custom properties + if (tokenStream.peek() === Tokens.MINUS) { + tokenStream.get(); + token = tokenStream.token(); + propertyName = token.value; + line = token.startLine; + col = token.startCol; + } + if (tokenStream.match(Tokens.IDENT)) { token = tokenStream.token(); - tokenValue = token.value; + propertyName += token.value; // check for underscore hack - no error if not allowed because it's valid CSS syntax - if (tokenValue.charAt(0) === "_" && this.options.underscoreHack) { + if (propertyName.charAt(0) === "_" && this.options.underscoreHack) { hack = "_"; - tokenValue = tokenValue.substring(1); + propertyName = propertyName.substring(1); } - value = new PropertyName(tokenValue, hack, line || token.startLine, col || token.startCol); + value = new PropertyName(propertyName, hack, line || token.startLine, col || token.startCol); this._readWhitespace(); + } else if (tokenStream.peek() === Tokens.RBRACE) { + // Encountered when there are no more properties. + } else { + this._unexpectedToken(tokenStream.LT(1)); } return value; diff --git a/src/css/TokenStream.js b/src/css/TokenStream.js index 70acd7a7..329077c4 100755 --- a/src/css/TokenStream.js +++ b/src/css/TokenStream.js @@ -18,31 +18,31 @@ var h = /^[0-9a-fA-F]$/, function isHexDigit(c) { - return c !== null && h.test(c); + return c != null && h.test(c); } function isDigit(c) { - return c !== null && /\d/.test(c); + return c != null && /\d/.test(c); } function isWhitespace(c) { - return c !== null && whitespace.test(c); + return c != null && whitespace.test(c); } function isNewLine(c) { - return c !== null && nl.test(c); + return c != null && nl.test(c); } function isNameStart(c) { - return c !== null && /[a-z_\u00A0-\uFFFF\\]/i.test(c); + return c != null && /[a-z_\u00A0-\uFFFF\\]/i.test(c); } function isNameChar(c) { - return c !== null && (isNameStart(c) || /[0-9\-\\]/.test(c)); + return c != null && (isNameStart(c) || /[0-9\-\\]/.test(c)); } function isIdentStart(c) { - return c !== null && (isNameStart(c) || /-\\/.test(c)); + return c != null && (isNameStart(c) || /-\\/.test(c)); } function mix(receiver, supplier) { @@ -54,6 +54,16 @@ function mix(receiver, supplier) { return receiver; } +function wouldStartIdent(twoCodePoints) { + return typeof twoCodePoints === "string" && + (twoCodePoints[0] === "-" && isNameStart(twoCodePoints[1]) || isNameStart(twoCodePoints[0])); +} + +function wouldStartUnsignedNumber(twoCodePoints) { + return typeof twoCodePoints === "string" && + (isDigit(twoCodePoints[0]) || (twoCodePoints[0] === "." && isDigit(twoCodePoints[1]))); +} + //----------------------------------------------------------------------------- // CSS Token Stream //----------------------------------------------------------------------------- @@ -89,7 +99,6 @@ TokenStream.prototype = mix(new TokenStreamBase(), { c = reader.read(); - while (c) { switch (c) { @@ -170,16 +179,31 @@ TokenStream.prototype = mix(new TokenStreamBase(), { /* * Potential tokens: * - CDC - * - MINUS * - NUMBER * - DIMENSION * - PERCENTAGE + * - IDENT + * - MINUS */ case "-": - if (reader.peek() === "-") { // could be closing HTML-style comment + if (wouldStartUnsignedNumber(reader.peekCount(2))) { + token = this.numberToken(c, startLine, startCol); + break; + } else if (reader.peekCount(2) === "->") { token = this.htmlCommentEndToken(c, startLine, startCol); - } else if (isNameStart(reader.peek())) { - token = this.identOrFunctionToken(c, startLine, startCol); + } else { + token = this._getDefaultToken(c, startLine, startCol); + } + break; + + /* + * Potential tokens: + * - NUMBER + * - PLUS + */ + case "+": + if (wouldStartUnsignedNumber(reader.peekCount(2))) { + token = this.numberToken(c, startLine, startCol); } else { token = this.charToken(c, startLine, startCol); } @@ -242,48 +266,13 @@ TokenStream.prototype = mix(new TokenStreamBase(), { case "u": if (reader.peek() === "+") { token = this.unicodeRangeToken(c, startLine, startCol); - break; - } - /* falls through */ - default: - - /* - * Potential tokens: - * - NUMBER - * - DIMENSION - * - LENGTH - * - FREQ - * - TIME - * - EMS - * - EXS - * - ANGLE - */ - if (isDigit(c)) { - token = this.numberToken(c, startLine, startCol); - } else - - /* - * Potential tokens: - * - S - */ - if (isWhitespace(c)) { - token = this.whitespaceToken(c, startLine, startCol); - } else - - /* - * Potential tokens: - * - IDENT - */ - if (isIdentStart(c)) { - token = this.identOrFunctionToken(c, startLine, startCol); } else { - /* - * Potential tokens: - * - CHAR - * - PLUS - */ - token = this.charToken(c, startLine, startCol); + token = this._getDefaultToken(c, startLine, startCol); } + break; + + default: + token = this._getDefaultToken(c, startLine, startCol); } @@ -299,6 +288,58 @@ TokenStream.prototype = mix(new TokenStreamBase(), { return token; }, + /** + * Produces a token based on the given character and location in the + * stream, when no other case applies. + * Potential tokens: + * - NUMBER + * - DIMENSION + * - LENGTH + * - FREQ + * - TIME + * - EMS + * - EXS + * - ANGLE + * @param {String} c The character for the token. + * @param {int} startLine The beginning line for the character. + * @param {int} startCol The beginning column for the character. + * @return {Object} A token object. + * @method _getDefaultToken + */ + _getDefaultToken: function(c, startLine, startCol) { + var reader = this._reader, + token = null; + + if (isDigit(c)) { + token = this.numberToken(c, startLine, startCol); + } else + + /* + * Potential tokens: + * - S + */ + if (isWhitespace(c)) { + token = this.whitespaceToken(c, startLine, startCol); + } else + + /* + * Potential tokens: + * - IDENT + */ + if (wouldStartIdent(c + reader.peekCount(1))) { + token = this.identOrFunctionToken(c, startLine, startCol); + } else { + /* + * Potential tokens: + * - CHAR + * - PLUS + */ + token = this.charToken(c, startLine, startCol); + } + + return token; + }, + //------------------------------------------------------------------------- // Methods to create tokens //------------------------------------------------------------------------- diff --git a/src/util/StringReader.js b/src/util/StringReader.js index 41f1d8fc..dea9daf1 100755 --- a/src/util/StringReader.js +++ b/src/util/StringReader.js @@ -164,6 +164,18 @@ StringReader.prototype = { // Advanced reading //------------------------------------------------------------------------- + /** + * Reads a given number of characters without advancing the cursor. + * @param {int} count How many characters to look ahead (default is 1). + * @return {String} The characters as a string, or an empty string if + * there is no next character. + * @method peek + */ + peekCount: function(count) { + count = typeof count === "undefined" ? 1 : Math.max(count, 0); + return this._input.substring(this._cursor, this._cursor + count); + }, + /** * Reads up to and including the given string. Throws an error if that * string is not found. diff --git a/tests/css/Parser.js b/tests/css/Parser.js index f050f412..3b5ec5e3 100644 --- a/tests/css/Parser.js +++ b/tests/css/Parser.js @@ -2138,6 +2138,21 @@ var YUITest = require("yuitest"), parser.parse(".foo {\n color: #fff;\n}"); }, + "Test rule with custom property": function() { + var parser = new Parser({ strict: true }); + parser.addListener("property", function(event) { + Assert.areEqual("--color-Foo_BAR", event.property.toString()); + Assert.areEqual("#fff", event.value.toString()); + Assert.areEqual(3, event.property.col, "Property column should be 3."); + Assert.areEqual(2, event.property.line, "Property line should be 2."); + Assert.areEqual(3, event.col, "Event column should be 3."); + Assert.areEqual(2, event.line, "Event line should be 2."); + Assert.areEqual(20, event.value.parts[0].col, "First part column should be 20."); + Assert.areEqual(2, event.value.parts[0].line, "First part line should be 2."); + }); + parser.parse(".foo {\n --color-Foo_BAR: #fff;\n}"); + }, + "Test rule with star hack property": function() { var parser = new Parser({ strict: true, @@ -2289,7 +2304,33 @@ var YUITest = require("yuitest"), name: "Invalid CSS Parsing Tests", - "Test parsing invalid celector": function() { + "Test parsing custom property typo": function() { + var error; + var parser = new Parser(); + parser.addListener("error", function(e) { + error = e; + }); + parser.parse("a:hover{\ncolor:red;\n==myFont:Helvetica;/*dropped*/;\nborder:0\n}"); + + Assert.areEqual("error", error.type); + Assert.areEqual(3, error.line); + Assert.areEqual(1, error.col); + }, + + "Test parsing invalid property": function() { + var error; + var parser = new Parser(); + parser.addListener("error", function(e) { + error = e; + }); + parser.parse("a:hover{\ncolor:red;\nfont::Helvetica;/*dropped*/;\nborder:0\n}"); + + Assert.areEqual("error", error.type); + Assert.areEqual(3, error.line); + Assert.areEqual(6, error.col); + }, + + "Test parsing invalid selector": function() { var error; var parser = new Parser(); parser.addListener("error", function(e) { diff --git a/tests/css/TokenStream.js b/tests/css/TokenStream.js index 55a10e15..63914752 100644 --- a/tests/css/TokenStream.js +++ b/tests/css/TokenStream.js @@ -104,17 +104,25 @@ var YUITest = require("yuitest"), name: "Test for identifiers", patterns: { - "a": [CSSTokens.IDENT], - "ab": [CSSTokens.IDENT], - "a1": [CSSTokens.IDENT], - "a_c": [CSSTokens.IDENT], - "a-c": [CSSTokens.IDENT], - "a90": [CSSTokens.IDENT], - "a\\09": [CSSTokens.IDENT], - "\\sa": [CSSTokens.IDENT], + "a": [CSSTokens.IDENT], + "ab": [CSSTokens.IDENT], + "a1": [CSSTokens.IDENT], + "a_c": [CSSTokens.IDENT], + "a-c": [CSSTokens.IDENT], + "a90": [CSSTokens.IDENT], + "a\\09": [CSSTokens.IDENT], + "\\sa": [CSSTokens.IDENT], + "-foo": [CSSTokens.IDENT], + "flex": [CSSTokens.IDENT], + "-webkit-flex": [CSSTokens.IDENT], //not identifiers - "9a": [CSSTokens.DIMENSION] + "9a": [CSSTokens.DIMENSION], + "a+boo": [CSSTokens.IDENT, CSSTokens.PLUS, CSSTokens.IDENT], + + // existing parsing bugs + // "u+boo": [CSSTokens.IDENT, CSSTokens.PLUS, CSSTokens.IDENT], + // "u+@": [CSSTokens.IDENT, CSSTokens.PLUS, CSSTokens.CHAR], } })); @@ -252,6 +260,7 @@ var YUITest = require("yuitest"), name : "Tests for Unicode ranges", patterns: { + "u+A5" : [CSSTokens.UNICODE_RANGE], "U+A5" : [CSSTokens.UNICODE_RANGE], "U+0-7F" : [CSSTokens.UNICODE_RANGE], "U+590-5ff" : [CSSTokens.UNICODE_RANGE], @@ -263,8 +272,16 @@ var YUITest = require("yuitest"), "U+0??????" : [CSSTokens.UNICODE_RANGE, CSSTokens.CHAR], "U+00-??" : [CSSTokens.UNICODE_RANGE, CSSTokens.MINUS, CSSTokens.CHAR, CSSTokens.CHAR], "U+?1" : [CSSTokens.UNICODE_RANGE, CSSTokens.NUMBER], - "U+" : [CSSTokens.CHAR, CSSTokens.PLUS], - "U+00-J" : [CSSTokens.UNICODE_RANGE, CSSTokens.IDENT] + "U+00-J" : [CSSTokens.UNICODE_RANGE, CSSTokens.IDENT], + + // Not unicode ranges + "U20" : [CSSTokens.IDENT], + + // existing parsing failures + // "u+" : [CSSTokens.IDENT, CSSTokens.PLUS], + // "U+" : [CSSTokens.IDENT, CSSTokens.PLUS], + // "U+@" : [CSSTokens.IDENT, CSSTokens.PLUS, CSSTokens.CHAR], + // "U+U" : [CSSTokens.IDENT, CSSTokens.PLUS, CSSTokens.IDENT], } })); @@ -390,10 +407,12 @@ var YUITest = require("yuitest"), "1" : [CSSTokens.NUMBER], "20.0" : [CSSTokens.NUMBER], ".3" : [CSSTokens.NUMBER], + "-0.3" : [CSSTokens.NUMBER], + "+0" : [CSSTokens.NUMBER], + "-.3" : [CSSTokens.NUMBER], + "+.5" : [CSSTokens.NUMBER], //invalid numbers - "-.3" : [CSSTokens.MINUS, CSSTokens.NUMBER], - "+0" : [CSSTokens.PLUS, CSSTokens.NUMBER], "-name" : [CSSTokens.IDENT], "+name" : [CSSTokens.PLUS, CSSTokens.IDENT] @@ -434,7 +453,7 @@ var YUITest = require("yuitest"), "rgb(255,0,1)" : [CSSTokens.FUNCTION, CSSTokens.NUMBER, CSSTokens.COMMA, CSSTokens.NUMBER, CSSTokens.COMMA, CSSTokens.NUMBER, CSSTokens.RPAREN], "counter(par-num,upper-roman)" : [CSSTokens.FUNCTION, CSSTokens.IDENT, CSSTokens.COMMA, CSSTokens.IDENT, CSSTokens.RPAREN], "calc(100% - 5px)" : [CSSTokens.FUNCTION, CSSTokens.PERCENTAGE, CSSTokens.S, CSSTokens.MINUS, CSSTokens.S, CSSTokens.LENGTH, CSSTokens.RPAREN], - "calc((5em - 100%) / -2)" : [CSSTokens.FUNCTION, CSSTokens.LPAREN, CSSTokens.LENGTH, CSSTokens.S, CSSTokens.MINUS, CSSTokens.S, CSSTokens.PERCENTAGE, CSSTokens.RPAREN, CSSTokens.S, CSSTokens.SLASH, CSSTokens.S, CSSTokens.MINUS, CSSTokens.NUMBER, CSSTokens.RPAREN], + "calc((5em - 100%) / -2)" : [CSSTokens.FUNCTION, CSSTokens.LPAREN, CSSTokens.LENGTH, CSSTokens.S, CSSTokens.MINUS, CSSTokens.S, CSSTokens.PERCENTAGE, CSSTokens.RPAREN, CSSTokens.S, CSSTokens.SLASH, CSSTokens.S, CSSTokens.NUMBER, CSSTokens.RPAREN], //old-style IE filters - interpreted as bunch of tokens "alpha(opacity=50)" : [CSSTokens.FUNCTION, CSSTokens.IDENT, CSSTokens.EQUALS, CSSTokens.NUMBER, CSSTokens.RPAREN], diff --git a/tests/css/Validation.js b/tests/css/Validation.js index cd17488c..d60054d0 100644 --- a/tests/css/Validation.js +++ b/tests/css/Validation.js @@ -191,7 +191,7 @@ var YUITest = require("yuitest"), }, error: { - "-0num": "Unexpected token '0num' at line 1, col 24." + "-0num": "Unexpected token '-0num' at line 1, col 23." } })); @@ -755,7 +755,7 @@ var YUITest = require("yuitest"), "red", "#f00", "transparent", - "currentColor" + "currentColor", ], invalid: { @@ -988,7 +988,7 @@ var YUITest = require("yuitest"), error: { "47Futura, sans-serif" : "Unexpected token '47Futura' at line 1, col 20.", - "-7Futura, sans-serif" : "Unexpected token '7Futura' at line 1, col 21.", + "-7Futura, sans-serif" : "Unexpected token '-7Futura' at line 1, col 20.", "Ahem!, sans-serif" : "Expected RBRACE at line 1, col 24.", "test@foo, sans-serif" : "Expected RBRACE at line 1, col 24.", "#POUND, sans-serif" : "Expected a hex color but found '#POUND' at line 1, col 20."