diff --git a/lib/ace/mode/css/csslint.js b/lib/ace/mode/css/csslint.js index e7d512c9..2c80f22c 100644 --- a/lib/ace/mode/css/csslint.js +++ b/lib/ace/mode/css/csslint.js @@ -22,8 +22,9 @@ THE SOFTWARE. */ define(function(require, exports, module) { -/* -Copyright (c) 2009 Nicholas C. Zakas. All rights reserved. +/*! +Parser-Lib +Copyright (c) 2009-2011 Nicholas C. Zakas. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -44,10 +45,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/* Build time: 13-July-2011 04:35:28 */ var parserlib = {}; (function(){ - /** * A generic base to inherit from for any object * that needs event handling. @@ -143,7 +144,7 @@ EventTarget.prototype = { * @param {String} text The text to read. */ function StringReader(text){ - + /** * The input text with line endings normalized. * @property _input @@ -151,8 +152,8 @@ function StringReader(text){ * @private */ this._input = text.replace(/\n\r?/g, "\n"); - - + + /** * The row for the character to be read next. * @property _line @@ -160,8 +161,8 @@ function StringReader(text){ * @private */ this._line = 1; - - + + /** * The column for the character to be read next. * @property _col @@ -169,13 +170,13 @@ function StringReader(text){ * @private */ this._col = 1; - + /** * The index of the character in the input to be read next. * @property _cursor * @type int * @private - */ + */ this._cursor = 0; } @@ -183,11 +184,11 @@ StringReader.prototype = { //restore constructor constructor: StringReader, - + //------------------------------------------------------------------------- // Position info //------------------------------------------------------------------------- - + /** * Returns the column of the character to be read next. * @return {int} The column of the character to be read next. @@ -196,29 +197,29 @@ StringReader.prototype = { getCol: function(){ return this._col; }, - + /** * Returns the row of the character to be read next. * @return {int} The row of the character to be read next. * @method getLine - */ + */ getLine: function(){ return this._line ; }, - + /** * Determines if you're at the end of the input. * @return {Boolean} True if there's no more input, false otherwise. * @method eof - */ + */ eof: function(){ - return (this._cursor == this._input.length) + return (this._cursor == this._input.length); }, - + //------------------------------------------------------------------------- // Basic reading //------------------------------------------------------------------------- - + /** * Reads the next character without advancing the cursor. * @param {int} count How many characters to look ahead (default is 1). @@ -228,17 +229,17 @@ StringReader.prototype = { peek: function(count){ var c = null; count = (typeof count == "undefined" ? 1 : count); - + //if we're not at the end of the input... - if (this._cursor < this._input.length){ - + if (this._cursor < this._input.length){ + //get character and increment cursor and column c = this._input.charAt(this._cursor + count - 1); } - + return c; - }, - + }, + /** * Reads the next character from the input and adjusts the row and column * accordingly. @@ -247,10 +248,10 @@ StringReader.prototype = { */ read: function(){ var c = null; - + //if we're not at the end of the input... if (this._cursor < this._input.length){ - + //if the last character was a newline, increment row count //and reset column count if (this._input.charAt(this._cursor) == "\n"){ @@ -259,18 +260,18 @@ StringReader.prototype = { } else { this._col++; } - + //get character and increment cursor and column c = this._input.charAt(this._cursor++); } - + return c; - }, - + }, + //------------------------------------------------------------------------- // Misc //------------------------------------------------------------------------- - + /** * Saves the current location so it can be returned to later. * @method mark @@ -283,7 +284,7 @@ StringReader.prototype = { col: this._col }; }, - + reset: function(){ if (this._bookmark){ this._cursor = this._bookmark.cursor; @@ -292,11 +293,11 @@ StringReader.prototype = { delete this._bookmark; } }, - + //------------------------------------------------------------------------- // Advanced reading //------------------------------------------------------------------------- - + /** * Reads up to and including the given string. Throws an error if that * string is not found. @@ -304,9 +305,9 @@ StringReader.prototype = { * @return {String} The string when it is found. * @throws Error when the string pattern is not found. * @method readTo - */ + */ readTo: function(pattern){ - + var buffer = "", c; @@ -323,11 +324,11 @@ StringReader.prototype = { throw new Error("Expected \"" + pattern + "\" at line " + this._line + ", col " + this._col + "."); } } - + return buffer; - + }, - + /** * Reads characters while each character causes the given * filter function to return true. The function is passed @@ -337,21 +338,21 @@ StringReader.prototype = { * @return {String} The string made up of all characters that passed the * filter check. * @method readWhile - */ + */ readWhile: function(filter){ - + var buffer = "", c = this.read(); - + while(c !== null && filter(c)){ buffer += c; c = this.read(); } - + return buffer; - + }, - + /** * Reads characters that match either text or a regular expression and * returns those characters. If a match is found, the row and column @@ -363,41 +364,41 @@ StringReader.prototype = { * @return {String} The string made up of all characters that matched or * null if there was no match. * @method readMatch - */ + */ readMatch: function(matcher){ - + var source = this._input.substring(this._cursor), value = null; - + //if it's a string, just do a straight match if (typeof matcher == "string"){ if (source.indexOf(matcher) === 0){ - value = this.readCount(matcher.length); + value = this.readCount(matcher.length); } } else if (matcher instanceof RegExp){ if (matcher.test(source)){ value = this.readCount(RegExp.lastMatch.length); } } - - return value; + + return value; }, - - + + /** * Reads a given number of characters. If the end of the input is reached, * it reads only the remaining characters and does not throw an error. * @param {int} count The number of characters to read. * @return {String} The string made up the read characters. * @method readCount - */ + */ readCount: function(count){ var buffer = ""; - + while(count--){ buffer += this.read(); } - + return buffer; } @@ -575,7 +576,7 @@ function TokenStreamBase(input, tokenData){ */ TokenStreamBase.createTokenData = function(tokens){ - var nameMap = [], + var nameMap = [], typeMap = {}, tokenData = tokens.concat([]), i = 0, @@ -669,7 +670,7 @@ TokenStreamBase.prototype = { if (!this.match.apply(this, arguments)){ token = this.LT(1); throw new SyntaxError("Expected " + this._tokenData[tokenTypes[0]].name + - " at line " + token.startLine + ", character " + token.startCol + ".", token.startLine, token.startCol); + " at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol); } }, @@ -910,7 +911,6 @@ TokenStreamBase.prototype = { - parserlib.util = { StringReader: StringReader, SyntaxError : SyntaxError, @@ -920,9 +920,9 @@ TokenStreamBase : TokenStreamBase }; })(); - /* -Copyright (c) 2009 Nicholas C. Zakas. All rights reserved. +Parser-Lib +Copyright (c) 2009-2011 Nicholas C. Zakas. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -943,6 +943,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/* Build time: 13-July-2011 04:35:28 */ (function(){ var EventTarget = parserlib.util.EventTarget, TokenStreamBase = parserlib.util.TokenStreamBase, @@ -950,7 +951,6 @@ StringReader = parserlib.util.StringReader, SyntaxError = parserlib.util.SyntaxError, SyntaxUnit = parserlib.util.SyntaxUnit; - var Colors = { aliceblue :"#f0f8ff", antiquewhite :"#faebd7", @@ -1425,7 +1425,7 @@ Parser.prototype = function(){ * : [ CHARSET_SYM S* STRING S* ';' ]? * [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* * [ namespace [S|CDO|CDC]* ]* - * [ [ ruleset | media | page | font_face ] [S|CDO|CDC]* ]* + * [ [ ruleset | media | page | font_face | keyframes ] [S|CDO|CDC]* ]* * ; */ @@ -1474,6 +1474,10 @@ Parser.prototype = function(){ this._font_face(); this._skipCruft(); break; + case Tokens.KEYFRAMES_SYM: + this._keyframes(); + this._skipCruft(); + break; case Tokens.S: this._readWhitespace(); break; @@ -1526,8 +1530,16 @@ Parser.prototype = function(){ }, _charset: function(emit){ - var tokenStream = this._tokenStream; + var tokenStream = this._tokenStream, + charset, + token, + line, + col; + if (tokenStream.match(Tokens.CHARSET_SYM)){ + line = tokenStream.token().startLine; + col = tokenStream.token().startCol; + this._readWhitespace(); tokenStream.mustMatch(Tokens.STRING); @@ -1540,7 +1552,9 @@ Parser.prototype = function(){ if (emit !== false){ this.fire({ type: "charset", - charset:charset + charset:charset, + line: line, + col: col }); } } @@ -1556,10 +1570,12 @@ Parser.prototype = function(){ var tokenStream = this._tokenStream, tt, uri, + importToken, mediaList = []; //read import symbol tokenStream.mustMatch(Tokens.IMPORT_SYM); + importToken = tokenStream.token(); this._readWhitespace(); tokenStream.mustMatch([Tokens.STRING, Tokens.URI]); @@ -1579,7 +1595,9 @@ Parser.prototype = function(){ this.fire({ type: "import", uri: uri, - media: mediaList + media: mediaList, + line: importToken.startLine, + col: importToken.startCol }); } @@ -1592,11 +1610,15 @@ Parser.prototype = function(){ */ var tokenStream = this._tokenStream, + line, + col, prefix, uri; //read import symbol tokenStream.mustMatch(Tokens.NAMESPACE_SYM); + line = tokenStream.token().startLine; + col = tokenStream.token().startCol; this._readWhitespace(); //it's a namespace prefix - no _namespace_prefix() method because it's just an IDENT @@ -1623,7 +1645,9 @@ Parser.prototype = function(){ this.fire({ type: "namespace", prefix: prefix, - uri: uri + uri: uri, + line: line, + col: col }); } @@ -1636,10 +1660,15 @@ Parser.prototype = function(){ * ; */ var tokenStream = this._tokenStream, + line, + col, mediaList;// = []; //look for @media tokenStream.mustMatch(Tokens.MEDIA_SYM); + line = tokenStream.token().startLine; + col = tokenStream.token().startCol; + this._readWhitespace(); mediaList = this._media_query_list(); @@ -1649,17 +1678,27 @@ Parser.prototype = function(){ this.fire({ type: "startmedia", - media: mediaList + media: mediaList, + line: line, + col: col }); - while(this._ruleset()){} + while(true) { + if (tokenStream.peek() == Tokens.PAGE_SYM){ + this._page(); + } else if (!this._ruleset()){ + break; + } + } tokenStream.mustMatch(Tokens.RBRACE); this._readWhitespace(); this.fire({ type: "endmedia", - media: mediaList + media: mediaList, + line: line, + col: col }); }, @@ -1677,8 +1716,8 @@ Parser.prototype = function(){ this._readWhitespace(); - if (tokenStream.peek() == Tokens.IDENT){ - mediaList.push(this._media_query()) + if (tokenStream.peek() == Tokens.IDENT || tokenStream.peek() == Tokens.LPAREN){ + mediaList.push(this._media_query()); } while(tokenStream.match(Tokens.COMMA)){ @@ -1819,11 +1858,16 @@ Parser.prototype = function(){ * ; */ var tokenStream = this._tokenStream, + line, + col, identifier = null, pseudoPage = null; //look for @page tokenStream.mustMatch(Tokens.PAGE_SYM); + line = tokenStream.token().startLine; + col = tokenStream.token().startCol; + this._readWhitespace(); if (tokenStream.match(Tokens.IDENT)){ @@ -1845,7 +1889,9 @@ Parser.prototype = function(){ this.fire({ type: "startpage", id: identifier, - pseudo: pseudoPage + pseudo: pseudoPage, + line: line, + col: col }); this._readDeclarations(true, true); @@ -1853,7 +1899,9 @@ Parser.prototype = function(){ this.fire({ type: "endpage", id: identifier, - pseudo: pseudoPage + pseudo: pseudoPage, + line: line, + col: col }); }, @@ -1866,19 +1914,28 @@ Parser.prototype = function(){ * ; */ var tokenStream = this._tokenStream, + line, + col, marginSym = this._margin_sym(); if (marginSym){ + line = tokenStream.token().startLine; + col = tokenStream.token().startCol; + this.fire({ type: "startpagemargin", - margin: marginSym + margin: marginSym, + line: line, + col: col }); this._readDeclarations(true); this.fire({ type: "endpagemargin", - margin: marginSym + margin: marginSym, + line: line, + col: col }); return true; } else { @@ -1951,20 +2008,29 @@ Parser.prototype = function(){ * '{' S* declaration [ ';' S* declaration ]* '}' S* * ; */ - var tokenStream = this._tokenStream; + var tokenStream = this._tokenStream, + line, + col; //look for @page tokenStream.mustMatch(Tokens.FONT_FACE_SYM); + line = tokenStream.token().startLine; + col = tokenStream.token().startCol; + this._readWhitespace(); this.fire({ - type: "startfontface" + type: "startfontface", + line: line, + col: col }); this._readDeclarations(true); this.fire({ - type: "endfontface" + type: "endfontface", + line: line, + col: col }); }, @@ -2077,6 +2143,7 @@ Parser.prototype = function(){ */ var tokenStream = this._tokenStream, + tt, selectors; @@ -2121,14 +2188,18 @@ Parser.prototype = function(){ this.fire({ type: "startrule", - selectors: selectors + selectors: selectors, + line: selectors[0].line, + col: selectors[0].col }); this._readDeclarations(true); this.fire({ type: "endrule", - selectors: selectors + selectors: selectors, + line: selectors[0].line, + col: selectors[0].col }); } @@ -2584,7 +2655,7 @@ Parser.prototype = function(){ while(tokenStream.match([Tokens.PLUS, Tokens.MINUS, Tokens.DIMENSION, Tokens.NUMBER, Tokens.STRING, Tokens.IDENT, Tokens.LENGTH, - Tokens.FREQ, Tokens.EMS, Tokens.EXS, Tokens.ANGLE, Tokens.TIME, + Tokens.FREQ, Tokens.ANGLE, Tokens.TIME, Tokens.RESOLUTION])){ value += tokenStream.token().value; @@ -2697,7 +2768,7 @@ Parser.prototype = function(){ property = this._property(); if (property !== null){ - + tokenStream.mustMatch(Tokens.COLON); this._readWhitespace(); @@ -2714,7 +2785,9 @@ Parser.prototype = function(){ type: "property", property: property, value: expr, - important: prio + important: prio, + line: property.line, + col: property.col }); return true; @@ -2790,7 +2863,7 @@ Parser.prototype = function(){ /* * term * : unary_operator? - * [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* | + * [ NUMBER S* | PERCENTAGE S* | LENGTH S* | ANGLE S* | * TIME S* | FREQ S* | function | ie_function ] * | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor * ; @@ -2820,7 +2893,7 @@ Parser.prototype = function(){ //see if there's a simple match } else if (tokenStream.match([Tokens.NUMBER, Tokens.PERCENTAGE, Tokens.LENGTH, - Tokens.EMS, Tokens.EXS, Tokens.ANGLE, Tokens.TIME, + Tokens.ANGLE, Tokens.TIME, Tokens.FREQ, Tokens.STRING, Tokens.IDENT, Tokens.URI, Tokens.UNICODE_RANGE])){ value = tokenStream.token().value; @@ -2893,7 +2966,7 @@ Parser.prototype = function(){ expr = this._expr(); tokenStream.match(Tokens.RPAREN); - functionText += expr + ")" + functionText += expr + ")"; this._readWhitespace(); } @@ -2944,7 +3017,7 @@ Parser.prototype = function(){ } while(tokenStream.match([Tokens.COMMA, Tokens.S])); tokenStream.match(Tokens.RPAREN); - functionText += ")" + functionText += ")"; this._readWhitespace(); } @@ -2973,7 +3046,7 @@ Parser.prototype = function(){ token = tokenStream.token(); color = token.value; if (!/#[a-f0-9]{3,6}/i.test(color)){ - throw new SyntaxError("Expected a hex color but found '" + color + "' at line " + token.startLine + ", character " + token.startCol + ".", token.startLine, token.startCol); + throw new SyntaxError("Expected a hex color but found '" + color + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol); } this._readWhitespace(); } @@ -2981,6 +3054,158 @@ Parser.prototype = function(){ return color; }, + //----------------------------------------------------------------- + // Animations methods + //----------------------------------------------------------------- + + _keyframes: function(){ + + /* + * keyframes: + * : KEYFRAMES_SYM S* keyframe_name S* '{' S* keyframe_rule* '}' { + * ; + */ + var tokenStream = this._tokenStream, + token, + tt, + name; + + tokenStream.mustMatch(Tokens.KEYFRAMES_SYM); + this._readWhitespace(); + name = this._keyframe_name(); + + this._readWhitespace(); + tokenStream.mustMatch(Tokens.LBRACE); + + this.fire({ + type: "startkeyframes", + name: name, + line: name.line, + col: name.col + }); + + this._readWhitespace(); + tt = tokenStream.peek(); + + //check for key + while(tt == Tokens.IDENT || tt == Tokens.PERCENTAGE) { + this._keyframe_rule(); + this._readWhitespace(); + tt = tokenStream.peek(); + } + + this.fire({ + type: "endkeyframes", + name: name, + line: name.line, + col: name.col + }); + + this._readWhitespace(); + tokenStream.mustMatch(Tokens.RBRACE); + + }, + + _keyframe_name: function(){ + + /* + * keyframe_name: + * : IDENT + * | STRING + * ; + */ + var tokenStream = this._tokenStream, + token; + + tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]); + return SyntaxUnit.fromToken(tokenStream.token()); + }, + + _keyframe_rule: function(){ + + /* + * keyframe_rule: + * : key_list S* + * '{' S* declaration [ ';' S* declaration ]* '}' S* + * ; + */ + var tokenStream = this._tokenStream, + token, + keyList = this._key_list(); + + this.fire({ + type: "startkeyframerule", + keys: keyList, + line: keyList[0].line, + col: keyList[0].col + }); + + this._readDeclarations(true); + + this.fire({ + type: "endkeyframerule", + keys: keyList, + line: keyList[0].line, + col: keyList[0].col + }); + + }, + + _key_list: function(){ + + /* + * key_list: + * : key [ S* ',' S* key]* + * ; + */ + var tokenStream = this._tokenStream, + token, + key, + keyList = []; + + //must be least one key + keyList.push(this._key()); + + this._readWhitespace(); + + while(tokenStream.match(Tokens.COMMA)){ + this._readWhitespace(); + keyList.push(this._key()); + this._readWhitespace(); + } + + return keyList; + }, + + _key: function(){ + /* + * There is a restriction that IDENT can be only "from" or "to". + * + * key + * : PERCENTAGE + * | IDENT + * ; + */ + + var tokenStream = this._tokenStream, + token; + + if (tokenStream.match(Tokens.PERCENTAGE)){ + return SyntaxUnit.fromToken(tokenStream.token()); + } else if (tokenStream.match(Tokens.IDENT)){ + token = tokenStream.token(); + + if (/from|to/i.test(token.value)){ + return SyntaxUnit.fromToken(token); + } + + tokenStream.unget(); + } + + //if it gets here, there wasn't a valid token, so time to explode + this._unexpectedToken(tokenStream.LT(1)); + }, + //----------------------------------------------------------------- // Helper methods //----------------------------------------------------------------- @@ -3117,7 +3342,7 @@ Parser.prototype = function(){ * @private */ _unexpectedToken: function(token){ - throw new SyntaxError("Unexpected token '" + token.value + "' at line " + token.startLine + ", char " + token.startCol + ".", token.startLine, token.startCol); + throw new SyntaxError("Unexpected token '" + token.value + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol); }, /** @@ -3556,7 +3781,7 @@ SelectorSubPart.prototype.constructor = SelectorSubPart; - + var h = /^[0-9a-fA-F]$/, nonascii = /^[\u0080-\uFFFF]$/, nl = /\n|\r\n|\r|\f/; @@ -3564,8 +3789,8 @@ var h = /^[0-9a-fA-F]$/, //----------------------------------------------------------------------------- // Helper functions //----------------------------------------------------------------------------- - - + + function isHexDigit(c){ return c != null && h.test(c); } @@ -3587,11 +3812,11 @@ function isNameStart(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) || c == "-"); + return c != null && (isNameStart(c) || /\-\\/.test(c)); } function mix(receiver, supplier){ @@ -3631,19 +3856,19 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @private */ _getToken: function(channel){ - + var c, reader = this._reader, token = null, startLine = reader.getLine(), startCol = reader.getCol(); - + c = reader.read(); - + while(c){ switch(c){ - + /* * Potential tokens: * - COMMENT @@ -3657,8 +3882,8 @@ TokenStream.prototype = mix(new TokenStreamBase(), { } else { token = this.charToken(c, startLine, startCol); } - break; - + break; + /* * Potential tokens: * - DASHMATCH @@ -3678,8 +3903,8 @@ TokenStream.prototype = mix(new TokenStreamBase(), { } else { token = this.charToken(c, startLine, startCol); } - break; - + break; + /* * Potential tokens: * - STRING @@ -3687,9 +3912,9 @@ TokenStream.prototype = mix(new TokenStreamBase(), { */ case "\"": case "'": - token = this.stringToken(c, startLine, startCol); + token = this.stringToken(c, startLine, startCol); break; - + /* * Potential tokens: * - HASH @@ -3697,12 +3922,12 @@ TokenStream.prototype = mix(new TokenStreamBase(), { */ case "#": if (isNameChar(reader.peek())){ - token = this.hashToken(c, startLine, startCol); + token = this.hashToken(c, startLine, startCol); } else { token = this.charToken(c, startLine, startCol); - } + } break; - + /* * Potential tokens: * - DOT @@ -3712,12 +3937,12 @@ TokenStream.prototype = mix(new TokenStreamBase(), { */ case ".": if (isDigit(reader.peek())){ - token = this.numberToken(c, startLine, startCol); + token = this.numberToken(c, startLine, startCol); } else { token = this.charToken(c, startLine, startCol); } - break; - + break; + /* * Potential tokens: * - CDC @@ -3735,7 +3960,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { token = this.charToken(c, startLine, startCol); } break; - + /* * Potential tokens: * - IMPORTANT_SYM @@ -3744,14 +3969,14 @@ TokenStream.prototype = mix(new TokenStreamBase(), { case "!": token = this.importantToken(c, startLine, startCol); break; - + /* * Any at-keyword or CHAR */ case "@": token = this.atRuleToken(c, startLine, startCol); break; - + /* * Potential tokens: * - NOT @@ -3759,8 +3984,8 @@ TokenStream.prototype = mix(new TokenStreamBase(), { */ case ":": token = this.notToken(c, startLine, startCol); - break; - + break; + /* * Potential tokens: * - CDO @@ -3768,7 +3993,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { */ case "<": token = this.htmlCommentStartToken(c, startLine, startCol); - break; + break; /* * Potential tokens: @@ -3781,11 +4006,11 @@ TokenStream.prototype = mix(new TokenStreamBase(), { if (reader.peek() == "+"){ token = this.unicodeRangeToken(c, startLine, startCol); break; - } + } /*falls through*/ - + default: - + /* * Potential tokens: * - NUMBER @@ -3799,58 +4024,58 @@ TokenStream.prototype = mix(new TokenStreamBase(), { */ if (isDigit(c)){ token = this.numberToken(c, startLine, startCol); - } else - + } else + /* * Potential tokens: * - S */ if (isWhitespace(c)){ token = this.whitespaceToken(c, startLine, startCol); - } else - + } else + /* * Potential tokens: * - IDENT - */ + */ if (isIdentStart(c)){ token = this.identOrFunctionToken(c, startLine, startCol); - } else - + } else + /* * Potential tokens: * - CHAR * - PLUS */ { - token = this.charToken(c, startLine, startCol); + token = this.charToken(c, startLine, startCol); } - - - - - - + + + + + + } - + //make sure this token is wanted //TODO: check channel break; - + c = reader.read(); } - + if (!token && c == null){ token = this.createToken(Tokens.EOF,null,startLine,startCol); } - + return token; }, - + //------------------------------------------------------------------------- // Methods to create tokens //------------------------------------------------------------------------- - + /** * Produces a token based on available data and the current * reader position information. This method is called by other @@ -3865,11 +4090,11 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * be hidden. * @return {Object} A token object. * @method createToken - */ + */ createToken: function(tt, value, startLine, startCol, options){ var reader = this._reader; options = options || {}; - + return { value: value, type: tt, @@ -3878,14 +4103,14 @@ TokenStream.prototype = mix(new TokenStreamBase(), { startLine: startLine, startCol: startCol, endLine: reader.getLine(), - endCol: reader.getCol() - }; - }, - + endCol: reader.getCol() + }; + }, + //------------------------------------------------------------------------- // Methods to create specific tokens - //------------------------------------------------------------------------- - + //------------------------------------------------------------------------- + /** * Produces a token for any at-rule. If the at-rule is unknown, then * the token is for a single "@" character. @@ -3894,15 +4119,15 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method atRuleToken - */ + */ atRuleToken: function(first, startLine, startCol){ var rule = first, reader = this._reader, tt = Tokens.CHAR, valid = false, ident, - c; - + c; + /* * First, mark where we are. There are only four @ rules, * so anything else is really just an invalid token. @@ -3911,22 +4136,22 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * parsing to continue after that point. */ reader.mark(); - - //try to find the at-keyword + + //try to find the at-keyword ident = this.readName(); rule = first + ident; tt = Tokens.type(rule.toLowerCase()); - + //if it's not valid, use the first character only and reset the reader if (tt == Tokens.CHAR || tt == Tokens.UNKNOWN){ tt = Tokens.CHAR; rule = first; reader.reset(); - } - - return this.createToken(tt, rule, startLine, startCol); - }, - + } + + return this.createToken(tt, rule, startLine, startCol); + }, + /** * Produces a character token based on the given character * and location in the stream. If there's a special (non-standard) @@ -3943,10 +4168,10 @@ TokenStream.prototype = mix(new TokenStreamBase(), { if (tt == -1){ tt = Tokens.CHAR; } - + return this.createToken(tt, c, startLine, startCol); - }, - + }, + /** * Produces a character token based on the given character * and location in the stream. If there's a special (non-standard) @@ -3956,14 +4181,14 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method commentToken - */ + */ commentToken: function(first, startLine, startCol){ var reader = this._reader, comment = this.readComment(first); - return this.createToken(Tokens.COMMENT, comment, startLine, startCol); - }, - + return this.createToken(Tokens.COMMENT, comment, startLine, startCol); + }, + /** * Produces a comparison token based on the given character * and location in the stream. The next character must be @@ -3978,10 +4203,10 @@ TokenStream.prototype = mix(new TokenStreamBase(), { var reader = this._reader, comparison = c + reader.read(), tt = Tokens.type(comparison) || Tokens.CHAR; - + return this.createToken(tt, comparison, startLine, startCol); }, - + /** * Produces a hash token based on the specified information. The * first character provided is the pound sign (#) and then this @@ -3996,9 +4221,9 @@ TokenStream.prototype = mix(new TokenStreamBase(), { var reader = this._reader, name = this.readName(first); - return this.createToken(Tokens.HASH, name, startLine, startCol); + return this.createToken(Tokens.HASH, name, startLine, startCol); }, - + /** * Produces a CDO or CHAR token based on the specified information. The * first character is provided and the rest is read by the function to determine @@ -4008,22 +4233,22 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method htmlCommentStartToken - */ + */ htmlCommentStartToken: function(first, startLine, startCol){ var reader = this._reader, text = first; - reader.mark(); + reader.mark(); text += reader.readCount(3); - + if (text == ""){ return this.createToken(Tokens.CDC, text, startLine, startCol); } else { reader.reset(); return this.charToken(first, startLine, startCol); - } - }, - + } + }, + /** * Produces an IDENT or FUNCTION token based on the specified information. The * first character is provided and the rest is read by the function to determine @@ -4058,7 +4283,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method identOrFunctionToken - */ + */ identOrFunctionToken: function(first, startLine, startCol){ var reader = this._reader, ident = this.readName(first), @@ -4070,7 +4295,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { if (ident.toLowerCase() == "url("){ tt = Tokens.URI; ident = this.readURI(ident); - + //didn't find a valid URL or there's no closing paren if (ident.toLowerCase() == "url("){ tt = Tokens.FUNCTION; @@ -4079,7 +4304,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { tt = Tokens.FUNCTION; } } else if (reader.peek() == ":"){ //might be an IE function - + //IE-specific functions always being with progid: if (ident.toLowerCase() == "progid"){ ident += reader.readTo("("); @@ -4087,9 +4312,9 @@ TokenStream.prototype = mix(new TokenStreamBase(), { } } - return this.createToken(tt, ident, startLine, startCol); + return this.createToken(tt, ident, startLine, startCol); }, - + /** * Produces an IMPORTANT_SYM or CHAR token based on the specified information. The * first character is provided and the rest is read by the function to determine @@ -4099,7 +4324,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method importantToken - */ + */ importantToken: function(first, startLine, startCol){ var reader = this._reader, important = first, @@ -4109,12 +4334,12 @@ TokenStream.prototype = mix(new TokenStreamBase(), { reader.mark(); c = reader.read(); - + while(c){ - + //there can be a comment in here if (c == "/"){ - + //if the next character isn't a star, then this isn't a valid !important token if (reader.peek() != "*"){ break; @@ -4131,24 +4356,24 @@ TokenStream.prototype = mix(new TokenStreamBase(), { if (/mportant/i.test(temp)){ important += c + temp; tt = Tokens.IMPORTANT_SYM; - + } break; //we're done } else { break; } - + c = reader.read(); } - + if (tt == Tokens.CHAR){ reader.reset(); return this.charToken(first, startLine, startCol); } else { return this.createToken(tt, important, startLine, startCol); } - - + + }, /** @@ -4160,14 +4385,14 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method notToken - */ + */ notToken: function(first, startLine, startCol){ var reader = this._reader, text = first; - reader.mark(); + reader.mark(); text += reader.readCount(4); - + if (text.toLowerCase() == ":not("){ return this.createToken(Tokens.NOT, text, startLine, startCol); } else { @@ -4186,31 +4411,27 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method numberToken - */ + */ numberToken: function(first, startLine, startCol){ var reader = this._reader, value = this.readNumber(first), ident, tt = Tokens.NUMBER, c = reader.peek(); - + if (isIdentStart(c)){ ident = this.readName(reader.read()); - value += ident; + value += ident; - if (/em/i.test(ident)){ - tt = Tokens.EMS; - } else if (/ex/i.test(ident)){ - tt = Tokens.EXS; - } else if (/px|cm|mm|in|pt|pc/i.test(ident)){ + if (/^em$|^ex$|^px$|^gd$|^rem$|^vw$|^vh$|^vm$|^ch$|^cm$|^mm$|^in$|^pt$|^pc$/i.test(ident)){ tt = Tokens.LENGTH; - } else if (/deg|rad|grad/i.test(ident)){ + } else if (/^deg|^rad$|^grad$/i.test(ident)){ tt = Tokens.ANGLE; - } else if (/ms|s/i.test(ident)){ + } else if (/^ms$|^s$/i.test(ident)){ tt = Tokens.TIME; - } else if (/hz|khz/i.test(ident)){ + } else if (/^hz$|^khz$/i.test(ident)){ tt = Tokens.FREQ; - } else if (/dpi|dpcm/i.test(ident)){ + } else if (/^dpi$|^dpcm$/i.test(ident)){ tt = Tokens.RESOLUTION; } else { tt = Tokens.DIMENSION; @@ -4220,10 +4441,10 @@ TokenStream.prototype = mix(new TokenStreamBase(), { value += reader.read(); tt = Tokens.PERCENTAGE; } - - return this.createToken(tt, value, startLine, startCol); - }, - + + return this.createToken(tt, value, startLine, startCol); + }, + /** * Produces a string token based on the given character * and location in the stream. Since strings may be indicated @@ -4236,7 +4457,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method stringToken - */ + */ stringToken: function(first, startLine, startCol){ var delim = first, string = first, @@ -4244,10 +4465,10 @@ TokenStream.prototype = mix(new TokenStreamBase(), { prev = first, tt = Tokens.STRING, c = reader.read(); - + while(c){ string += c; - + //if the delimiter is found with an escapement, we're done. if (c == delim && prev != "\\"){ break; @@ -4258,47 +4479,47 @@ TokenStream.prototype = mix(new TokenStreamBase(), { tt = Tokens.INVALID; break; } - + //save previous and get next prev = c; c = reader.read(); } - + //if c is null, that means we're out of input and the string was never closed if (c == null){ tt = Tokens.INVALID; } - - return this.createToken(tt, string, startLine, startCol); - }, - + + return this.createToken(tt, string, startLine, startCol); + }, + unicodeRangeToken: function(first, startLine, startCol){ var reader = this._reader, value = first, temp, tt = Tokens.CHAR; - + //then it should be a unicode range if (reader.peek() == "+"){ reader.mark(); value += reader.read(); value += this.readUnicodeRangePart(true); - + //ensure there's an actual unicode range here if (value.length == 2){ reader.reset(); } else { - + tt = Tokens.UNICODE_RANGE; - + //if there's a ? in the first part, there can't be a second part if (value.indexOf("?") == -1){ - + if (reader.peek() == "-"){ reader.mark(); temp = reader.read(); temp += this.readUnicodeRangePart(false); - + //if there's not another value, back up and just take the first if (temp.length == 1){ reader.reset(); @@ -4310,10 +4531,10 @@ TokenStream.prototype = mix(new TokenStreamBase(), { } } } - + return this.createToken(tt, value, startLine, startCol); }, - + /** * Produces a S token based on the specified information. Since whitespace * may have multiple characters, this consumes all whitespace characters @@ -4323,57 +4544,57 @@ TokenStream.prototype = mix(new TokenStreamBase(), { * @param {int} startCol The beginning column for the character. * @return {Object} A token object. * @method whitespaceToken - */ + */ whitespaceToken: function(first, startLine, startCol){ var reader = this._reader, value = first + this.readWhitespace(); - return this.createToken(Tokens.S, value, startLine, startCol); - }, - + return this.createToken(Tokens.S, value, startLine, startCol); + }, + //------------------------------------------------------------------------- // Methods to read values from the string stream //------------------------------------------------------------------------- - + readUnicodeRangePart: function(allowQuestionMark){ var reader = this._reader, - part = "", + part = "", c = reader.peek(); - + //first read hex digits while(isHexDigit(c) && part.length < 6){ reader.read(); part += c; - c = reader.peek(); + c = reader.peek(); } - + //then read question marks if allowed if (allowQuestionMark){ while(c == "?" && part.length < 6){ reader.read(); part += c; - c = reader.peek(); + c = reader.peek(); } } //there can't be any other characters after this point - - return part; + + return part; }, - + readWhitespace: function(){ var reader = this._reader, whitespace = "", c = reader.peek(); - + while(isWhitespace(c)){ reader.read(); whitespace += c; - c = reader.peek(); + c = reader.peek(); } - + return whitespace; }, readNumber: function(first){ @@ -4381,7 +4602,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { number = first, hasDot = (first == "."), c = reader.peek(); - + while(c){ if (isDigit(c)){ @@ -4396,23 +4617,23 @@ TokenStream.prototype = mix(new TokenStreamBase(), { } else { break; } - + c = reader.peek(); - } - + } + return number; }, readString: function(){ var reader = this._reader, delim = reader.read(), - string = delim, + string = delim, prev = delim, c = reader.peek(); - + while(c){ c = reader.read(); string += c; - + //if the delimiter is found with an escapement, we're done. if (c == delim && prev != "\\"){ break; @@ -4423,17 +4644,17 @@ TokenStream.prototype = mix(new TokenStreamBase(), { string = ""; break; } - + //save previous and get next prev = c; c = reader.peek(); } - + //if c is null, that means we're out of input and the string was never closed if (c == null){ string = ""; } - + return string; }, readURI: function(first){ @@ -4441,18 +4662,30 @@ TokenStream.prototype = mix(new TokenStreamBase(), { uri = first, inner = "", c = reader.peek(); - + reader.mark(); - + + //skip whitespace before + while(c && isWhitespace(c)){ + reader.read(); + c = reader.peek(); + } + //it's a string if (c == "'" || c == "\""){ inner = this.readString(); } else { inner = this.readURL(); } - + c = reader.peek(); - + + //skip whitespace after + while(c && isWhitespace(c)){ + reader.read(); + c = reader.peek(); + } + //if there was no inner value or the next character isn't closing paren, it's not a URI if (inner == "" || c != ")"){ uri = first; @@ -4460,64 +4693,90 @@ TokenStream.prototype = mix(new TokenStreamBase(), { } else { uri += inner + reader.read(); } - + return uri; }, readURL: function(){ var reader = this._reader, url = "", c = reader.peek(); - + //TODO: Check for escape and nonascii while (/^[!#$%&\\*-~]$/.test(c)){ url += reader.read(); c = reader.peek(); } - + return url; - + }, readName: function(first){ var reader = this._reader, ident = first || "", c = reader.peek(); - - while(c && isNameChar(c)){ - ident += reader.read(); - c = reader.peek(); + while(true){ + if (c == "\\"){ + ident += this.readEscape(reader.read()); + c = reader.peek(); + } else if(c && isNameChar(c)){ + ident += reader.read(); + c = reader.peek(); + } else { + break; + } + } + + return ident; + }, + + readEscape: function(first){ + var reader = this._reader, + cssEscape = first || "", + i = 0, + c = reader.peek(); + + if (isHexDigit(c)){ + do { + cssEscape += reader.read(); + c = reader.peek(); + } while(c && isHexDigit(c) && ++i < 6); } - return ident; - }, + if (cssEscape.length == 3 && /\s/.test(c) || + cssEscape.length == 7 || cssEscape.length == 1){ + reader.read(); + } else { + c = ""; + } + + return cssEscape + c; + }, + readComment: function(first){ var reader = this._reader, comment = first || "", c = reader.read(); - + if (c == "*"){ while(c){ comment += c; - + //look for end of comment if (c == "*" && reader.peek() == "/"){ comment += reader.read(); break; } - + c = reader.read(); } - + return comment; } else { return ""; } - - }, - - - + } }); @@ -4555,13 +4814,14 @@ var Tokens = [ { name: "CHARSET_SYM", text: "@charset"}, { name: "NAMESPACE_SYM", text: "@namespace"}, //{ name: "ATKEYWORD"}, + + //CSS3 animations + { name: "KEYFRAMES_SYM", text: [ "@keyframes", "@-webkit-keyframes", "@-moz-keyframes" ] }, //important symbol { name: "IMPORTANT_SYM"}, //measurements - { name: "EMS"}, - { name: "EXS"}, { name: "LENGTH"}, { name: "ANGLE"}, { name: "TIME"}, @@ -4704,7 +4964,13 @@ var Tokens = [ nameMap.push(Tokens[i].name); Tokens[Tokens[i].name] = i; if (Tokens[i].text){ - typeMap[Tokens[i].text] = i; + if (Tokens[i].text instanceof Array){ + for (var j=0; j < Tokens[i].text.length; j++){ + typeMap[Tokens[i].text[j]] = i; + } + } else { + typeMap[Tokens[i].text] = i; + } } } @@ -4722,7 +4988,6 @@ var Tokens = [ - parserlib.css = { Colors :Colors, Combinator :Combinator, @@ -4740,9 +5005,6 @@ Tokens :Tokens }; })(); - - - /** * Main CSSLint object. * @class CSSLint @@ -4751,13 +5013,16 @@ Tokens :Tokens */ var CSSLint = (function(){ - var rules = [], - api = new parserlib.util.EventTarget(); + var rules = [], + formatters = [], + api = new parserlib.util.EventTarget(); + + api.version = "@VERSION@"; //------------------------------------------------------------------------- // Rule Management //------------------------------------------------------------------------- - + /** * Adds a new rule to the engine. * @param {Object} rule The rule to add. @@ -4767,7 +5032,7 @@ var CSSLint = (function(){ rules.push(rule); rules[rule.id] = rule; }; - + /** * Clears all rule from the engine. * @method clearRules @@ -4776,67 +5041,120 @@ var CSSLint = (function(){ rules = []; }; + //------------------------------------------------------------------------- + // Formatters + //------------------------------------------------------------------------- + + /** + * Adds a new formatter to the engine. + * @param {Object} formatter The formatter to add. + * @method addFormatter + */ + api.addFormatter = function(formatter) { + // formatters.push(formatter); + formatters[formatter.id] = formatter; + }; + + /** + * Retrieves a formatter for use. + * @param {String} formatId The name of the format to retrieve. + * @return {Object} The formatter or undefined. + * @method getFormatter + */ + api.getFormatter = function(formatId){ + return formatters[formatId]; + }; + + /** + * Formats the results in a particular format for a single file. + * @param {Object} result The results returned from CSSLint.verify(). + * @param {String} filename The filename for which the results apply. + * @param {String} formatId The name of the formatter to use. + * @return {String} A formatted string for the results. + * @method format + */ + api.format = function(results, filename, formatId) { + var formatter = this.getFormatter(formatId), + result = null; + + if (formatter){ + result = formatter.startFormat(); + result += formatter.formatResults(results, filename); + result += formatter.endFormat(); + } + + return result; + } + + /** + * Indicates if the given format is supported. + * @param {String} formatId The ID of the format to check. + * @return {Boolean} True if the format exists, false if not. + * @method hasFormat + */ + api.hasFormat = function(formatId){ + return formatters.hasOwnProperty(formatId); + }; + //------------------------------------------------------------------------- // Verification //------------------------------------------------------------------------- - + /** * Starts the verification process for the given CSS text. * @param {String} text The CSS text to verify. - * @param {Object} options (Optional) List of rules to apply. If null, then + * @param {Object} ruleset (Optional) List of rules to apply. If null, then * all rules are used. * @return {Object} Results of the verification. * @method verify */ - api.verify = function(text, options){ - + api.verify = function(text, ruleset){ + var i = 0, len = rules.length, reporter, lines, - parser = new parserlib.css.Parser({ starHack: true, ieFilters: true, + parser = new parserlib.css.Parser({ starHack: true, ieFilters: true, underscoreHack: true, strict: false }); lines = text.split(/\n\r?/g); reporter = new Reporter(lines); - - if (!options){ + + if (!ruleset){ while (i < len){ rules[i++].init(parser, reporter); } } else { - for (i in options){ - if(options.hasOwnProperty(i)){ + ruleset.errors = 1; //always report parsing errors + for (i in ruleset){ + if(ruleset.hasOwnProperty(i)){ if (rules[i]){ rules[i].init(parser, reporter); } } } } - + //capture most horrible error type try { parser.parse(text); } catch (ex) { reporter.error("Fatal error, cannot continue: " + ex.message, ex.line, ex.col); } - + return { messages : reporter.messages, stats : reporter.stats }; }; - //------------------------------------------------------------------------- // Publish the API //------------------------------------------------------------------------- - + return api; })(); - - /** * An instance of Report is used to report results of the * verification back to the main API. @@ -4852,14 +5170,14 @@ function Reporter(lines){ * @type String[] */ this.messages = []; - + /** * List of statistics being reported. * @property stats * @type String[] */ - this.stats = []; - + this.stats = []; + /** * Lines of code being reported on. Used to provide contextual information * for messages. @@ -4873,7 +5191,7 @@ Reporter.prototype = { //restore constructor constructor: Reporter, - + /** * Report an error. * @param {String} message The message to store. @@ -4892,7 +5210,7 @@ Reporter.prototype = { rule : rule }); }, - + /** * Report an warning. * @param {String} message The message to store. @@ -4911,7 +5229,7 @@ Reporter.prototype = { rule : rule }); }, - + /** * Report some informational text. * @param {String} message The message to store. @@ -4930,7 +5248,7 @@ Reporter.prototype = { rule : rule }); }, - + /** * Report some rollup error information. * @param {String} message The message to store. @@ -4945,7 +5263,7 @@ Reporter.prototype = { rule : rule }); }, - + /** * Report some rollup warning information. * @param {String} message The message to store. @@ -4960,7 +5278,7 @@ Reporter.prototype = { rule : rule }); }, - + /** * Report a statistic. * @param {String} name The name of the stat to store. @@ -4985,13 +5303,13 @@ Reporter.prototype = { */ function mix(reciever, supplier){ var prop; - + for (prop in supplier){ if (supplier.hasOwnProperty(prop)){ receiver[prop] = supplier[prop]; } } - + return prop; } @@ -5022,8 +5340,8 @@ CSSLint.addRule({ id: "adjoining-classes", name: "Adjoining Classes", desc: "Don't use adjoining classes.", - browsers: "IE6, IE7", - + browsers: "IE6", + //initialization init: function(parser, reporter){ var rule = this; @@ -5034,26 +5352,26 @@ CSSLint.addRule({ modifier, classCount, i, j, k; - + for (i=0; i < selectors.length; i++){ selector = selectors[i]; - for (j=0; j < selector.parts.length; j++){ + for (j=0; j < selector.parts.length; j++){ part = selector.parts[j]; if (part instanceof parserlib.css.SelectorPart){ classCount = 0; for (k=0; k < part.modifiers.length; k++){ modifier = part.modifiers[k]; if (modifier.type == "class"){ - classCount++; + classCount++; } if (classCount > 1){ reporter.warn("Don't use adjoining classes.", part.line, part.col, rule); } } - } + } } } - }); + }); } }); @@ -5067,59 +5385,253 @@ CSSLint.addRule({ name: "Box Model", desc: "Don't use width or height when using padding or border.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this, - propertiesToCheck = { + widthProperties = { border: 1, "border-left": 1, "border-right": 1, - "border-bottom": 1, - "border-top": 1, padding: 1, "padding-left": 1, - "padding-right": 1, + "padding-right": 1 + }, + heightProperties = { + border: 1, + "border-bottom": 1, + "border-top": 1, + padding: 1, "padding-bottom": 1, - "padding-top": 1 + "padding-top": 1 }, properties; - - parser.addListener("startrule", function(event){ - properties = { + + parser.addListener("startrule", function(){ + properties = { }; }); - + parser.addListener("property", function(event){ - var name = event.property; + var name = event.property.text.toLowerCase(); - if (propertiesToCheck[name]){ - properties[name] = { line: name.line, col: name.col }; + if (heightProperties[name] || widthProperties[name]){ + if (!/^0\S*$/.test(event.value) && !(name == "border" && event.value == "none")){ + properties[name] = { line: event.property.line, col: event.property.col, value: event.value }; + } } else { if (name == "width" || name == "height"){ - properties._flagProperty = name.text; + properties[name] = 1; } } }); - - parser.addListener("endrule", function(event){ + + parser.addListener("endrule", function(){ var prop; - if (properties._flagProperty){ - for (prop in propertiesToCheck){ - if (propertiesToCheck.hasOwnProperty(prop) && properties[prop]){ - reporter.warn("Broken box model: using " + properties._flagProperty + " with " + prop + ".", properties[prop].line, properties[prop].col, rule); + if (properties["height"]){ + for (prop in heightProperties){ + if (heightProperties.hasOwnProperty(prop) && properties[prop]){ + + //special case for padding + if (prop == "padding" && properties[prop].value.parts.length == 2 && properties[prop].value.parts[0].value == 0){ + //noop + } else { + reporter.warn("Broken box model: using height with " + prop + ".", properties[prop].line, properties[prop].col, rule); + } } } } + + if (properties["width"]){ + for (prop in widthProperties){ + if (widthProperties.hasOwnProperty(prop) && properties[prop]){ + + if (prop == "padding" && properties[prop].value.parts.length == 2 && properties[prop].value.parts[1].value == 0){ + //noop + } else { + reporter.warn("Broken box model: using width with " + prop + ".", properties[prop].line, properties[prop].col, rule); + } + } + } + } + }); } }); +/* + * Rule: Include all compatible vendor prefixes to reach a wider + * range of users. + */ +/*global CSSLint*/ +CSSLint.addRule({ + + //rule information + id: "compatible-vendor-prefixes", + name: "Compatible Vendor Prefixes", + desc: "Include all compatible vendor prefixes to reach a wider range of users.", + browsers: "All", + + //initialization + init: function (parser, reporter) { + var rule = this, + compatiblePrefixes, + properties, + prop, + variations, + prefixed, + i, + len, + arrayPush = Array.prototype.push, + applyTo = []; + + // See http://peter.sh/experiments/vendor-prefixed-css-property-overview/ for details + compatiblePrefixes = { + "animation" : "webkit moz", + "animation-delay" : "webkit moz", + "animation-direction" : "webkit moz", + "animation-duration" : "webkit moz", + "animation-fill-mode" : "webkit moz", + "animation-iteration-count" : "webkit moz", + "animation-name" : "webkit moz", + "animation-play-state" : "webkit moz", + "animation-timing-function" : "webkit moz", + "appearance" : "webkit moz", + "border-end" : "webkit moz", + "border-end-color" : "webkit moz", + "border-end-style" : "webkit moz", + "border-end-width" : "webkit moz", + "border-image" : "webkit moz o", + "border-radius" : "webkit moz", + "border-start" : "webkit moz", + "border-start-color" : "webkit moz", + "border-start-style" : "webkit moz", + "border-start-width" : "webkit moz", + "box-align" : "webkit moz ms", + "box-direction" : "webkit moz ms", + "box-flex" : "webkit moz ms", + "box-lines" : "webkit ms", + "box-ordinal-group" : "webkit moz ms", + "box-orient" : "webkit moz ms", + "box-pack" : "webkit moz ms", + "box-sizing" : "webkit moz", + "box-shadow" : "webkit moz", + "column-count" : "webkit moz", + "column-gap" : "webkit moz", + "column-rule" : "webkit moz", + "column-rule-color" : "webkit moz", + "column-rule-style" : "webkit moz", + "column-rule-width" : "webkit moz", + "column-width" : "webkit moz", + "hyphens" : "epub moz", + "line-break" : "webkit ms", + "margin-end" : "webkit moz", + "margin-start" : "webkit moz", + "marquee-speed" : "webkit wap", + "marquee-style" : "webkit wap", + "padding-end" : "webkit moz", + "padding-start" : "webkit moz", + "tab-size" : "moz o", + "text-size-adjust" : "webkit ms", + "transform" : "webkit moz ms o", + "transform-origin" : "webkit moz ms o", + "transition" : "webkit moz o", + "transition-delay" : "webkit moz o", + "transition-duration" : "webkit moz o", + "transition-property" : "webkit moz o", + "transition-timing-function" : "webkit moz o", + "user-modify" : "webkit moz", + "user-select" : "webkit moz", + "word-break" : "epub ms", + "writing-mode" : "epub ms" + }; + + for (prop in compatiblePrefixes) { + if (compatiblePrefixes.hasOwnProperty(prop)) { + variations = []; + prefixed = compatiblePrefixes[prop].split(' '); + for (i = 0, len = prefixed.length; i < len; i++) { + variations.push('-' + prefixed[i] + '-' + prop); + } + compatiblePrefixes[prop] = variations; + arrayPush.apply(applyTo, variations); + } + } + parser.addListener("startrule", function () { + properties = []; + }); + + parser.addListener("property", function (event) { + var name = event.property.text; + if (applyTo.indexOf(name) > -1) { + properties.push(name); + } + }); + + parser.addListener("endrule", function (event) { + if (!properties.length) { + return; + } + + var propertyGroups = {}, + i, + len, + name, + prop, + variations, + value, + full, + actual, + item, + propertiesSpecified; + + for (i = 0, len = properties.length; i < len; i++) { + name = properties[i]; + + for (prop in compatiblePrefixes) { + if (compatiblePrefixes.hasOwnProperty(prop)) { + variations = compatiblePrefixes[prop]; + if (variations.indexOf(name) > -1) { + if (propertyGroups[prop] === undefined) { + propertyGroups[prop] = { + full : variations.slice(0), + actual : [] + }; + } + if (propertyGroups[prop].actual.indexOf(name) === -1) { + propertyGroups[prop].actual.push(name); + } + } + } + } + } + + for (prop in propertyGroups) { + if (propertyGroups.hasOwnProperty(prop)) { + value = propertyGroups[prop]; + full = value.full; + actual = value.actual; + + if (full.length > actual.length) { + for (i = 0, len = full.length; i < len; i++) { + item = full[i]; + if (actual.indexOf(item) === -1) { + propertiesSpecified = (actual.length === 1) ? actual[0] : (actual.length == 2) ? actual.join(" and ") : actual.join(", "); + reporter.warn("The property " + item + " is compatible with " + propertiesSpecified + " and should be included as well.", event.selectors[0].line, event.selectors[0].col, rule); + } + } + + } + } + } + }); + } +}); /* * Rule: Certain properties don't play well with certain display values. * - float should not be used with inline-block - * - height, width, margin, padding, float should not be used with inline + * - height, width, margin-top, margin-bottom, float should not be used with inline * - vertical-align should not be used with block * - margin, float should not be used with table-* */ @@ -5130,75 +5642,68 @@ CSSLint.addRule({ name: "Display Property Grouping", desc: "Certain properties shouldn't be used with certain display property values.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; - + var propertiesToCheck = { display: 1, - "float": 1, + "float": "none", height: 1, width: 1, margin: 1, "margin-left": 1, "margin-right": 1, "margin-bottom": 1, - "margin-top": 1, + "margin-top": 1, padding: 1, "padding-left": 1, "padding-right": 1, "padding-bottom": 1, - "padding-top": 1, + "padding-top": 1, "vertical-align": 1 }, properties; - - parser.addListener("startrule", function(event){ - properties = {}; - }); + + parser.addListener("startrule", function(){ + properties = {}; + }); parser.addListener("property", function(event){ - var name = event.property; - + var name = event.property.text.toLowerCase(); + if (propertiesToCheck[name]){ - properties[name] = { value: event.value.text, line: name.line, col: name.col }; - } - }); - - parser.addListener("endrule", function(event){ - + properties[name] = { value: event.value.text, line: event.property.line, col: event.property.col }; + } + }); + + parser.addListener("endrule", function(){ + var display = properties.display ? properties.display.value : null; if (display){ switch(display){ - + case "inline": - //height, width, margin, padding, float should not be used with inline + //height, width, margin-top, margin-bottom, float should not be used with inline reportProperty("height", display); reportProperty("width", display); reportProperty("margin", display); - reportProperty("margin-left", display); - reportProperty("margin-right", display); reportProperty("margin-top", display); - reportProperty("margin-bottom", display); - reportProperty("padding", display); - reportProperty("padding-left", display); - reportProperty("padding-right", display); - reportProperty("padding-top", display); - reportProperty("padding-bottom", display); - reportProperty("float", display); + reportProperty("margin-bottom", display); + reportProperty("float", display, "display:inline has no effect on floated elements (but may be used to fix the IE6 double-margin bug)."); break; - + case "block": //vertical-align should not be used with block reportProperty("vertical-align", display); break; - + case "inline-block": //float should not be used with inline-block reportProperty("float", display); break; - + default: //margin, float should not be used with table if (display.indexOf("table-") == 0){ @@ -5207,23 +5712,68 @@ CSSLint.addRule({ reportProperty("margin-right", display); reportProperty("margin-top", display); reportProperty("margin-bottom", display); - reportProperty("float", display); + reportProperty("float", display); } - - //otherwise do nothing + + //otherwise do nothing } } - }); - - - function reportProperty(name, display){ + }); + + + function reportProperty(name, display, msg){ if (properties[name]){ - reporter.warn(name + " can't be used with display: " + display + ".", properties[name].line, properties[name].col, rule); - } + if (!(typeof propertiesToCheck[name] == "string") || properties[name].value.toLowerCase() != propertiesToCheck[name]){ + reporter.warn(msg || name + " can't be used with display: " + display + ".", properties[name].line, properties[name].col, rule); + } + } } } +}); +/* + * Rule: Duplicate properties must appear one after the other. If an already-defined + * property appears somewhere else in the rule, then it's likely an error. + */ +CSSLint.addRule({ + + //rule information + id: "duplicate-properties", + name: "Duplicate Properties", + desc: "Duplicate properties must appear one after the other.", + browsers: "All", + + //initialization + init: function(parser, reporter){ + var rule = this, + properties, + lastProperty; + + function startRule(event){ + properties = {}; + } + + parser.addListener("startrule", startRule); + parser.addListener("startfontface", startRule); + parser.addListener("startpage", startRule); + + parser.addListener("property", function(event){ + var property = event.property, + name = property.text.toLowerCase(); + + if (properties[name] && (lastProperty != name || properties[name] == event.value.text)){ + reporter.warn("Duplicate property '" + event.property + "' found.", event.line, event.col, rule); + } + + properties[name] = event.value.text; + lastProperty = name; + + }); + + + } + }); /* * Rule: Style rules without any properties defined should be removed. @@ -5235,26 +5785,26 @@ CSSLint.addRule({ name: "Empty Rules", desc: "Rules without any properties specified should be removed.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this, - count = 0; - - parser.addListener("startrule", function(event){ + count = 0; + + parser.addListener("startrule", function(){ count=0; }); - - parser.addListener("property", function(event){ + + parser.addListener("property", function(){ count++; }); - + parser.addListener("endrule", function(event){ var selectors = event.selectors; if (count == 0){ reporter.warn("Rule is empty.", selectors[0].line, selectors[0].col, rule); } - }); + }); } }); @@ -5268,11 +5818,11 @@ CSSLint.addRule({ name: "Parsing Errors", desc: "This rule looks for recoverable syntax errors.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; - + parser.addListener("error", function(event){ reporter.error(event.message, event.line, event.col, rule); }); @@ -5291,26 +5841,27 @@ CSSLint.addRule({ name: "Floats", desc: "This rule tests if the float property is used too many times", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; var count = 0; - + //count how many times "float" is used parser.addListener("property", function(event){ - if (event.property == "float"){ + if (event.property.text.toLowerCase() == "float" && + event.value.text.toLowerCase() != "none"){ count++; } }); - + //report the results - parser.addListener("endstylesheet", function(event){ + parser.addListener("endstylesheet", function(){ reporter.stat("floats", count); if (count >= 10){ - reporter.rollupWarn("Too many floats (" + count + "), abstraction needed.", rule); + reporter.rollupWarn("Too many floats (" + count + "), you're probably using them for layout. Consider using a grid system instead.", rule); } - }); + }); } }); @@ -5324,22 +5875,22 @@ CSSLint.addRule({ name: "Font Faces", desc: "Too many different web fonts in the same stylesheet.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this, count = 0; - - - parser.addListener("startfontface", function(event){ + + + parser.addListener("startfontface", function(){ count++; }); - parser.addListener("endstylesheet", function(event){ + parser.addListener("endstylesheet", function(){ if (count > 5){ reporter.rollupWarn("Too many @font-face declarations (" + count + ").", rule); } - }); + }); } }); @@ -5354,27 +5905,26 @@ CSSLint.addRule({ name: "Font Sizes", desc: "Checks the number of font-size declarations.", browsers: "All", - + //initialization init: function(parser, reporter){ - var rule = this, + var rule = this, count = 0; - + //check for use of "font-size" parser.addListener("property", function(event){ - var part = event.value.parts[0]; if (event.property == "font-size"){ - count++; + count++; } }); - + //report the results - parser.addListener("endstylesheet", function(event){ + parser.addListener("endstylesheet", function(){ reporter.stat("font-sizes", count); if (count >= 10){ reporter.rollupWarn("Too many font-size declarations (" + count + "), abstraction needed.", rule); } - }); + }); } }); @@ -5388,13 +5938,13 @@ CSSLint.addRule({ name: "Gradients", desc: "When using a vendor-prefixed gradient, make sure to use them all.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this, gradients; - - parser.addListener("startrule", function(event){ + + parser.addListener("startrule", function(){ gradients = { moz: 0, webkit: 0, @@ -5402,37 +5952,37 @@ CSSLint.addRule({ o: 0 }; }); - + parser.addListener("property", function(event){ - + if (/\-(moz|ms|o|webkit)(?:\-(?:linear|radial))\-gradient/.test(event.value)){ gradients[RegExp.$1] = 1; } - + }); - + parser.addListener("endrule", function(event){ var missing = []; - + if (!gradients.moz){ missing.push("Firefox 3.6+"); } - + if (!gradients.webkit){ missing.push("Webkit (Safari, Chrome)"); } - + if (!gradients.ms){ missing.push("Internet Explorer 10+"); } - + if (!gradients.o){ missing.push("Opera 11.1+"); } - + if (missing.length && missing.length < 4){ reporter.warn("Missing vendor-prefixed CSS gradients for " + missing.join(", ") + ".", event.selectors[0].line, event.selectors[0].col, rule); - } + } }); @@ -5449,7 +5999,7 @@ CSSLint.addRule({ name: "IDs", desc: "Selectors should not contain IDs.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; @@ -5460,12 +6010,12 @@ CSSLint.addRule({ modifier, idCount, i, j, k; - + for (i=0; i < selectors.length; i++){ selector = selectors[i]; idCount = 0; - for (j=0; j < selector.parts.length; j++){ + for (j=0; j < selector.parts.length; j++){ part = selector.parts[j]; if (part instanceof parserlib.css.SelectorPart){ for (k=0; k < part.modifiers.length; k++){ @@ -5474,17 +6024,382 @@ CSSLint.addRule({ idCount++; } } - } + } } - - if (idCount == 1){ - reporter.warn("Don't use IDs in selectors.", selector.line, selector.col, rule); - } else if (idCount > 1){ - reporter.warn(idCount + " IDs in the selector, really?", selector.line, selector.col, rule); - } - } - }); + if (idCount == 1){ + reporter.warn("Don't use IDs in selectors.", selector.line, selector.col, rule); + } else if (idCount > 1){ + reporter.warn(idCount + " IDs in the selector, really?", selector.line, selector.col, rule); + } + } + + }); + } + +}); +/* + * Rule: Don't use @import, use instead. + */ +CSSLint.addRule({ + + //rule information + id: "import", + name: "@import", + desc: "Don't use @import, use instead.", + browsers: "All", + + //initialization + init: function(parser, reporter){ + var rule = this; + + parser.addListener("import", function(event){ + reporter.warn("@import prevents parallel downloads, use instead.", event.line, event.col, rule); + }); + + } + +}); +/* + * Rule: Make sure !important is not overused, this could lead to specificity + * war. Display a warning on !important declarations, an error if it's + * used more at least 10 times. + */ +CSSLint.addRule({ + + //rule information + id: "important", + name: "Important", + desc: "Be careful when using !important declaration", + browsers: "All", + + //initialization + init: function(parser, reporter){ + var rule = this, + count = 0; + + //warn that important is used and increment the declaration counter + parser.addListener("property", function(event){ + if (event.important === true){ + count++; + reporter.warn("Use of !important", event.line, event.col, rule); + } + }); + + //if there are more than 10, show an error + parser.addListener("endstylesheet", function(){ + reporter.stat("important", count); + if (count >= 10){ + reporter.rollupWarn("Too many !important declarations (" + count + "), try to use less than 10 to avoid specifity issues.", rule); + } + }); + } + +}); +/* + * Rule: Properties should be known (listed in CSS3 specification) or + * be a vendor-prefixed property. + */ +CSSLint.addRule({ + + //rule information + id: "known-properties", + name: "Known Properties", + desc: "Properties should be known (listed in CSS specification) or be a vendor-prefixed property.", + browsers: "All", + + //initialization + init: function(parser, reporter){ + var rule = this, + properties = { + + "alignment-adjust": 1, + "alignment-baseline": 1, + "animation": 1, + "animation-delay": 1, + "animation-direction": 1, + "animation-duration": 1, + "animation-iteration-count": 1, + "animation-name": 1, + "animation-play-state": 1, + "animation-timing-function": 1, + "appearance": 1, + "azimuth": 1, + "backface-visibility": 1, + "background": 1, + "background-attachment": 1, + "background-break": 1, + "background-clip": 1, + "background-color": 1, + "background-image": 1, + "background-origin": 1, + "background-position": 1, + "background-repeat": 1, + "background-size": 1, + "baseline-shift": 1, + "binding": 1, + "bleed": 1, + "bookmark-label": 1, + "bookmark-level": 1, + "bookmark-state": 1, + "bookmark-target": 1, + "border": 1, + "border-bottom": 1, + "border-bottom-color": 1, + "border-bottom-left-radius": 1, + "border-bottom-right-radius": 1, + "border-bottom-style": 1, + "border-bottom-width": 1, + "border-collapse": 1, + "border-color": 1, + "border-image": 1, + "border-image-outset": 1, + "border-image-repeat": 1, + "border-image-slice": 1, + "border-image-source": 1, + "border-image-width": 1, + "border-left": 1, + "border-left-color": 1, + "border-left-style": 1, + "border-left-width": 1, + "border-radius": 1, + "border-right": 1, + "border-right-color": 1, + "border-right-style": 1, + "border-right-width": 1, + "border-spacing": 1, + "border-style": 1, + "border-top": 1, + "border-top-color": 1, + "border-top-left-radius": 1, + "border-top-right-radius": 1, + "border-top-style": 1, + "border-top-width": 1, + "border-width": 1, + "bottom": 1, + "box-align": 1, + "box-decoration-break": 1, + "box-direction": 1, + "box-flex": 1, + "box-flex-group": 1, + "box-lines": 1, + "box-ordinal-group": 1, + "box-orient": 1, + "box-pack": 1, + "box-shadow": 1, + "box-sizing": 1, + "break-after": 1, + "break-before": 1, + "break-inside": 1, + "caption-side": 1, + "clear": 1, + "clip": 1, + "color": 1, + "color-profile": 1, + "column-count": 1, + "column-fill": 1, + "column-gap": 1, + "column-rule": 1, + "column-rule-color": 1, + "column-rule-style": 1, + "column-rule-width": 1, + "column-span": 1, + "column-width": 1, + "columns": 1, + "content": 1, + "counter-increment": 1, + "counter-reset": 1, + "crop": 1, + "cue": 1, + "cue-after": 1, + "cue-before": 1, + "cursor": 1, + "direction": 1, + "display": 1, + "dominant-baseline": 1, + "drop-initial-after-adjust": 1, + "drop-initial-after-align": 1, + "drop-initial-before-adjust": 1, + "drop-initial-before-align": 1, + "drop-initial-size": 1, + "drop-initial-value": 1, + "elevation": 1, + "empty-cells": 1, + "fit": 1, + "fit-position": 1, + "float": 1, + "float-offset": 1, + "font": 1, + "font-family": 1, + "font-size": 1, + "font-size-adjust": 1, + "font-stretch": 1, + "font-style": 1, + "font-variant": 1, + "font-weight": 1, + "grid-columns": 1, + "grid-rows": 1, + "hanging-punctuation": 1, + "height": 1, + "hyphenate-after": 1, + "hyphenate-before": 1, + "hyphenate-character": 1, + "hyphenate-lines": 1, + "hyphenate-resource": 1, + "hyphens": 1, + "icon": 1, + "image-orientation": 1, + "image-rendering": 1, + "image-resolution": 1, + "inline-box-align": 1, + "left": 1, + "letter-spacing": 1, + "line-height": 1, + "line-stacking": 1, + "line-stacking-ruby": 1, + "line-stacking-shift": 1, + "line-stacking-strategy": 1, + "list-style": 1, + "list-style-image": 1, + "list-style-position": 1, + "list-style-type": 1, + "margin": 1, + "margin-bottom": 1, + "margin-left": 1, + "margin-right": 1, + "margin-top": 1, + "mark": 1, + "mark-after": 1, + "mark-before": 1, + "marks": 1, + "marquee-direction": 1, + "marquee-play-count": 1, + "marquee-speed": 1, + "marquee-style": 1, + "max-height": 1, + "max-width": 1, + "min-height": 1, + "min-width": 1, + "move-to": 1, + "nav-down": 1, + "nav-index": 1, + "nav-left": 1, + "nav-right": 1, + "nav-up": 1, + "opacity": 1, + "orphans": 1, + "outline": 1, + "outline-color": 1, + "outline-offset": 1, + "outline-style": 1, + "outline-width": 1, + "overflow": 1, + "overflow-style": 1, + "overflow-x": 1, + "overflow-y": 1, + "padding": 1, + "padding-bottom": 1, + "padding-left": 1, + "padding-right": 1, + "padding-top": 1, + "page": 1, + "page-break-after": 1, + "page-break-before": 1, + "page-break-inside": 1, + "page-policy": 1, + "pause": 1, + "pause-after": 1, + "pause-before": 1, + "perspective": 1, + "perspective-origin": 1, + "phonemes": 1, + "pitch": 1, + "pitch-range": 1, + "play-during": 1, + "position": 1, + "presentation-level": 1, + "punctuation-trim": 1, + "quotes": 1, + "rendering-intent": 1, + "resize": 1, + "rest": 1, + "rest-after": 1, + "rest-before": 1, + "richness": 1, + "right": 1, + "rotation": 1, + "rotation-point": 1, + "ruby-align": 1, + "ruby-overhang": 1, + "ruby-position": 1, + "ruby-span": 1, + "size": 1, + "speak": 1, + "speak-header": 1, + "speak-numeral": 1, + "speak-punctuation": 1, + "speech-rate": 1, + "stress": 1, + "string-set": 1, + "table-layout": 1, + "target": 1, + "target-name": 1, + "target-new": 1, + "target-position": 1, + "text-align": 1, + "text-align-last": 1, + "text-decoration": 1, + "text-emphasis": 1, + "text-height": 1, + "text-indent": 1, + "text-justify": 1, + "text-outline": 1, + "text-shadow": 1, + "text-transform": 1, + "text-wrap": 1, + "top": 1, + "transform": 1, + "transform-origin": 1, + "transform-style": 1, + "transition": 1, + "transition-delay": 1, + "transition-duration": 1, + "transition-property": 1, + "transition-timing-function": 1, + "unicode-bidi": 1, + "vertical-align": 1, + "visibility": 1, + "voice-balance": 1, + "voice-duration": 1, + "voice-family": 1, + "voice-pitch": 1, + "voice-pitch-range": 1, + "voice-rate": 1, + "voice-stress": 1, + "voice-volume": 1, + "volume": 1, + "white-space": 1, + "white-space-collapse": 1, + "widows": 1, + "width": 1, + "word-break": 1, + "word-spacing": 1, + "word-wrap": 1, + "z-index": 1, + + //IE + "filter": 1, + "zoom": 1 + }; + + parser.addListener("property", function(event){ + var name = event.property.text.toLowerCase(); + + if (!properties[name] && name.charAt(0) != "-"){ + reporter.error("Unknown property '" + event.property + "'.", event.line, event.col, rule); + } + + }); } }); @@ -5498,36 +6413,55 @@ CSSLint.addRule({ name: "Overqualified Elements", desc: "Don't use classes or IDs with elements (a.foo or a#foo).", browsers: "All", - + //initialization init: function(parser, reporter){ - var rule = this; + var rule = this, + classes = {}; + parser.addListener("startrule", function(event){ var selectors = event.selectors, selector, part, modifier, i, j, k; - + for (i=0; i < selectors.length; i++){ selector = selectors[i]; - for (j=0; j < selector.parts.length; j++){ + for (j=0; j < selector.parts.length; j++){ part = selector.parts[j]; if (part instanceof parserlib.css.SelectorPart){ - if (part.elementName){ - for (k=0; k < part.modifiers.length; k++){ - modifier = part.modifiers[k]; - if (modifier.type == "class" || modifier.type == "id"){ - reporter.warn("Element (" + part + ") is overqualified, just use " + modifier + " without element name.", part.line, part.col, rule); + for (k=0; k < part.modifiers.length; k++){ + modifier = part.modifiers[k]; + if (part.elementName && modifier.type == "id"){ + reporter.warn("Element (" + part + ") is overqualified, just use " + modifier + " without element name.", part.line, part.col, rule); + } else if (modifier.type == "class"){ + + if (!classes[modifier]){ + classes[modifier] = []; } + classes[modifier].push({ modifier: modifier, part: part }); } - } - } + } } } - }); + }); + + parser.addListener("endstylesheet", function(){ + + var prop; + for (prop in classes){ + if (classes.hasOwnProperty(prop)){ + + //one use means that this is overqualified + if (classes[prop].length == 1 && classes[prop][0].part.elementName){ + reporter.warn("Element (" + classes[prop][0].part + ") is overqualified, just use " + classes[prop][0].modifier + " without element name.", classes[prop][0].part.line, classes[prop][0].part.col, rule); + } + } + } + }); } }); @@ -5541,31 +6475,30 @@ CSSLint.addRule({ name: "Qualified Headings", desc: "Headings should not be qualified (namespaced).", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; - + parser.addListener("startrule", function(event){ var selectors = event.selectors, selector, part, - modifier, - i, j, k; - + i, j; + for (i=0; i < selectors.length; i++){ selector = selectors[i]; - for (j=0; j < selector.parts.length; j++){ + for (j=0; j < selector.parts.length; j++){ part = selector.parts[j]; if (part instanceof parserlib.css.SelectorPart){ if (part.elementName && /h[1-6]/.test(part.elementName.toString()) && j > 0){ reporter.warn("Heading (" + part.elementName + ") should not be qualified.", part.line, part.col, rule); } - } + } } } - }); + }); } }); @@ -5579,21 +6512,21 @@ CSSLint.addRule({ name: "Regex Selectors", desc: "Selectors that look like regular expressions are slow and should be avoided.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; - + parser.addListener("startrule", function(event){ var selectors = event.selectors, selector, part, modifier, i, j, k; - + for (i=0; i < selectors.length; i++){ selector = selectors[i]; - for (j=0; j < selector.parts.length; j++){ + for (j=0; j < selector.parts.length; j++){ part = selector.parts[j]; if (part instanceof parserlib.css.SelectorPart){ for (k=0; k < part.modifiers.length; k++){ @@ -5601,14 +6534,14 @@ CSSLint.addRule({ if (modifier.type == "attribute"){ if (/([\~\|\^\$\*]=)/.test(modifier)){ reporter.warn("Attribute selectors with " + RegExp.$1 + " are slow!", modifier.line, modifier.col, rule); - } + } } } - } + } } } - }); + }); } }); @@ -5622,20 +6555,52 @@ CSSLint.addRule({ name: "Rules Count", desc: "Track how many rules there are.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this, count = 0; - + //count each rule - parser.addListener("startrule", function(event){ + parser.addListener("startrule", function(){ count++; }); - - parser.addListener("endstylesheet", function(event){ + + parser.addListener("endstylesheet", function(){ reporter.stat("rule-count", count); - }); + }); + } + +}); +/* + * Rule: Don't use text-indent for image replacement if you need to support rtl. + * + */ +/* + * Should we be checking for rtl/ltr? + */ +//Commented out due to lack of tests +CSSLint.addRule({ + + //rule information + id: "text-indent", + name: "Text Indent", + desc: "Checks for text indent less than -99px", + browsers: "All", + + //initialization + init: function(parser, reporter){ + var rule = this; + + //check for use of "font-size" + parser.addListener("property", function(event){ + var name = event.property, + value = event.value.parts[0].value; + + if (name == "text-indent" && value < -99){ + reporter.warn("Negative text-indent doesn't work well with RTL. If you use text-indent for image replacement explicitly set text-direction for that item to ltr.", name.line, name.col, rule); + } + }); } }); @@ -5649,11 +6614,11 @@ CSSLint.addRule({ name: "Unique Headings", desc: "Headings should be defined only once.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; - + var headings = { h1: 0, h2: 0, @@ -5662,30 +6627,59 @@ CSSLint.addRule({ h5: 0, h6: 0 }; - + + parser.addListener("startrule", function(event){ + var selectors = event.selectors, + selector, + part, + i; + + for (i=0; i < selectors.length; i++){ + selector = selectors[i]; + part = selector.parts[selector.parts.length-1]; + + if (part.elementName && /(h[1-6])/.test(part.elementName.toString())){ + headings[RegExp.$1]++; + if (headings[RegExp.$1] > 1) { + reporter.warn("Heading (" + part.elementName + ") has already been defined.", part.line, part.col, rule); + } + } + } + }); + } + +}); +/* + * Rule: Don't use universal selector because it's slow. + */ +CSSLint.addRule({ + + //rule information + id: "universal-selector", + name: "Universal Selector", + desc: "The universal selector (*) is known to be slow.", + browsers: "All", + + //initialization + init: function(parser, reporter){ + var rule = this; + parser.addListener("startrule", function(event){ var selectors = event.selectors, selector, part, modifier, i, j, k; - + for (i=0; i < selectors.length; i++){ selector = selectors[i]; - - for (j=0; j < selector.parts.length; j++){ - part = selector.parts[j]; - if (part instanceof parserlib.css.SelectorPart){ - if (part.elementName && /(h[1-6])/.test(part.elementName.toString())){ - headings[RegExp.$1]++; - if (headings[RegExp.$1] > 1) { - reporter.warn("Heading (" + part.elementName + ") has already been defined.", part.line, part.col, rule); - } - } - } + + part = selector.parts[selector.parts.length-1]; + if (part.elementName == "*"){ + reporter.warn(rule.desc, part.line, part.col, rule); } } - }); + }); } }); @@ -5700,69 +6694,88 @@ CSSLint.addRule({ name: "Vendor Prefix", desc: "When using a vendor-prefixed property, make sure to include the standard one.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this, properties, - num; - - parser.addListener("startrule", function(event){ + num, + propertiesToCheck = { + "-moz-border-radius": "border-radius", + "-webkit-border-radius": "border-radius", + "-webkit-border-top-left-radius": "border-top-left-radius", + "-webkit-border-top-right-radius": "border-top-right-radius", + "-webkit-border-bottom-left-radius": "border-bottom-left-radius", + "-webkit-border-bottom-right-radius": "border-bottom-right-radius", + "-moz-border-radius-topleft": "border-top-left-radius", + "-moz-border-radius-topright": "border-top-right-radius", + "-moz-border-radius-bottomleft": "border-bottom-left-radius", + "-moz-border-radius-bottomright": "border-bottom-right-radius", + "-moz-box-shadow": "box-shadow", + "-webkit-box-shadow": "box-shadow", + "-moz-transform" : "transform", + "-webkit-transform" : "transform", + "-o-transform" : "transform", + "-ms-transform" : "transform", + "-moz-box-sizing" : "box-sizing", + "-webkit-box-sizing" : "box-sizing", + "-moz-user-select" : "user-select", + "-khtml-user-select" : "user-select", + "-webkit-user-select" : "user-select" + }; + + //event handler for beginning of rules + function startRule(){ properties = {}; - num=1; - }); - - parser.addListener("property", function(event){ - var name = event.property, - parts = event.value.parts, - i = 0, - len = parts.length, - j; - - if (!properties[name]){ - properties[name] = []; - } - - properties[name].push({ name: event.property, value : event.value, pos:num++ }); - }); + num=1; + } - parser.addListener("endrule", function(event){ + //event handler for end of rules + function endRule(event){ var prop, i, len, standard, needed, actual, needsStandard = []; - + for (prop in properties){ - if (/(\-(?:ms|moz|webkit|o)\-)/.test(prop)){ - needsStandard.push({ actual: prop, needed: prop.substring(RegExp.$1.length)}); + if (propertiesToCheck[prop]){ + needsStandard.push({ actual: prop, needed: propertiesToCheck[prop]}); } } - + for (i=0, len=needsStandard.length; i < len; i++){ needed = needsStandard[i].needed; actual = needsStandard[i].actual; - //special case for Mozilla's border radius - if (/\-moz\-border\-radius\-(.+)/.test(actual)){ - standard = "border-" + RegExp.$1.replace(/(left|right)/, "-$1") + "-radius"; - } else { - standard = needed; - } - if (!properties[needed]){ - reporter.warn("Missing standard property '" + standard + "' to go along with '" + actual + "'.", event.selectors[0].line, event.selectors[0].col, rule); + reporter.warn("Missing standard property '" + needed + "' to go along with '" + actual + "'.", event.line, event.col, rule); } else { //make sure standard property is last if (properties[needed][0].pos < properties[actual][0].pos){ - reporter.warn("Standard property '" + standard + "' should come after vendor-prefixed property '" + actual + "'.", event.selectors[0].line, event.selectors[0].col, rule); + reporter.warn("Standard property '" + needed + "' should come after vendor-prefixed property '" + actual + "'.", event.line, event.col, rule); } } } + } + + parser.addListener("startrule", startRule); + parser.addListener("startfontface", startRule); + + parser.addListener("property", function(event){ + var name = event.property.text.toLowerCase(); + + if (!properties[name]){ + properties[name] = []; + } + + properties[name].push({ name: event.property, value : event.value, pos:num++ }); }); + parser.addListener("endrule", endRule); + parser.addListener("endfontface", endRule); } }); @@ -5770,29 +6783,45 @@ CSSLint.addRule({ * Rule: If an element has a width of 100%, be careful when placing within * an element that has padding. It may look strange. */ -CSSLint.addRule({ +//Commented out pending further review. +/*CSSLint.addRule({ //rule information id: "width-100", name: "Width 100%", desc: "Be careful when using width: 100% on elements.", browsers: "All", - + //initialization init: function(parser, reporter){ - var rule = this; + var rule = this, + width100, + boxsizing; + + parser.addListener("startrule", function(){ + width100 = null; + boxsizing = false; + }); parser.addListener("property", function(event){ - var name = event.property, + var name = event.property.text.toLowerCase(), value = event.value; - + if (name == "width" && value == "100%"){ - reporter.warn("Elements with a width of 100% may not appear as you expect inside of other elements.", name.line, name.col, rule); + width100 = event.property; + } else if (name == "box-sizing" || /\-(?:webkit|ms|moz)\-box-sizing/.test(name)){ //means you know what you're doing + boxsizing = true; } - }); + }); + + parser.addListener("endrule", function(){ + if (width100 && !boxsizing){ + reporter.warn("Elements with a width of 100% may not appear as you expect inside of other elements.", width100.line, width100.col, rule); + } + }); } -}); +});*/ /* * Rule: You don't need to specify units when a value is 0. */ @@ -5803,25 +6832,24 @@ CSSLint.addRule({ name: "Zero Units", desc: "You don't need to specify units when a value is 0.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; - + //count how many times "float" is used parser.addListener("property", function(event){ var parts = event.value.parts, i = 0, - len = parts.length, - j; - + len = parts.length; + while(i < len){ if ((parts[i].units || parts[i].type == "percentage") && parts[i].value === 0){ reporter.warn("Values of 0 shouldn't have units specified.", parts[i].line, parts[i].col, rule); } i++; } - + }); } @@ -5829,4 +6857,4 @@ CSSLint.addRule({ }); exports.CSSLint = CSSLint; -}); \ No newline at end of file +});