diff --git a/tools/csslint-rhino.js b/tools/csslint-rhino.js index 9740a1cea36f..822283354c8e 100644 --- a/tools/csslint-rhino.js +++ b/tools/csslint-rhino.js @@ -21,9 +21,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/* Build time: 5-July-2011 03:16:53 */ var CSSLint = (function(){ -/* -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 +46,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/* Build time: 5-July-2011 03:12:40 */ var parserlib = {}; (function(){ - /** * A generic base to inherit from for any object * that needs event handling. @@ -62,7 +64,7 @@ function EventTarget(){ * @property _listeners * @private */ - this._listeners = {}; + this._listeners = {}; } EventTarget.prototype = { @@ -84,14 +86,14 @@ EventTarget.prototype = { this._listeners[type].push(listener); }, - + /** * Fires an event based on the passed-in object. * @param {Object|String} event An object with at least a 'type' attribute * or a string indicating the event name. * @return {void} * @method fire - */ + */ fire: function(event){ if (typeof event == "string"){ event = { type: event }; @@ -99,19 +101,19 @@ EventTarget.prototype = { if (!event.target){ event.target = this; } - + if (!event.type){ throw new Error("Event object missing 'type' property."); } - + if (this._listeners[event.type]){ - + //create a copy of the array and use that so listeners can't chane var listeners = this._listeners[event.type].concat(); for (var i=0, len=listeners.length; i < len; i++){ listeners[i].call(this, event); } - } + } }, /** @@ -130,9 +132,9 @@ EventTarget.prototype = { break; } } - - - } + + + } } }; /** @@ -143,7 +145,7 @@ EventTarget.prototype = { * @param {String} text The text to read. */ function StringReader(text){ - + /** * The input text with line endings normalized. * @property _input @@ -151,8 +153,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 +162,8 @@ function StringReader(text){ * @private */ this._line = 1; - - + + /** * The column for the character to be read next. * @property _col @@ -169,13 +171,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 +185,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 +198,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 +230,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 +249,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 +261,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 +285,7 @@ StringReader.prototype = { col: this._col }; }, - + reset: function(){ if (this._bookmark){ this._cursor = this._bookmark.cursor; @@ -292,11 +294,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 +306,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 +325,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 +339,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 +365,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; } @@ -490,7 +492,7 @@ SyntaxUnit.prototype = { //restore constructor constructor: SyntaxUnit, - + /** * Returns the text representation of the unit. * @return {String} The text representation of the unit. @@ -499,7 +501,7 @@ SyntaxUnit.prototype = { valueOf: function(){ return this.toString(); }, - + /** * Returns the text representation of the unit. * @return {String} The text representation of the unit. @@ -515,7 +517,7 @@ SyntaxUnit.prototype = { * @class TokenStreamBase * @namespace parserlib.util * @constructor - * @param {String|StringReader} input The text to tokenize or a reader from + * @param {String|StringReader} input The text to tokenize or a reader from * which to read the input. */ function TokenStreamBase(input, tokenData){ @@ -528,15 +530,15 @@ function TokenStreamBase(input, tokenData){ */ //this._reader = (typeof input == "string") ? new StringReader(input) : input; this._reader = input ? new StringReader(input.toString()) : null; - + /** * Token object for the last consumed token. * @type Token * @property _token * @private */ - this._token = null; - + this._token = null; + /** * The array of token information. * @type Array @@ -544,7 +546,7 @@ function TokenStreamBase(input, tokenData){ * @private */ this._tokenData = tokenData; - + /** * Lookahead token buffer. * @type Array @@ -552,7 +554,7 @@ function TokenStreamBase(input, tokenData){ * @private */ this._lt = []; - + /** * Lookahead token buffer index. * @type int @@ -560,7 +562,7 @@ function TokenStreamBase(input, tokenData){ * @private */ this._ltIndex = 0; - + this._ltIndexCache = []; } @@ -580,7 +582,7 @@ TokenStreamBase.createTokenData = function(tokens){ tokenData = tokens.concat([]), i = 0, len = tokenData.length+1; - + tokenData.UNKNOWN = -1; tokenData.unshift({name:"EOF"}); @@ -591,27 +593,27 @@ TokenStreamBase.createTokenData = function(tokens){ typeMap[tokenData[i].text] = i; } } - + tokenData.name = function(tt){ return nameMap[tt]; }; - + tokenData.type = function(c){ return typeMap[c]; }; - + return tokenData; }; TokenStreamBase.prototype = { //restore constructor - constructor: TokenStreamBase, - + constructor: TokenStreamBase, + //------------------------------------------------------------------------- // Matching methods //------------------------------------------------------------------------- - + /** * Determines if the next token matches the given token type. * If so, that token is consumed; if not, the token is placed @@ -627,27 +629,27 @@ TokenStreamBase.prototype = { * @method match */ match: function(tokenTypes, channel){ - + //always convert to an array, makes things easier if (!(tokenTypes instanceof Array)){ tokenTypes = [tokenTypes]; } - + var tt = this.get(channel), i = 0, len = tokenTypes.length; - + while(i < len){ if (tt == tokenTypes[i++]){ return true; } } - + //no match found, put the token back this.unget(); return false; - }, - + }, + /** * Determines if the next token matches the given token type. * If so, that token is consumed; if not, an error is thrown. @@ -658,7 +660,7 @@ TokenStreamBase.prototype = { * provided, reads from the default (unnamed) channel. * @return {void} * @method mustMatch - */ + */ mustMatch: function(tokenTypes, channel){ //always convert to an array, makes things easier @@ -666,17 +668,17 @@ TokenStreamBase.prototype = { tokenTypes = [tokenTypes]; } - if (!this.match.apply(this, arguments)){ + if (!this.match.apply(this, arguments)){ token = this.LT(1); - throw new SyntaxError("Expected " + this._tokenData[tokenTypes[0]].name + + throw new SyntaxError("Expected " + this._tokenData[tokenTypes[0]].name + " at line " + token.startLine + ", character " + token.startCol + ".", token.startLine, token.startCol); } }, - + //------------------------------------------------------------------------- // Consuming methods //------------------------------------------------------------------------- - + /** * Keeps reading from the token stream until either one of the specified * token types is found or until the end of the input is reached. @@ -689,21 +691,21 @@ TokenStreamBase.prototype = { * @method advance */ advance: function(tokenTypes, channel){ - + while(this.LA(0) != 0 && !this.match(tokenTypes, channel)){ this.get(); } - return this.LA(0); + return this.LA(0); }, - + /** - * Consumes the next token from the token stream. + * Consumes the next token from the token stream. * @return {int} The token type of the token that was just consumed. * @method get - */ + */ get: function(channel){ - + var tokenInfo = this._tokenData, reader = this._reader, value, @@ -712,14 +714,14 @@ TokenStreamBase.prototype = { found = false, token, info; - + //check the lookahead buffer first - if (this._lt.length && this._ltIndex >= 0 && this._ltIndex < this._lt.length){ - + if (this._lt.length && this._ltIndex >= 0 && this._ltIndex < this._lt.length){ + i++; this._token = this._lt[this._ltIndex++]; info = tokenInfo[this._token.type]; - + //obey channels logic while((info.channel !== undefined && channel !== info.channel) && this._ltIndex < this._lt.length){ @@ -727,7 +729,7 @@ TokenStreamBase.prototype = { info = tokenInfo[this._token.type]; i++; } - + //here be dragons if ((info.channel === undefined || channel === info.channel) && this._ltIndex <= this._lt.length){ @@ -735,45 +737,45 @@ TokenStreamBase.prototype = { return this._token.type; } } - + //call token retriever method token = this._getToken(); //if it should be hidden, don't save a token if (token.type > -1 && !tokenInfo[token.type].hide){ - + //apply token channel token.channel = tokenInfo[token.type].channel; - + //save for later this._token = token; this._lt.push(token); //save space that will be moved (must be done before array is truncated) - this._ltIndexCache.push(this._lt.length - this._ltIndex + i); - + this._ltIndexCache.push(this._lt.length - this._ltIndex + i); + //keep the buffer under 5 items if (this._lt.length > 5){ - this._lt.shift(); + this._lt.shift(); } - + //also keep the shift buffer under 5 items if (this._ltIndexCache.length > 5){ this._ltIndexCache.shift(); } - + //update lookahead index this._ltIndex = this._lt.length; } - + /* * Skip to the next token if: * 1. The token type is marked as hidden. * 2. The token type has a channel specified and it isn't the current channel. */ info = tokenInfo[token.type]; - if (info && - (info.hide || + if (info && + (info.hide || (info.channel !== undefined && channel !== info.channel))){ return this.get(channel); } else { @@ -781,7 +783,7 @@ TokenStreamBase.prototype = { return token.type; } }, - + /** * Looks ahead a certain number of tokens and returns the token type at * that position. This will throw an error if you lookahead past the @@ -800,34 +802,34 @@ TokenStreamBase.prototype = { if (index > 5){ throw new Error("Too much lookahead."); } - + //get all those tokens while(total){ - tt = this.get(); - total--; + tt = this.get(); + total--; } - + //unget all those tokens while(total < index){ this.unget(); total++; } } else if (index < 0){ - + if(this._lt[this._ltIndex+index]){ tt = this._lt[this._ltIndex+index].type; } else { throw new Error("Too much lookbehind."); } - + } else { tt = this._token.type; } - + return tt; - + }, - + /** * Looks ahead a certain number of tokens and returns the token at * that position. This will throw an error if you lookahead past the @@ -837,18 +839,18 @@ TokenStreamBase.prototype = { * current token, 1 for the next, -1 for the previous, etc. * @return {Object} The token of the token in the given position. * @method LA - */ + */ LT: function(index){ - + //lookahead first to prime the token buffer this.LA(index); - + //now find the token, subtract one because _ltIndex is already at the next index - return this._lt[this._ltIndex+index-1]; + return this._lt[this._ltIndex+index-1]; }, - + /** - * Returns the token type for the next token in the stream without + * Returns the token type for the next token in the stream without * consuming it. * @return {int} The token type of the next token in the stream. * @method peek @@ -856,7 +858,7 @@ TokenStreamBase.prototype = { peek: function(){ return this.LA(1); }, - + /** * Returns the actual token object for the last consumed token. * @return {Token} The token object for the last consumed token. @@ -865,7 +867,7 @@ TokenStreamBase.prototype = { token: function(){ return this._token; }, - + /** * Returns the name of the token for the given token type. * @param {int} tokenType The type of token to get the name of. @@ -880,22 +882,22 @@ TokenStreamBase.prototype = { return this._tokenData[tokenType].name; } }, - + /** * Returns the token type value for the given token name. * @param {String} tokenName The name of the token whose value should be returned. * @return {int} The token type value for the given token name or -1 * for an unknown token. * @method tokenName - */ + */ tokenType: function(tokenName){ return this._tokenData[tokenName] || -1; }, - + /** * Returns the last consumed token to the token stream. * @method unget - */ + */ unget: function(){ //if (this._ltIndex > -1){ if (this._ltIndexCache.length){ @@ -909,8 +911,6 @@ TokenStreamBase.prototype = { }; - - parserlib.util = { StringReader: StringReader, SyntaxError : SyntaxError, @@ -919,10 +919,9 @@ EventTarget : EventTarget, 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 +942,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/* Build time: 5-July-2011 03:12:40 */ (function(){ var EventTarget = parserlib.util.EventTarget, TokenStreamBase = parserlib.util.TokenStreamBase, @@ -950,7 +950,6 @@ StringReader = parserlib.util.StringReader, SyntaxError = parserlib.util.SyntaxError, SyntaxUnit = parserlib.util.SyntaxUnit; - var Colors = { aliceblue :"#f0f8ff", antiquewhite :"#faebd7", @@ -1099,12 +1098,12 @@ var Colors = { * @class Combinator * @extends parserlib.util.SyntaxUnit * @constructor - * @param {String} text The text representation of the unit. + * @param {String} text The text representation of the unit. * @param {int} line The line of text on which the unit resides. * @param {int} col The column of text on which the unit resides. */ function Combinator(text, line, col){ - + SyntaxUnit.call(this, text, line, col); /** @@ -1113,7 +1112,7 @@ function Combinator(text, line, col){ * @property type */ this.type = "unknown"; - + //pretty simple if (/^\s+$/.test(text)){ this.type = "descendant"; @@ -1131,7 +1130,6 @@ Combinator.prototype = new SyntaxUnit(); Combinator.prototype.constructor = Combinator; - var Level1Properties = { "background": 1, @@ -1140,7 +1138,7 @@ var Level1Properties = { "background-image": 1, "background-position": 1, "background-repeat": 1, - + "border": 1, "border-bottom": 1, "border-bottom-width": 1, @@ -1153,50 +1151,50 @@ var Level1Properties = { "border-top": 1, "border-top-width": 1, "border-width": 1, - + "clear": 1, "color": 1, "display": 1, "float": 1, - + "font": 1, "font-family": 1, "font-size": 1, "font-style": 1, "font-variant": 1, "font-weight": 1, - + "height": 1, "letter-spacing": 1, "line-height": 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, - + "padding": 1, "padding-bottom": 1, "padding-left": 1, "padding-right": 1, "padding-top": 1, - + "text-align": 1, "text-decoration": 1, "text-indent": 1, "text-transform": 1, - + "vertical-align": 1, "white-space": 1, "width": 1, "word-spacing": 1 - + }; var Level2Properties = { @@ -1222,7 +1220,7 @@ var Level2Properties = { "stress": 1, "voice-family": 1, "volume": 1, - + //Paged "orphans": 1, "page-break-after": 1, @@ -1235,15 +1233,15 @@ var Level2Properties = { "outline-color": 1, "outline-style": 1, "outline-width": 1, - "outline": 1, - + "outline": 1, + //Visual "background-attachment": 1, "background-color": 1, "background-image": 1, "background-position": 1, "background-repeat": 1, - "background": 1, + "background": 1, "border-collapse": 1, "border-color": 1, "border-spacing": 1, @@ -1254,7 +1252,7 @@ var Level2Properties = { "border-top-width": 1, "border-width": 1, "border": 1, - "bottom": 1, + "bottom": 1, "caption-side": 1, "clear": 1, "clip": 1, @@ -1317,7 +1315,7 @@ var Level2Properties = { * @param {SyntaxUnit} value The value of the feature or null if none. */ function MediaFeature(name, value){ - + SyntaxUnit.call(this, "(" + name + (value !== null ? ":" + value : "") + ")", name.startLine, name.startCol); /** @@ -1338,7 +1336,6 @@ function MediaFeature(name, value){ MediaFeature.prototype = new SyntaxUnit(); MediaFeature.prototype.constructor = MediaFeature; - /** * Represents an individual media query. * @namespace parserlib.css @@ -1352,7 +1349,7 @@ MediaFeature.prototype.constructor = MediaFeature; * @param {int} col The column of text on which the unit resides. */ function MediaQuery(modifier, mediaType, features, line, col){ - + SyntaxUnit.call(this, (modifier ? modifier + " ": "") + (mediaType ? mediaType + " " : "") + features.join(" and "), line, col); /** @@ -1367,8 +1364,8 @@ function MediaQuery(modifier, mediaType, features, line, col){ * @type String * @property mediaType */ - this.mediaType = mediaType; - + this.mediaType = mediaType; + /** * The parts that make up the selector. * @type Array @@ -1381,7 +1378,6 @@ function MediaQuery(modifier, mediaType, features, line, col){ MediaQuery.prototype = new SyntaxUnit(); MediaQuery.prototype.constructor = MediaQuery; - /** * A CSS3 parser. * @namespace parserlib.css @@ -1410,16 +1406,16 @@ Parser.prototype = function(){ var proto = new EventTarget(), //new prototype prop, additions = { - + //restore constructor constructor: Parser, - + //----------------------------------------------------------------- // Grammar //----------------------------------------------------------------- - + _stylesheet: function(){ - + /* * stylesheet * : [ CHARSET_SYM S* STRING S* ';' ]? @@ -1427,18 +1423,18 @@ Parser.prototype = function(){ * [ namespace [S|CDO|CDC]* ]* * [ [ ruleset | media | page | font_face ] [S|CDO|CDC]* ]* * ; - */ - + */ + var tokenStream = this._tokenStream, charset = null, token, tt; - + this.fire("startstylesheet"); - + //try to read character set this._charset(); - + this._skipCruft(); //try to read imports - may be more than one @@ -1446,40 +1442,40 @@ Parser.prototype = function(){ this._import(); this._skipCruft(); } - + //try to read namespaces - may be more than one while (tokenStream.peek() == Tokens.NAMESPACE_SYM){ this._namespace(); this._skipCruft(); } - + //get the next token tt = tokenStream.peek(); - + //try to read the rest while(tt > Tokens.EOF){ - + try { - + switch(tt){ case Tokens.MEDIA_SYM: this._media(); this._skipCruft(); break; case Tokens.PAGE_SYM: - this._page(); + this._page(); this._skipCruft(); - break; + break; case Tokens.FONT_FACE_SYM: - this._font_face(); + this._font_face(); this._skipCruft(); - break; + break; case Tokens.S: this._readWhitespace(); break; - default: + default: if(!this._ruleset()){ - + //error handling for known issues switch(tt){ case Tokens.CHARSET_SYM: @@ -1498,7 +1494,7 @@ Parser.prototype = function(){ tokenStream.get(); //get the last token this._unexpectedToken(tokenStream.token()); } - + } } } catch(ex) { @@ -1509,126 +1505,146 @@ Parser.prototype = function(){ message: ex.message, line: ex.line, col: ex.col - }); + }); } else { throw ex; } } - + tt = tokenStream.peek(); } - + if (tt != Tokens.EOF){ this._unexpectedToken(tokenStream.token()); } - + this.fire("endstylesheet"); }, - + _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); - + token = tokenStream.token(); charset = token.value; - + this._readWhitespace(); tokenStream.mustMatch(Tokens.SEMICOLON); - + if (emit !== false){ - this.fire({ + this.fire({ type: "charset", - charset:charset + charset:charset, + line: line, + col: col }); } - } + } }, - + _import: function(emit){ /* * import * : IMPORT_SYM S* * [STRING|URI] S* media_query_list? ';' S* - */ - + */ + 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]); - + //grab the URI value - uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1"); + uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1"); this._readWhitespace(); - + mediaList = this._media_query_list(); - + //must end with a semicolon tokenStream.mustMatch(Tokens.SEMICOLON); this._readWhitespace(); - + if (emit !== false){ this.fire({ type: "import", uri: uri, - media: mediaList + media: mediaList, + line: importToken.startLine, + col: importToken.startCol }); } - + }, - + _namespace: function(emit){ /* * namespace * : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S* - */ - + */ + 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 if (tokenStream.match(Tokens.IDENT)){ prefix = tokenStream.token().value; this._readWhitespace(); } - + tokenStream.mustMatch([Tokens.STRING, Tokens.URI]); /*if (!tokenStream.match(Tokens.STRING)){ tokenStream.mustMatch(Tokens.URI); }*/ - + //grab the URI value - uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1"); + uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1"); this._readWhitespace(); //must end with a semicolon tokenStream.mustMatch(Tokens.SEMICOLON); this._readWhitespace(); - + if (emit !== false){ this.fire({ type: "namespace", prefix: prefix, - uri: uri + uri: uri, + line: line, + col: col }); } - - }, - + + }, + _media: function(){ /* * media @@ -1636,33 +1652,48 @@ Parser.prototype = function(){ * ; */ var tokenStream = this._tokenStream, + line, + col, mediaList;// = []; - + //look for @media tokenStream.mustMatch(Tokens.MEDIA_SYM); - this._readWhitespace(); + line = tokenStream.token().startLine; + col = tokenStream.token().startCol; + + this._readWhitespace(); mediaList = this._media_query_list(); tokenStream.mustMatch(Tokens.LBRACE); this._readWhitespace(); - + 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 }); - }, - + }, + //CSS3 Media Queries _media_query_list: function(){ @@ -1673,26 +1704,26 @@ Parser.prototype = function(){ */ var tokenStream = this._tokenStream, mediaList = []; - - + + this._readWhitespace(); - - if (tokenStream.peek() == Tokens.IDENT){ + + if (tokenStream.peek() == Tokens.IDENT || tokenStream.peek() == Tokens.LPAREN){ mediaList.push(this._media_query()); } - + while(tokenStream.match(Tokens.COMMA)){ this._readWhitespace(); mediaList.push(this._media_query()); } - + return mediaList; }, - + /* * Note: "expression" in the grammar maps to the _media_expression * method. - + */ _media_query: function(){ /* @@ -1706,10 +1737,10 @@ Parser.prototype = function(){ ident = null, token = null, expressions = []; - + if (tokenStream.match(Tokens.IDENT)){ ident = tokenStream.token().value.toLowerCase(); - + //since there's no custom tokens for these, need to manually check if (ident != "only" && ident != "not"){ tokenStream.unget(); @@ -1718,9 +1749,9 @@ Parser.prototype = function(){ token = tokenStream.token(); } } - + this._readWhitespace(); - + if (tokenStream.peek() == Tokens.IDENT){ type = this._media_type(); if (token === null){ @@ -1731,17 +1762,17 @@ Parser.prototype = function(){ token = tokenStream.LT(1); } expressions.push(this._media_expression()); - } - + } + if (type === null && expressions.length === 0){ return null; - } else { + } else { this._readWhitespace(); while (tokenStream.match(Tokens.IDENT)){ if (tokenStream.token().value.toLowerCase() != "and"){ this._unexpectedToken(tokenStream.token()); } - + this._readWhitespace(); expressions.push(this._media_expression()); } @@ -1757,7 +1788,7 @@ Parser.prototype = function(){ * : IDENT * ; */ - return this._media_feature(); + return this._media_feature(); }, /** @@ -1778,22 +1809,22 @@ Parser.prototype = function(){ feature = null, token, expression = null; - + tokenStream.mustMatch(Tokens.LPAREN); - + feature = this._media_feature(); this._readWhitespace(); - + if (tokenStream.match(Tokens.COLON)){ this._readWhitespace(); token = tokenStream.LT(1); expression = this._expression(); } - + tokenStream.mustMatch(Tokens.RPAREN); this._readWhitespace(); - return new MediaFeature(feature, (expression ? new SyntaxUnit(expression, token.startLine, token.startCol) : null)); + return new MediaFeature(feature, (expression ? new SyntaxUnit(expression, token.startLine, token.startCol) : null)); }, //CSS3 Media Queries @@ -1804,28 +1835,33 @@ Parser.prototype = function(){ * ; */ var tokenStream = this._tokenStream; - + tokenStream.mustMatch(Tokens.IDENT); - - return SyntaxUnit.fromToken(tokenStream.token()); + + return SyntaxUnit.fromToken(tokenStream.token()); }, - + //CSS3 Paged Media _page: function(){ /* * page: - * PAGE_SYM S* IDENT? pseudo_page? S* + * PAGE_SYM S* IDENT? pseudo_page? S* * '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* * ; - */ + */ 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)){ identifier = tokenStream.token().value; @@ -1833,31 +1869,35 @@ Parser.prototype = function(){ if (identifier.toLowerCase() === "auto"){ this._unexpectedToken(tokenStream.token()); } - } - + } + //see if there's a colon upcoming if (tokenStream.peek() == Tokens.COLON){ pseudoPage = this._pseudo_page(); } - + this._readWhitespace(); - + this.fire({ type: "startpage", id: identifier, - pseudo: pseudoPage - }); - - this._readDeclarations(true, true); + pseudo: pseudoPage, + line: line, + col: col + }); + this._readDeclarations(true, true); + this.fire({ type: "endpage", id: identifier, - pseudo: pseudoPage - }); - + pseudo: pseudoPage, + line: line, + col: col + }); + }, - + //CSS3 Paged Media _margin: function(){ /* @@ -1866,20 +1906,29 @@ 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 { return false; @@ -1888,17 +1937,17 @@ Parser.prototype = function(){ //CSS3 Paged Media _margin_sym: function(){ - + /* * margin_sym : - * TOPLEFTCORNER_SYM | - * TOPLEFT_SYM | - * TOPCENTER_SYM | - * TOPRIGHT_SYM | + * TOPLEFTCORNER_SYM | + * TOPLEFT_SYM | + * TOPCENTER_SYM | + * TOPRIGHT_SYM | * TOPRIGHTCORNER_SYM | - * BOTTOMLEFTCORNER_SYM | - * BOTTOMLEFT_SYM | - * BOTTOMCENTER_SYM | + * BOTTOMLEFTCORNER_SYM | + * BOTTOMLEFT_SYM | + * BOTTOMCENTER_SYM | * BOTTOMRIGHT_SYM | * BOTTOMRIGHTCORNER_SYM | * LEFTTOP_SYM | @@ -1906,133 +1955,142 @@ Parser.prototype = function(){ * LEFTBOTTOM_SYM | * RIGHTTOP_SYM | * RIGHTMIDDLE_SYM | - * RIGHTBOTTOM_SYM + * RIGHTBOTTOM_SYM * ; */ - + var tokenStream = this._tokenStream; - + if(tokenStream.match([Tokens.TOPLEFTCORNER_SYM, Tokens.TOPLEFT_SYM, Tokens.TOPCENTER_SYM, Tokens.TOPRIGHT_SYM, Tokens.TOPRIGHTCORNER_SYM, - Tokens.BOTTOMLEFTCORNER_SYM, Tokens.BOTTOMLEFT_SYM, + Tokens.BOTTOMLEFTCORNER_SYM, Tokens.BOTTOMLEFT_SYM, Tokens.BOTTOMCENTER_SYM, Tokens.BOTTOMRIGHT_SYM, - Tokens.BOTTOMRIGHTCORNER_SYM, Tokens.LEFTTOP_SYM, + Tokens.BOTTOMRIGHTCORNER_SYM, Tokens.LEFTTOP_SYM, Tokens.LEFTMIDDLE_SYM, Tokens.LEFTBOTTOM_SYM, Tokens.RIGHTTOP_SYM, Tokens.RIGHTMIDDLE_SYM, Tokens.RIGHTBOTTOM_SYM])) { - return SyntaxUnit.fromToken(tokenStream.token()); + return SyntaxUnit.fromToken(tokenStream.token()); } else { return null; } - + }, - + _pseudo_page: function(){ /* * pseudo_page * : ':' IDENT - * ; + * ; */ - + var tokenStream = this._tokenStream; - + tokenStream.mustMatch(Tokens.COLON); tokenStream.mustMatch(Tokens.IDENT); - + //TODO: CSS3 Paged Media says only "left", "center", and "right" are allowed - + return tokenStream.token().value; }, - + _font_face: function(){ /* * font_face - * : FONT_FACE_SYM S* + * : FONT_FACE_SYM S* * '{' 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 + }); }, _operator: function(){ - + /* * operator * : '/' S* | ',' S* | /( empty )/ * ; - */ - + */ + var tokenStream = this._tokenStream, token = null; - + if (tokenStream.match([Tokens.SLASH, Tokens.COMMA])){ token = tokenStream.token(); this._readWhitespace(); - } + } return token ? PropertyValuePart.fromToken(token) : null; - + }, - + _combinator: function(){ - + /* * combinator * : PLUS S* | GREATER S* | TILDE S* | S+ * ; - */ - + */ + var tokenStream = this._tokenStream, value = null, token; - - if(tokenStream.match([Tokens.PLUS, Tokens.GREATER, Tokens.TILDE])){ + + if(tokenStream.match([Tokens.PLUS, Tokens.GREATER, Tokens.TILDE])){ token = tokenStream.token(); value = new Combinator(token.value, token.startLine, token.startCol); this._readWhitespace(); } - + return value; }, - + _unary_operator: function(){ - + /* * unary_operator * : '-' | '+' * ; */ - + var tokenStream = this._tokenStream; - + if (tokenStream.match([Tokens.MINUS, Tokens.PLUS])){ return tokenStream.token().value; } else { return null; - } + } }, - + _property: function(){ - + /* * property * : IDENT S* - * ; + * ; */ - + var tokenStream = this._tokenStream, value = null, hack = null, @@ -2040,7 +2098,7 @@ Parser.prototype = function(){ token, line, col; - + //check for star hack - throws error if not allowed if (tokenStream.peek() == Tokens.STAR && this.options.starHack){ tokenStream.get(); @@ -2049,34 +2107,35 @@ Parser.prototype = function(){ line = token.startLine; col = token.startCol; } - + if(tokenStream.match(Tokens.IDENT)){ token = tokenStream.token(); tokenValue = token.value; - + //check for underscore hack - no error if not allowed because it's valid CSS syntax if (tokenValue.charAt(0) == "_" && this.options.underscoreHack){ hack = "_"; tokenValue = tokenValue.substring(1); } - + value = new PropertyName(tokenValue, hack, (line||token.startLine), (col||token.startCol)); this._readWhitespace(); } - + return value; }, - + //Augmented with CSS3 Selectors _ruleset: function(){ /* * ruleset * : selectors_group * '{' S* declaration? [ ';' S* declaration? ]* '}' S* - * ; - */ - + * ; + */ + var tokenStream = this._tokenStream, + tt, selectors; @@ -2088,7 +2147,7 @@ Parser.prototype = function(){ selectors = this._selectors_group(); } catch (ex){ if (ex instanceof SyntaxError && !this.options.strict){ - + //fire error event this.fire({ type: "error", @@ -2096,8 +2155,8 @@ Parser.prototype = function(){ message: ex.message, line: ex.line, col: ex.col - }); - + }); + //skip over everything until closing brace tt = tokenStream.advance([Tokens.RBRACE]); if (tt == Tokens.RBRACE){ @@ -2105,53 +2164,57 @@ Parser.prototype = function(){ } else { //otherwise, rethrow the error because it wasn't handled properly throw ex; - } - + } + } else { //not a syntax error, rethrow it throw ex; - } - + } + //trigger parser to continue return true; } - + //if it got here, all selectors parsed - if (selectors){ - + if (selectors){ + this.fire({ type: "startrule", - selectors: selectors - }); - - this._readDeclarations(true); - + 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 + }); + } - + return selectors; - + }, //CSS3 Selectors _selectors_group: function(){ - - /* + + /* * selectors_group * : selector [ COMMA S* selector ]* * ; - */ + */ var tokenStream = this._tokenStream, selectors = [], selector; - + selector = this._selector(); if (selector !== null){ - + selectors.push(selector); while(tokenStream.match(Tokens.COMMA)){ this._readWhitespace(); @@ -2166,83 +2229,83 @@ Parser.prototype = function(){ return selectors.length ? selectors : null; }, - + //CSS3 Selectors _selector: function(){ /* * selector * : simple_selector_sequence [ combinator simple_selector_sequence ]* - * ; + * ; */ - + var tokenStream = this._tokenStream, selector = [], nextSelector = null, combinator = null, ws = null; - + //if there's no simple selector, then there's no selector nextSelector = this._simple_selector_sequence(); if (nextSelector === null){ return null; } - + selector.push(nextSelector); - + do { - + //look for a combinator combinator = this._combinator(); - + if (combinator !== null){ selector.push(combinator); nextSelector = this._simple_selector_sequence(); - + //there must be a next selector if (nextSelector === null){ this._unexpectedToken(this.LT(1)); } else { - + //nextSelector is an instance of SelectorPart selector.push(nextSelector); } } else { - + //if there's not whitespace, we're done - if (this._readWhitespace()){ - + if (this._readWhitespace()){ + //add whitespace separator ws = new Combinator(tokenStream.token().value, tokenStream.token().startLine, tokenStream.token().startCol); - + //combinator is not required combinator = this._combinator(); - + //selector is required if there's a combinator nextSelector = this._simple_selector_sequence(); - if (nextSelector === null){ + if (nextSelector === null){ if (combinator !== null){ this._unexpectedToken(tokenStream.LT(1)); } } else { - + if (combinator !== null){ selector.push(combinator); } else { selector.push(ws); } - + selector.push(nextSelector); - } + } } else { break; - } - + } + } } while(true); - + return new Selector(selector, selector[0].line, selector[0].col); }, - + //CSS3 Selectors _simple_selector_sequence: function(){ /* @@ -2252,13 +2315,13 @@ Parser.prototype = function(){ * | [ HASH | class | attrib | pseudo | negation ]+ * ; */ - + var tokenStream = this._tokenStream, - + //parts of a simple selector elementName = null, modifiers = [], - + //complete selector text selectorText= "", @@ -2281,35 +2344,35 @@ Parser.prototype = function(){ found = false, line, col; - - + + //get starting line and column for the selector line = tokenStream.LT(1).startLine; col = tokenStream.LT(1).startCol; - + elementName = this._type_selector(); if (!elementName){ elementName = this._universal(); } - + if (elementName !== null){ selectorText += elementName; - } - + } + while(true){ //whitespace means we're done if (tokenStream.peek() === Tokens.S){ break; } - + //check for each component while(i < len && component === null){ component = components[i++].call(this); } - + if (component === null){ - + //we don't have a selector if (selectorText === ""){ return null; @@ -2319,17 +2382,17 @@ Parser.prototype = function(){ } else { i = 0; modifiers.push(component); - selectorText += component.toString(); + selectorText += component.toString(); component = null; } } - + return selectorText !== "" ? new SelectorPart(elementName, modifiers, selectorText, line, col) : null; - }, - + }, + //CSS3 Selectors _type_selector: function(){ /* @@ -2337,12 +2400,12 @@ Parser.prototype = function(){ * : [ namespace_prefix ]? element_name * ; */ - + var tokenStream = this._tokenStream, ns = this._namespace_prefix(), elementName = this._element_name(); - - if (!elementName){ + + if (!elementName){ /* * Need to back out the namespace that was read due to both * type_selector and universal reading namespace_prefix @@ -2355,9 +2418,9 @@ Parser.prototype = function(){ tokenStream.unget(); } } - + return null; - } else { + } else { if (ns){ elementName.text = ns + elementName.text; elementName.col -= ns.length; @@ -2365,97 +2428,97 @@ Parser.prototype = function(){ return elementName; } }, - + //CSS3 Selectors _class: function(){ /* * class * : '.' IDENT * ; - */ - + */ + var tokenStream = this._tokenStream, token; - + if (tokenStream.match(Tokens.DOT)){ - tokenStream.mustMatch(Tokens.IDENT); + tokenStream.mustMatch(Tokens.IDENT); token = tokenStream.token(); - return new SelectorSubPart("." + token.value, "class", token.startLine, token.startCol - 1); + return new SelectorSubPart("." + token.value, "class", token.startLine, token.startCol - 1); } else { return null; } - + }, - + //CSS3 Selectors _element_name: function(){ /* * element_name * : IDENT * ; - */ - + */ + var tokenStream = this._tokenStream, token; - + if (tokenStream.match(Tokens.IDENT)){ token = tokenStream.token(); - return new SelectorSubPart(token.value, "elementName", token.startLine, token.startCol); - + return new SelectorSubPart(token.value, "elementName", token.startLine, token.startCol); + } else { return null; } }, - + //CSS3 Selectors _namespace_prefix: function(){ - /* + /* * namespace_prefix * : [ IDENT | '*' ]? '|' * ; */ var tokenStream = this._tokenStream, value = ""; - + //verify that this is a namespace prefix if (tokenStream.LA(1) === Tokens.PIPE || tokenStream.LA(2) === Tokens.PIPE){ - + if(tokenStream.match([Tokens.IDENT, Tokens.STAR])){ value += tokenStream.token().value; } - + tokenStream.mustMatch(Tokens.PIPE); value += "|"; - + } - - return value.length ? value : null; + + return value.length ? value : null; }, - + //CSS3 Selectors _universal: function(){ /* * universal * : [ namespace_prefix ]? '*' - * ; + * ; */ var tokenStream = this._tokenStream, value = "", ns; - + ns = this._namespace_prefix(); if(ns){ value += ns; } - + if(tokenStream.match(Tokens.STAR)){ value += "*"; } - + return value.length ? value : null; - + }, - + //CSS3 Selectors _attrib: function(){ /* @@ -2468,69 +2531,69 @@ Parser.prototype = function(){ * INCLUDES | * DASHMATCH ] S* [ IDENT | STRING ] S* * ]? ']' - * ; + * ; */ - + var tokenStream = this._tokenStream, value = null, ns, token; - + if (tokenStream.match(Tokens.LBRACKET)){ token = tokenStream.token(); value = token.value; value += this._readWhitespace(); - + ns = this._namespace_prefix(); - + if (ns){ value += ns; } - + tokenStream.mustMatch(Tokens.IDENT); - value += tokenStream.token().value; + value += tokenStream.token().value; value += this._readWhitespace(); - + if(tokenStream.match([Tokens.PREFIXMATCH, Tokens.SUFFIXMATCH, Tokens.SUBSTRINGMATCH, Tokens.EQUALS, Tokens.INCLUDES, Tokens.DASHMATCH])){ - - value += tokenStream.token().value; + + value += tokenStream.token().value; value += this._readWhitespace(); - + tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]); - value += tokenStream.token().value; + value += tokenStream.token().value; value += this._readWhitespace(); } - + tokenStream.mustMatch(Tokens.RBRACKET); - + return new SelectorSubPart(value + "]", "attribute", token.startLine, token.startCol); } else { return null; } }, - + //CSS3 Selectors _pseudo: function(){ - + /* * pseudo * : ':' ':'? [ IDENT | functional_pseudo ] - * ; - */ - + * ; + */ + var tokenStream = this._tokenStream, pseudo = null, colons = ":", line, col; - + if (tokenStream.match(Tokens.COLON)){ - + if (tokenStream.match(Tokens.COLON)){ colons += ":"; } - + if (tokenStream.match(Tokens.IDENT)){ pseudo = tokenStream.token().value; line = tokenStream.token().startLine; @@ -2540,26 +2603,26 @@ Parser.prototype = function(){ col = tokenStream.LT(1).startCol - colons.length; pseudo = this._functional_pseudo(); } - + if (pseudo){ pseudo = new SelectorSubPart(colons + pseudo, "pseudo", line, col); } } - + return pseudo; }, - + //CSS3 Selectors _functional_pseudo: function(){ /* * functional_pseudo * : FUNCTION S* expression ')' * ; - */ - + */ + var tokenStream = this._tokenStream, value = null; - + if(tokenStream.match(Tokens.FUNCTION)){ value = tokenStream.token().value; value += this._readWhitespace(); @@ -2567,10 +2630,10 @@ Parser.prototype = function(){ tokenStream.mustMatch(Tokens.RPAREN); value += ")"; } - + return value; }, - + //CSS3 Selectors _expression: function(){ /* @@ -2578,26 +2641,26 @@ Parser.prototype = function(){ * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ * ; */ - + var tokenStream = this._tokenStream, value = ""; - + 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.RESOLUTION])){ - + value += tokenStream.token().value; - value += this._readWhitespace(); + value += this._readWhitespace(); } - + return value.length ? value : null; - + }, //CSS3 Selectors _negation: function(){ - /* + /* * negation * : NOT S* negation_arg S* ')' * ; @@ -2609,7 +2672,7 @@ Parser.prototype = function(){ value = "", arg, subpart = null; - + if (tokenStream.match(Tokens.NOT)){ value = tokenStream.token().value; line = tokenStream.token().startLine; @@ -2620,22 +2683,22 @@ Parser.prototype = function(){ value += this._readWhitespace(); tokenStream.match(Tokens.RPAREN); value += tokenStream.token().value; - + subpart = new SelectorSubPart(value, "not", line, col); subpart.args.push(arg); } - + return subpart; }, - + //CSS3 Selectors - _negation_arg: function(){ + _negation_arg: function(){ /* * negation_arg * : type_selector | universal | HASH | class | attrib | pseudo - * ; - */ - + * ; + */ + var tokenStream = this._tokenStream, args = [ this._type_selector, @@ -2643,11 +2706,11 @@ Parser.prototype = function(){ function(){ return tokenStream.match(Tokens.HASH) ? new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) : - null; + null; }, this._class, this._attrib, - this._pseudo + this._pseudo ], arg = null, i = 0, @@ -2656,108 +2719,110 @@ Parser.prototype = function(){ line, col, part; - + line = tokenStream.LT(1).startLine; col = tokenStream.LT(1).startCol; - + while(i < len && arg === null){ - + arg = args[i].call(this); i++; } - + //must be a negation arg if (arg === null){ this._unexpectedToken(tokenStream.LT(1)); } - + //it's an element name if (arg.type == "elementName"){ part = new SelectorPart(arg, [], arg.toString(), line, col); } else { part = new SelectorPart(null, [arg], arg.toString(), line, col); } - - return part; + + return part; }, - + _declaration: function(){ - + /* * declaration * : property ':' S* expr prio? * | /( empty )/ - * ; - */ - + * ; + */ + var tokenStream = this._tokenStream, property = null, expr = null, prio = null; - + property = this._property(); if (property !== null){ tokenStream.mustMatch(Tokens.COLON); this._readWhitespace(); - + expr = this._expr(); - + //if there's no parts for the value, it's an error if (!expr || expr.length === 0){ this._unexpectedToken(tokenStream.LT(1)); } - + prio = this._prio(); - + this.fire({ type: "property", property: property, value: expr, - important: prio - }); - + important: prio, + line: property.line, + col: property.col + }); + return true; } else { return false; } }, - + _prio: function(){ /* * prio * : IMPORTANT_SYM S* - * ; + * ; */ - + var tokenStream = this._tokenStream, result = tokenStream.match(Tokens.IMPORTANT_SYM); - + this._readWhitespace(); return result; }, - + _expr: function(){ /* * expr * : term [ operator term ]* * ; */ - + var tokenStream = this._tokenStream, values = [], //valueParts = [], value = null, operator = null; - + value = this._term(); if (value !== null){ - + values.push(value); - + do { operator = this._operator(); - + //if there's an operator, keep building up the value parts if (operator){ values.push(operator); @@ -2766,9 +2831,9 @@ Parser.prototype = function(){ values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col)); valueParts = []; }*/ - + value = this._term(); - + if (value === null){ break; } else { @@ -2776,17 +2841,17 @@ Parser.prototype = function(){ } } while(true); } - + //cleanup /*if (valueParts.length){ values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col)); }*/ - + return values.length > 0 ? new PropertyValue(values, values[0].startLine, values[0].startCol) : null; }, - - _term: function(){ - + + _term: function(){ + /* * term * : unary_operator? @@ -2794,35 +2859,35 @@ Parser.prototype = function(){ * TIME S* | FREQ S* | function | ie_function ] * | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor * ; - */ - + */ + var tokenStream = this._tokenStream, unary = null, value = null, line, col; - + //returns the operator or null unary = this._unary_operator(); if (unary !== null){ line = tokenStream.token().startLine; col = tokenStream.token().startCol; - } - + } + //exception for IE filters if (tokenStream.peek() == Tokens.IE_FUNCTION && this.options.ieFilters){ - + value = this._ie_function(); if (unary === null){ line = tokenStream.token().startLine; col = tokenStream.token().startCol; } - + //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.FREQ, Tokens.STRING, Tokens.IDENT, Tokens.URI, Tokens.UNICODE_RANGE])){ - + value = tokenStream.token().value; if (unary === null){ line = tokenStream.token().startLine; @@ -2830,20 +2895,20 @@ Parser.prototype = function(){ } this._readWhitespace(); } else { - + //see if it's a color value = this._hexcolor(); if (value === null){ - + //if there's no unary, get the start of the next token for line/col info if (unary === null){ line = tokenStream.LT(1).startLine; col = tokenStream.LT(1).startCol; - } - + } + //has to be a function if (value === null){ - + /* * This checks for alpha(opacity=0) style of IE * functions. IE_FUNCTION only presents progid: style. @@ -2859,81 +2924,81 @@ Parser.prototype = function(){ return null; //throw new Error("Expected identifier at line " + tokenStream.token().startLine + ", character " + tokenStream.token().startCol + "."); }*/ - + } else { if (unary === null){ line = tokenStream.token().startLine; col = tokenStream.token().startCol; - } + } } - - } - + + } + return value !== null ? new PropertyValuePart(unary !== null ? unary + value : value, line, col) : null; - + }, - + _function: function(){ - + /* * function * : FUNCTION S* expr ')' S* * ; */ - + var tokenStream = this._tokenStream, functionText = null, expr = null; - + if (tokenStream.match(Tokens.FUNCTION)){ functionText = tokenStream.token().value; this._readWhitespace(); expr = this._expr(); - - tokenStream.match(Tokens.RPAREN); + + tokenStream.match(Tokens.RPAREN); functionText += expr + ")"; this._readWhitespace(); - } - + } + return functionText; - }, - + }, + _ie_function: function(){ - + /* (My own extension) * ie_function * : IE_FUNCTION S* IDENT '=' term [S* ','? IDENT '=' term]+ ')' S* * ; */ - + var tokenStream = this._tokenStream, functionText = null, expr = null, lt; - + //IE function can begin like a regular function, too if (tokenStream.match([Tokens.IE_FUNCTION, Tokens.FUNCTION])){ functionText = tokenStream.token().value; - + do { - + if (this._readWhitespace()){ functionText += tokenStream.token().value; } - + //might be second time in the loop if (tokenStream.LA(0) == Tokens.COMMA){ functionText += tokenStream.token().value; } - + tokenStream.match(Tokens.IDENT); functionText += tokenStream.token().value; - + tokenStream.match(Tokens.EQUALS); functionText += tokenStream.token().value; - + //functionText += this._term(); lt = tokenStream.peek(); while(lt != Tokens.COMMA && lt != Tokens.S && lt != Tokens.RPAREN){ @@ -2941,16 +3006,16 @@ Parser.prototype = function(){ functionText += tokenStream.token().value; lt = tokenStream.peek(); } - } while(tokenStream.match([Tokens.COMMA, Tokens.S])); - - tokenStream.match(Tokens.RPAREN); + } while(tokenStream.match([Tokens.COMMA, Tokens.S])); + + tokenStream.match(Tokens.RPAREN); functionText += ")"; this._readWhitespace(); - } - + } + return functionText; - }, - + }, + _hexcolor: function(){ /* * There is a constraint on the color that it must @@ -2961,15 +3026,15 @@ Parser.prototype = function(){ * : HASH S* * ; */ - + var tokenStream = this._tokenStream, token, color = null; - + if(tokenStream.match(Tokens.HASH)){ - + //need to do some validation here - + token = tokenStream.token(); color = token.value; if (!/#[a-f0-9]{3,6}/i.test(color)){ @@ -2977,14 +3042,14 @@ Parser.prototype = function(){ } this._readWhitespace(); } - + return color; }, - + //----------------------------------------------------------------- // Helper methods //----------------------------------------------------------------- - + /** * Not part of CSS grammar, but useful for skipping over * combination of white space and HTML-style comments. @@ -3018,24 +3083,24 @@ Parser.prototype = function(){ * S* '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* * Note that this is how it is described in CSS3 Paged Media, but is actually incorrect. * A semicolon is only necessary following a delcaration is there's another declaration - * or margin afterwards. + * or margin afterwards. */ var tokenStream = this._tokenStream, tt; - + this._readWhitespace(); - + if (checkStart){ - tokenStream.mustMatch(Tokens.LBRACE); + tokenStream.mustMatch(Tokens.LBRACE); } - + this._readWhitespace(); try { - + while(true){ - + if (readMargins && this._margin()){ //noop } else if (this._declaration()){ @@ -3045,19 +3110,19 @@ Parser.prototype = function(){ } else { break; } - + //if ((!this._margin() && !this._declaration()) || !tokenStream.match(Tokens.SEMICOLON)){ // break; //} this._readWhitespace(); } - + tokenStream.mustMatch(Tokens.RBRACE); this._readWhitespace(); - + } catch (ex) { if (ex instanceof SyntaxError && !this.options.strict){ - + //fire error event this.fire({ type: "error", @@ -3065,8 +3130,8 @@ Parser.prototype = function(){ message: ex.message, line: ex.line, col: ex.col - }); - + }); + //see if there's another declaration tt = tokenStream.advance([Tokens.SEMICOLON, Tokens.RBRACE]); if (tt == Tokens.SEMICOLON){ @@ -3077,16 +3142,16 @@ Parser.prototype = function(){ } else { //otherwise, rethrow the error because it wasn't handled properly throw ex; - } - + } + } else { //not a syntax error, rethrow it throw ex; } - } - - }, - + } + + }, + /** * In some cases, you can end up with two white space tokens in a * row. Instead of making a change in every function that looks for @@ -3097,17 +3162,17 @@ Parser.prototype = function(){ * @private */ _readWhitespace: function(){ - + var tokenStream = this._tokenStream, ws = ""; - + while(tokenStream.match(Tokens.S)){ ws += tokenStream.token().value; } - + return ws; }, - + /** * Throws an error when an unexpected token is found. @@ -3119,7 +3184,7 @@ Parser.prototype = function(){ _unexpectedToken: function(token){ throw new SyntaxError("Unexpected token '" + token.value + "' at line " + token.startLine + ", char " + token.startCol + ".", token.startLine, token.startCol); }, - + /** * Helper method used for parsing subparts of a style sheet. * @return {void} @@ -3129,57 +3194,57 @@ Parser.prototype = function(){ _verifyEnd: function(){ if (this._tokenStream.LA(1) != Tokens.EOF){ this._unexpectedToken(this._tokenStream.LT(1)); - } + } }, - + //----------------------------------------------------------------- // Parsing methods //----------------------------------------------------------------- - - parse: function(input){ + + parse: function(input){ this._tokenStream = new TokenStream(input, Tokens); this._stylesheet(); }, - + parseStyleSheet: function(input){ //just passthrough return this.parse(input); }, - + parseMediaQuery: function(input){ this._tokenStream = new TokenStream(input, Tokens); var result = this._media_query(); - + //if there's anything more, then it's an invalid selector this._verifyEnd(); - + //otherwise return result - return result; + return result; }, - + /** * Parses a property value (everything after the semicolon). * @return {parserlib.css.PropertyValue} The property value. * @throws parserlib.util.SyntaxError If an unexpected token is found. * @method parserPropertyValue - */ + */ parsePropertyValue: function(input){ - + this._tokenStream = new TokenStream(input, Tokens); this._readWhitespace(); - + var result = this._expr(); - + //okay to have a trailing white space this._readWhitespace(); - + //if there's anything more, then it's an invalid selector this._verifyEnd(); - + //otherwise return result return result; }, - + /** * Parses a complete CSS rule, including selectors and * properties. @@ -3189,22 +3254,22 @@ Parser.prototype = function(){ */ parseRule: function(input){ this._tokenStream = new TokenStream(input, Tokens); - + //skip any leading white space this._readWhitespace(); - + var result = this._ruleset(); - + //skip any trailing white space this._readWhitespace(); //if there's anything more, then it's an invalid selector this._verifyEnd(); - + //otherwise return result - return result; + return result; }, - + /** * Parses a single CSS selector (no comma) * @param {String} input The text to parse as a CSS selector. @@ -3213,31 +3278,31 @@ Parser.prototype = function(){ * @method parseSelector */ parseSelector: function(input){ - + this._tokenStream = new TokenStream(input, Tokens); - + //skip any leading white space this._readWhitespace(); - + var result = this._selector(); - + //skip any trailing white space this._readWhitespace(); //if there's anything more, then it's an invalid selector this._verifyEnd(); - + //otherwise return result return result; } - + }; - + //copy over onto prototype for (prop in additions){ proto[prop] = additions[prop]; - } - + } + return proto; }(); @@ -3254,13 +3319,13 @@ nth * @class PropertyName * @extends parserlib.util.SyntaxUnit * @constructor - * @param {String} text The text representation of the unit. + * @param {String} text The text representation of the unit. * @param {String} hack The type of IE hack applied ("*", "_", or null). * @param {int} line The line of text on which the unit resides. * @param {int} col The column of text on which the unit resides. */ function PropertyName(text, hack, line, col){ - + SyntaxUnit.call(this, (hack||"") + text, line, col); /** @@ -3275,7 +3340,6 @@ function PropertyName(text, hack, line, col){ PropertyName.prototype = new SyntaxUnit(); PropertyName.prototype.constructor = PropertyName; - /** * Represents a single part of a CSS property value, meaning that it represents * just everything single part between ":" and ";". If there are multiple values @@ -3291,20 +3355,19 @@ PropertyName.prototype.constructor = PropertyName; function PropertyValue(parts, line, col){ SyntaxUnit.call(this, parts.join(" "), line, col); - + /** * The parts that make up the selector. * @type Array * @property parts */ this.parts = parts; - + } PropertyValue.prototype = new SyntaxUnit(); PropertyValue.prototype.constructor = PropertyValue; - /** * Represents a single part of a CSS property value, meaning that it represents * just one part of the data between ":" and ";". @@ -3319,7 +3382,7 @@ PropertyValue.prototype.constructor = PropertyValue; function PropertyValuePart(text, line, col){ SyntaxUnit.apply(this,arguments); - + /** * Indicates the type of value unit. * @type String @@ -3328,18 +3391,18 @@ function PropertyValuePart(text, line, col){ this.type = "unknown"; //figure out what type of data it is - + var temp; - + //it is a measurement? if (/^([+\-]?[\d\.]+)([a-z]+)$/i.test(text)){ //dimension this.type = "dimension"; this.value = +RegExp.$1; this.units = RegExp.$2; - + //try to narrow down switch(this.units.toLowerCase()){ - + case "em": case "rem": case "ex": @@ -3351,32 +3414,32 @@ function PropertyValuePart(text, line, col){ case "pc": this.type = "length"; break; - + case "deg": case "rad": case "grad": this.type = "angle"; break; - + case "ms": case "s": this.type = "time"; break; - + case "hz": case "khz": this.type = "frequency"; break; - + case "dpi": case "dpcm": this.type = "resolution"; break; - + //default - + } - + } else if (/^([+\-]?[\d\.]+)%$/i.test(text)){ //percentage this.type = "percentage"; this.value = +RegExp.$1; @@ -3389,18 +3452,18 @@ function PropertyValuePart(text, line, col){ } else if (/^([+\-]?[\d\.]+)$/i.test(text)){ //number this.type = "number"; this.value = +RegExp.$1; - + } else if (/^#([a-f0-9]{3,6})/i.test(text)){ //hexcolor this.type = "color"; temp = RegExp.$1; if (temp.length == 3){ this.red = parseInt(temp.charAt(0)+temp.charAt(0),16); this.green = parseInt(temp.charAt(1)+temp.charAt(1),16); - this.blue = parseInt(temp.charAt(2)+temp.charAt(2),16); + this.blue = parseInt(temp.charAt(2)+temp.charAt(2),16); } else { this.red = parseInt(temp.substring(0,2),16); this.green = parseInt(temp.substring(2,4),16); - this.blue = parseInt(temp.substring(4,6),16); + this.blue = parseInt(temp.substring(4,6),16); } } else if (/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i.test(text)){ //rgb() color with absolute numbers this.type = "color"; @@ -3423,7 +3486,7 @@ function PropertyValuePart(text, line, col){ temp = Colors[text.toLowerCase()].substring(1); this.red = parseInt(temp.substring(0,2),16); this.green = parseInt(temp.substring(2,4),16); - this.blue = parseInt(temp.substring(4,6),16); + this.blue = parseInt(temp.substring(4,6),16); } else if (/^[\,\/]$/.test(text)){ this.type = "operator"; this.value = text; @@ -3461,9 +3524,9 @@ PropertyValuePart.fromToken = function(token){ * @param {int} col The column of text on which the unit resides. */ function Selector(parts, line, col){ - + SyntaxUnit.call(this, parts.join(" "), line, col); - + /** * The parts that make up the selector. * @type Array @@ -3476,7 +3539,6 @@ function Selector(parts, line, col){ Selector.prototype = new SyntaxUnit(); Selector.prototype.constructor = Selector; - /** * Represents a single part of a selector string, meaning a single set of * element name and modifiers. This does not include combinators such as @@ -3489,12 +3551,12 @@ Selector.prototype.constructor = Selector; * if there is no element name. * @param {Array} modifiers Array of individual modifiers for the element. * May be empty if there are none. - * @param {String} text The text representation of the unit. + * @param {String} text The text representation of the unit. * @param {int} line The line of text on which the unit resides. * @param {int} col The column of text on which the unit resides. */ function SelectorPart(elementName, modifiers, text, line, col){ - + SyntaxUnit.call(this, text, line, col); /** @@ -3504,7 +3566,7 @@ function SelectorPart(elementName, modifiers, text, line, col){ * @property elementName */ this.elementName = elementName; - + /** * The parts that come after the element name, such as class names, IDs, * pseudo classes/elements, etc. @@ -3518,7 +3580,6 @@ function SelectorPart(elementName, modifiers, text, line, col){ SelectorPart.prototype = new SyntaxUnit(); SelectorPart.prototype.constructor = SelectorPart; - /** * Represents a selector modifier string, meaning a class name, element name, * element ID, pseudo rule, etc. @@ -3526,13 +3587,13 @@ SelectorPart.prototype.constructor = SelectorPart; * @class SelectorSubPart * @extends parserlib.util.SyntaxUnit * @constructor - * @param {String} text The text representation of the unit. + * @param {String} text The text representation of the unit. * @param {String} type The type of selector modifier. * @param {int} line The line of text on which the unit resides. * @param {int} col The column of text on which the unit resides. */ function SelectorSubPart(text, type, line, col){ - + SyntaxUnit.call(this, text, line, col); /** @@ -3541,7 +3602,7 @@ function SelectorSubPart(text, type, line, col){ * @property type */ this.type = type; - + /** * Some subparts have arguments, this represents them. * @type Array @@ -3555,8 +3616,7 @@ SelectorSubPart.prototype = new SyntaxUnit(); SelectorSubPart.prototype.constructor = SelectorSubPart; - - + var h = /^[0-9a-fA-F]$/, nonascii = /^[\u0080-\uFFFF]$/, nl = /\n|\r\n|\r|\f/; @@ -3564,8 +3624,8 @@ var h = /^[0-9a-fA-F]$/, //----------------------------------------------------------------------------- // Helper functions //----------------------------------------------------------------------------- - - + + function isHexDigit(c){ return c != null && h.test(c); } @@ -3631,19 +3691,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 +3717,8 @@ TokenStream.prototype = mix(new TokenStreamBase(), { } else { token = this.charToken(c, startLine, startCol); } - break; - + break; + /* * Potential tokens: * - DASHMATCH @@ -3678,8 +3738,8 @@ TokenStream.prototype = mix(new TokenStreamBase(), { } else { token = this.charToken(c, startLine, startCol); } - break; - + break; + /* * Potential tokens: * - STRING @@ -3687,9 +3747,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 +3757,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 +3772,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 +3795,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { token = this.charToken(c, startLine, startCol); } break; - + /* * Potential tokens: * - IMPORTANT_SYM @@ -3744,14 +3804,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 +3819,8 @@ TokenStream.prototype = mix(new TokenStreamBase(), { */ case ":": token = this.notToken(c, startLine, startCol); - break; - + break; + /* * Potential tokens: * - CDO @@ -3768,7 +3828,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { */ case "<": token = this.htmlCommentStartToken(c, startLine, startCol); - break; + break; /* * Potential tokens: @@ -3781,11 +3841,11 @@ TokenStream.prototype = mix(new TokenStreamBase(), { if (reader.peek() == "+"){ token = this.unicodeRangeToken(c, startLine, startCol); break; - } + } /*falls through*/ - + default: - + /* * Potential tokens: * - NUMBER @@ -3799,58 +3859,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 +3925,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 +3938,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 +3954,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 +3971,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 +4003,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 +4016,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 +4038,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 +4056,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 +4068,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 +4118,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 +4130,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 +4139,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 +4147,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 +4159,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 +4169,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 +4191,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 +4220,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,23 +4246,19 @@ 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)){ tt = Tokens.ANGLE; @@ -4220,10 +4276,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 +4292,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 +4300,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 +4314,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 +4366,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 +4379,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 +4437,7 @@ TokenStream.prototype = mix(new TokenStreamBase(), { number = first, hasDot = (first == "."), c = reader.peek(); - + while(c){ if (isDigit(c)){ @@ -4396,23 +4452,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 +4479,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 +4497,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,68 +4528,72 @@ 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(); } - + return ident; - }, + }, 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 ""; } - } -}); + + }, + + + +}); var Tokens = [ /* * The following token names are defined in CSS3 Grammar: http://www.w3.org/TR/css3-syntax/#lexical */ - + //HTML-style comments { name: "CDO"}, { name: "CDC"}, @@ -4529,16 +4601,16 @@ var Tokens = [ //ignorables { name: "S", whitespace: true/*, channel: "ws"*/}, { name: "COMMENT", comment: true, hide: true, channel: "comment" }, - + //attribute equality { name: "INCLUDES", text: "~="}, { name: "DASHMATCH", text: "|="}, { name: "PREFIXMATCH", text: "^="}, { name: "SUFFIXMATCH", text: "$="}, { name: "SUBSTRINGMATCH", text: "*="}, - + //identifier types - { name: "STRING"}, + { name: "STRING"}, { name: "IDENT"}, { name: "HASH"}, @@ -4564,30 +4636,30 @@ var Tokens = [ { name: "DIMENSION"}, { name: "PERCENTAGE"}, { name: "NUMBER"}, - + //functions { name: "URI"}, { name: "FUNCTION"}, - + //Unicode ranges { name: "UNICODE_RANGE"}, - + /* * The following token names are defined in CSS3 Selectors: http://www.w3.org/TR/css3-selectors/#selector-syntax - */ - + */ + //invalid string { name: "INVALID"}, - + //combinators { name: "PLUS", text: "+" }, { name: "GREATER", text: ">"}, { name: "COMMA", text: ","}, { name: "TILDE", text: "~"}, - + //modifier - { name: "NOT"}, - + { name: "NOT"}, + /* * Defined in CSS3 Paged Media */ @@ -4619,13 +4691,13 @@ var Tokens = [ /* * The following token names are not defined in any CSS specification but are used by the lexer. */ - + //not a real token, but useful for stupid IE filters { name: "IE_FUNCTION" }, //part of CSS3 grammar but not the Flex code { name: "CHAR" }, - + //TODO: Needed? //Not defined as tokens, but might as well be { @@ -4648,19 +4720,19 @@ var Tokens = [ { name: "LBRACE", text: "{" - }, + }, { name: "RBRACE", text: "}" - }, + }, { name: "LBRACKET", text: "[" - }, + }, { name: "RBRACKET", text: "]" - }, + }, { name: "EQUALS", text: "=" @@ -4668,20 +4740,20 @@ var Tokens = [ { name: "COLON", text: ":" - }, + }, { name: "SEMICOLON", text: ";" - }, - + }, + { name: "LPAREN", text: "(" - }, + }, { name: "RPAREN", text: ")" - }, + }, { name: "DOT", text: "." @@ -4692,7 +4764,7 @@ var Tokens = [ var nameMap = [], typeMap = {}; - + Tokens.UNKNOWN = -1; Tokens.unshift({name:"EOF"}); for (var i=0, len = Tokens.length; i < len; i++){ @@ -4702,11 +4774,11 @@ var Tokens = [ typeMap[Tokens[i].text] = i; } } - + Tokens.name = function(tt){ return nameMap[tt]; }; - + Tokens.type = function(c){ return typeMap[c] || -1; }; @@ -4716,11 +4788,9 @@ var Tokens = [ - - parserlib.css = { -Colors :Colors, -Combinator :Combinator, +Colors :Colors, +Combinator :Combinator, Parser :Parser, PropertyName :PropertyName, PropertyValue :PropertyValue, @@ -4734,9 +4804,6 @@ TokenStream :TokenStream, Tokens :Tokens }; })(); - - - /** * YUI Test Framework * @module yuitest @@ -9298,7 +9365,6 @@ YUITest.PageManager = YUITest.Util.mix(new YUITest.EventTarget(), { return new TestRunner(); }(); - /** * Main CSSLint object. * @class CSSLint @@ -9307,8 +9373,11 @@ YUITest.PageManager = YUITest.Util.mix(new YUITest.EventTarget(), { */ var CSSLint = (function(){ - var rules = [], - api = new parserlib.util.EventTarget(); + var rules = [], + formatters = [], + api = new parserlib.util.EventTarget(); + + api.version = "@VERSION@"; //------------------------------------------------------------------------- // Rule Management @@ -9333,21 +9402,76 @@ var CSSLint = (function(){ }; //------------------------------------------------------------------------- - // Verification + // Formatters //------------------------------------------------------------------------- /** - * 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 - * all rules are used. - * @return {Object} Results of the verification. - * @method verify + * Adds a new formatter to the engine. + * @param {Object} formatter The formatter to add. + * @method addFormatter */ - api.verify = function(text, options){ - - var i = 0, - len = rules.length, + 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} 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, ruleset){ + + var i = 0, + len = rules.length, reporter, lines, parser = new parserlib.css.Parser({ starHack: true, ieFilters: true, @@ -9356,13 +9480,14 @@ var CSSLint = (function(){ 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); } @@ -9383,7 +9508,6 @@ var CSSLint = (function(){ }; }; - //------------------------------------------------------------------------- // Publish the API //------------------------------------------------------------------------- @@ -9391,7 +9515,6 @@ var CSSLint = (function(){ return api; })(); - /** * An instance of Report is used to report results of the * verification back to the main API. @@ -9526,7 +9649,6 @@ Reporter.prototype = { this.stats[name] = value; } }; - /* * Utility functions that make life easier. */ @@ -9579,7 +9701,7 @@ CSSLint.addRule({ name: "Adjoining Classes", desc: "Don't use adjoining classes.", browsers: "IE6", - + //initialization init: function(parser, reporter){ var rule = this; @@ -9590,26 +9712,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); } } - } + } } } - }); + }); } }); @@ -9623,39 +9745,39 @@ 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, widthProperties = { border: 1, "border-left": 1, - "border-right": 1, + "border-right": 1, padding: 1, "padding-left": 1, - "padding-right": 1 + "padding-right": 1 }, heightProperties = { border: 1, "border-bottom": 1, - "border-top": 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.text.toLowerCase(); - if (heightProperties[name] || widthProperties){ - if (event.value != "0"){ - 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 }; } } else { if (name == "width" || name == "height"){ @@ -9664,33 +9786,201 @@ CSSLint.addRule({ } }); - - parser.addListener("endrule", function(event){ + + parser.addListener("endrule", function(){ var prop; if (properties["height"]){ for (prop in heightProperties){ if (heightProperties.hasOwnProperty(prop) && properties[prop]){ 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]){ 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-* */ @@ -9701,11 +9991,11 @@ 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": "none", @@ -9715,61 +10005,54 @@ CSSLint.addRule({ "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){ @@ -9778,25 +10061,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]){ if (!(typeof propertiesToCheck[name] == "string") || properties[name].value.toLowerCase() != propertiesToCheck[name]){ - reporter.warn(name + " can't be used with display: " + display + ".", properties[name].line, properties[name].col, rule); + 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. @@ -9808,26 +10134,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); } - }); + }); } }); @@ -9841,11 +10167,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); }); @@ -9864,27 +10190,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.text.toLowerCase() == "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); } - }); + }); } }); @@ -9898,22 +10224,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); } - }); + }); } }); @@ -9928,27 +10254,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); } - }); + }); } }); @@ -9962,13 +10287,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, @@ -9976,37 +10301,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); - } + } }); @@ -10023,7 +10348,7 @@ CSSLint.addRule({ name: "IDs", desc: "Selectors should not contain IDs.", browsers: "All", - + //initialization init: function(parser, reporter){ var rule = this; @@ -10034,12 +10359,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++){ @@ -10048,17 +10373,39 @@ CSSLint.addRule({ idCount++; } } - } + } } - + if (idCount == 1){ - reporter.warn("Don't use IDs in selectors.", selector.line, selector.col, rule); + 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); + 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); + }); - }); } }); @@ -10090,7 +10437,7 @@ CSSLint.addRule({ }); //report the results - parser.addListener("endstylesheet", function(event){ + parser.addListener("endstylesheet", function(){ reporter.stat("important", count); if (count >= 10){ reporter.rollupError("Too many !important declarations (" + count + "), be careful with rule specificity", rule); @@ -10109,36 +10456,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); + } + } + } + }); } }); @@ -10152,31 +10518,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); } - } + } } } - }); + }); } }); @@ -10190,21 +10555,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++){ @@ -10212,14 +10577,14 @@ CSSLint.addRule({ if (modifier.type == "attribute"){ if (/([\~\|\^\$\*]=)/.test(modifier)){ reporter.warn("Attribute selectors with " + RegExp.$1 + " are slow!", modifier.line, modifier.col, rule); - } + } } } - } + } } } - }); + }); } }); @@ -10233,23 +10598,55 @@ 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; + + 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); + } + }); + } + +});*/ /* * Rule: Headings (h1-h6) should be defined only once. */ @@ -10260,11 +10657,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, @@ -10273,26 +10670,25 @@ CSSLint.addRule({ h5: 0, h6: 0 }; - + parser.addListener("startrule", function(event){ var selectors = event.selectors, selector, part, - modifier, - i, j, k; - + 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())){ + 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); } - } + } } - }); + }); } }); @@ -10307,69 +10703,85 @@ 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" + }; + + //event handler for beginning of rules + function startRule(){ properties = {}; - num=1; - }); - - parser.addListener("property", function(event){ - var name = event.property.text.toLowerCase(), - 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[standard]){ - reporter.warn("Missing standard property '" + standard + "' to go along with '" + actual + "'.", event.selectors[0].line, event.selectors[0].col, rule); + if (!properties[needed]){ + 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[standard][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); + if (properties[needed][0].pos < properties[actual][0].pos){ + 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); } }); @@ -10377,21 +10789,22 @@ 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, width100, boxsizing; - - parser.addListener("startrule", function(event){ + + parser.addListener("startrule", function(){ width100 = null; boxsizing = false; }); @@ -10399,22 +10812,22 @@ CSSLint.addRule({ parser.addListener("property", function(event){ var name = event.property.text.toLowerCase(), value = event.value; - + if (name == "width" && value == "100%"){ 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(event){ + 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); + 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. */ @@ -10425,135 +10838,301 @@ 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++; } - + }); } }); +CSSLint.addFormatter({ + //format information + id: "lint-xml", + name: "Lint XML format", + + startFormat: function(){ + return ""; + }, -return CSSLint; -})(); + endFormat: function(){ + return ""; + }, + + formatResults: function(results, filename) { + var messages = results.messages, + output = []; -importPackage(java.io); + var replaceDoubleQuotes = function(str) { + if (!str || str.constructor !== String) { + return ""; + } + return str.replace(/\"/g, "'"); + }; -(function (argsArray) { - var files = [], - exitCode = 0; + if (messages.length > 0) { + //rollups at the bottom + messages.sort(function (a, b) { + if (a.rollup && !b.rollup) { + return 1; + } else if (!a.rollup && b.rollup) { + return -1; + } else { + return 0; + } + }); + + output.push(""); + messages.forEach(function (message, i) { + if (message.rollup) { + output.push(""); + } else { + output.push(""); + } + }); + output.push(""); + } - if (argsArray.length === 0) { - print("Usage: csslint-rhino.js [file|dir]*"); - quit(1); + return output.join(""); } +}); +CSSLint.addFormatter({ + //format information + id: "text", + name: "Plain Text", + + startFormat: function(){ + return ""; + }, + + endFormat: function(){ + return ""; + }, - var getFiles = function (dir) { - var files = []; + formatResults: function(results, filename) { + var messages = results.messages; + if (messages.length === 0) { + return "\n\ncsslint: No errors in " + filename + "."; + } + + output = "\n\ncsslint: There are " + messages.length + " problems in " + filename + "."; + var pos = filename.lastIndexOf("/"), + shortFilename = filename; - var traverse = function (dir) { - var dirList = dir.listFiles(); - dirList.forEach(function (file) { - if (/\.css$/.test(file)) { - files.push(file); - } else if (file.isDirectory()) { - traverse(file); - } - }); - }; + if (pos == -1){ + pos = filename.lastIndexOf("\\"); + } + if (pos > -1){ + shortFilename = filename.substring(pos+1); + } - traverse(dir); + //rollups at the bottom + messages.sort(function (a, b){ + if (a.rollup && !b.rollup){ + return 1; + } else if (!a.rollup && b.rollup){ + return -1; + } else { + return 0; + } + }); - return files; - }; + messages.forEach(function (message, i) { + output = output + "\n\n" + shortFilename; + if (message.rollup) { + output += "\n" + (i+1) + ": " + message.type; + output += "\n" + message.message; + } else { + output += "\n" + (i+1) + ": " + message.type + " at line " + message.line + ", col " + message.col; + output += "\n" + message.message; + output += "\n" + message.evidence; + } + }); + + return output; + } +}); + +return CSSLint; +})(); +//print for rhino and nodejs +if(typeof print == "undefined") { + var print = console.log; +} + +//readFile for rhino and nodejs +if(typeof readFile == "undefined") { + var readFile = function(filepath) { + var fs = require("fs"); + return fs.readFileSync(filepath, "utf-8"); + } +} + +//filter messages by type +var pluckByType = function(messages, type){ + return messages.filter(function(message) { + return message.type === type; + }); +}; - var pluckByType = function(messages, type) { - return messages.filter(function(message) { - return message.type == type; +function gatherRules(options){ + var ruleset; + + if (options.rules){ + ruleset = {}; + options.rules.split(",").forEach(function(value){ + ruleset[value] = 1; }); } + + return ruleset; +} - argsArray.forEach(function (arg) { - var curFile = new File(arg); +//process a list of files, return 1 if one or more error occurred +var processFile = function(filename, options) { + var input = readFile(filename), + result = CSSLint.verify(input, gatherRules(options)), + formatId = options.format || "text", + messages = result.messages || [], + exitCode = 0; + + if (!input) { + print("csslint: Could not read file data in " + filename + ". Is the file empty?"); + exitCode = 1; + } else { + print(CSSLint.getFormatter(formatId).formatResults(result, filename, formatId)); - if (!curFile.exists()) { - print("File or directory '" + arg + "' not found."); - return; + if (messages.length > 0 && pluckByType(messages, 'error').length > 0) { + exitCode = 1; } + } + + return exitCode; +}; - if (curFile.isDirectory()) { - files = files.concat(getFiles(curFile)); +//output CLI help screen +function outputHelp(){ + print([ + "\nUsage: csslint-rhino.js [options]* [file|dir]*", + " ", + "Global Options", + " --help Displays this information.", + " --rules= Indicate which rules to include.", + " --format= Indicate which format to use for output.", + " --version Outputs the current version number." + ].join("\n") + "\n\n"); +} + +function processFiles(files, options){ + var exitCode = 0, + formatId = options.format || "text", + formatter; + if (!files.length) { + print("No files specified."); + exitCode = 1; + } else { + if (!CSSLint.hasFormat(formatId)){ + print("csslint: Unknown format '" + formatId + "'. Cannot proceed."); + exitCode = 1; } else { - files.push(arg); + formatter = CSSLint.getFormatter(formatId); + print(formatter.startFormat()); + exitCode = files.some(function(file){ + processFile(file,options); + }); + print(formatter.endFormat()); } - }); + } + return exitCode; +} +/* + * CSSLint Rhino Command Line Interface + */ - files.forEach(function (filename) { - var input = readFile(filename); - if (!input) { - print("csslint: Could not read file data in " + filename + ". Is the file empty?"); - return; - } +importPackage(java.io); - var result = CSSLint.verify(input); - var messages = result.messages || [], - errors, - warnings; +//----------------------------------------------------------------------------- +// Helper Functions +//----------------------------------------------------------------------------- - if (messages.length > 0) { - warnings = pluckByType(messages, 'warning'); - errors = pluckByType(messages, 'error'); +function getFiles(dir) { + var files = []; + var traverse = function (dir) { + var dirList = dir.listFiles(); + dirList.forEach(function (file) { + if (/\.css$/.test(file)) { + files.push(file); + } else if (file.isDirectory()) { + traverse(file); + } + }); + }; - print("\n\n\ncsslint: There are " + errors.length + " errors and " + warnings.length + " warnings in " + filename + "."); + traverse(dir); - messages.sort(function (a, b) { - if (a.rollup && !b.rollup) { - return 1; - } else if (!a.rollup && b.rollup) { - return -1; - } else { - return 0; - } - }); + return files; +}; - messages.forEach(function (message, i) { - print("\n" + filename); - if (message.rollup) { - print("" + (i+1) + ": " + message.type); - print(message.message); - } else { - print("" + (i+1) + ": " + message.type + " at line " + message.line + ", col " + message.col); - print(message.message); - print(message.evidence); - } - }); +//----------------------------------------------------------------------------- +// Process command line +//----------------------------------------------------------------------------- - if(errors.length > 0) { - exitCode = 1; - } +var args = Array.prototype.slice.call(arguments), + argName, + arg = args.shift(), + options = {}, + files = []; +while(arg){ + if (arg.indexOf("--") == 0){ + argName = arg.substring(2); + options[argName] = true; + + if (argName.indexOf("rules=") > -1){ + options.rules = argName.substring(argName.indexOf("=") + 1); + } else if (argName.indexOf("format=") > -1) { + options.format = argName.substring(argName.indexOf("=") + 1); + } + } else { + var curFile = new File(arg); + + //see if it's a directory or a file + if (curFile.isDirectory()){ + files = files.concat(getFiles(arg)); } else { - print("csslint: No problems found in " + filename); + files.push(arg); } - }); + } + arg = args.shift(); +} + +if (options.help || arguments.length == 0){ + outputHelp(); + quit(0); +} + +if (options.version){ + print("v" + CSSLint.version); + quit(0); +} + - quit(exitCode); -})(Array.prototype.slice.call(arguments)); +quit(processFiles(files,options));