Owen Leonard
08900d5653
system preferences This patch adds CodeMirror plugins for linting JS, CSS, HTML, and YAML. When invalid data is entered in a linted CodeMirror editor an icon is displayed in the editor's "gutter." Hovering over the icon displays the error message. This patch renames the minified CodeMirror JS file to match convention but the version is unchanged. To test, apply the patch and go to Administration -> System preferences. Test preferences of each type and confirm that each type of CodeMirror editor shows an error indicator if you entry invalid data. Valid data should trigger no error indicator. - HTML: e.g. OpacMainUserBlock, opacheader. Enter invalid HTML, for example "<h1>Hello <h2>World</h2>." Example valid HTML: "<h1>Hello world</h1>" - JavaScript: e.g. OpacUserJS, IntranetUserJS. Example bad JS, "alert("Success!');" Example valid JS: "alert("Success!");" - CSS: e.g. IntranetUserCSS, SCOUserCSS. Example bad CSS, "p { color blue }" Example valid CSS, "p { color: blue; }" - YAML: e.g. OpacHiddenItems. Example bad YAML: "one: two, three: four" Example valid YAML: "one: two three: four" Also test that other CodeMirror instances still work correctly without linting: The advanced MARC editor, SQL reports editing. Signed-off-by: David Nind <david@davidnind.com> Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de> Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
9126 lines
293 KiB
JavaScript
9126 lines
293 KiB
JavaScript
/*!
|
|
CSSLint
|
|
Copyright (c) 2011 Nicole Sullivan and 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
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
*/
|
|
/* Build time: 14-May-2012 10:24:48 */
|
|
var CSSLint = (function(){
|
|
|
|
/*!
|
|
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
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
*/
|
|
/* Version v0.1.7, Build time: 4-May-2012 03:57:04 */
|
|
var parserlib = {};
|
|
(function(){
|
|
|
|
|
|
/**
|
|
* A generic base to inherit from for any object
|
|
* that needs event handling.
|
|
* @class EventTarget
|
|
* @constructor
|
|
*/
|
|
function EventTarget(){
|
|
|
|
/**
|
|
* The array of listeners for various events.
|
|
* @type Object
|
|
* @property _listeners
|
|
* @private
|
|
*/
|
|
this._listeners = {};
|
|
}
|
|
|
|
EventTarget.prototype = {
|
|
|
|
//restore constructor
|
|
constructor: EventTarget,
|
|
|
|
/**
|
|
* Adds a listener for a given event type.
|
|
* @param {String} type The type of event to add a listener for.
|
|
* @param {Function} listener The function to call when the event occurs.
|
|
* @return {void}
|
|
* @method addListener
|
|
*/
|
|
addListener: function(type, listener){
|
|
if (!this._listeners[type]){
|
|
this._listeners[type] = [];
|
|
}
|
|
|
|
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 };
|
|
}
|
|
if (typeof event.target != "undefined"){
|
|
event.target = this;
|
|
}
|
|
|
|
if (typeof event.type == "undefined"){
|
|
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);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes a listener for a given event type.
|
|
* @param {String} type The type of event to remove a listener from.
|
|
* @param {Function} listener The function to remove from the event.
|
|
* @return {void}
|
|
* @method removeListener
|
|
*/
|
|
removeListener: function(type, listener){
|
|
if (this._listeners[type]){
|
|
var listeners = this._listeners[type];
|
|
for (var i=0, len=listeners.length; i < len; i++){
|
|
if (listeners[i] === listener){
|
|
listeners.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* Convenient way to read through strings.
|
|
* @namespace parserlib.util
|
|
* @class StringReader
|
|
* @constructor
|
|
* @param {String} text The text to read.
|
|
*/
|
|
function StringReader(text){
|
|
|
|
/**
|
|
* The input text with line endings normalized.
|
|
* @property _input
|
|
* @type String
|
|
* @private
|
|
*/
|
|
this._input = text.replace(/\n\r?/g, "\n");
|
|
|
|
|
|
/**
|
|
* The row for the character to be read next.
|
|
* @property _line
|
|
* @type int
|
|
* @private
|
|
*/
|
|
this._line = 1;
|
|
|
|
|
|
/**
|
|
* The column for the character to be read next.
|
|
* @property _col
|
|
* @type int
|
|
* @private
|
|
*/
|
|
this._col = 1;
|
|
|
|
/**
|
|
* The index of the character in the input to be read next.
|
|
* @property _cursor
|
|
* @type int
|
|
* @private
|
|
*/
|
|
this._cursor = 0;
|
|
}
|
|
|
|
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.
|
|
* @method getCol
|
|
*/
|
|
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);
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Basic reading
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Reads the next character without advancing the cursor.
|
|
* @param {int} count How many characters to look ahead (default is 1).
|
|
* @return {String} The next character or null if there is no next character.
|
|
* @method peek
|
|
*/
|
|
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){
|
|
|
|
//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.
|
|
* @return {String} The next character or null if there is no next character.
|
|
* @method read
|
|
*/
|
|
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"){
|
|
this._line++;
|
|
this._col=1;
|
|
} 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
|
|
* @return {void}
|
|
*/
|
|
mark: function(){
|
|
this._bookmark = {
|
|
cursor: this._cursor,
|
|
line: this._line,
|
|
col: this._col
|
|
};
|
|
},
|
|
|
|
reset: function(){
|
|
if (this._bookmark){
|
|
this._cursor = this._bookmark.cursor;
|
|
this._line = this._bookmark.line;
|
|
this._col = this._bookmark.col;
|
|
delete this._bookmark;
|
|
}
|
|
},
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Advanced reading
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Reads up to and including the given string. Throws an error if that
|
|
* string is not found.
|
|
* @param {String} pattern The string to read.
|
|
* @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;
|
|
|
|
/*
|
|
* First, buffer must be the same length as the pattern.
|
|
* Then, buffer must end with the pattern or else reach the
|
|
* end of the input.
|
|
*/
|
|
while (buffer.length < pattern.length || buffer.lastIndexOf(pattern) != buffer.length - pattern.length){
|
|
c = this.read();
|
|
if (c){
|
|
buffer += c;
|
|
} else {
|
|
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
|
|
* in each character and either returns true to continue
|
|
* reading or false to stop.
|
|
* @param {Function} filter The function to read on each character.
|
|
* @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
|
|
* are adjusted; if no match is found, the reader's state is unchanged.
|
|
* reading or false to stop.
|
|
* @param {String|RegExp} matchter If a string, then the literal string
|
|
* value is searched for. If a regular expression, then any string
|
|
* matching the pattern is search for.
|
|
* @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);
|
|
}
|
|
} else if (matcher instanceof RegExp){
|
|
if (matcher.test(source)){
|
|
value = this.readCount(RegExp.lastMatch.length);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
};
|
|
/**
|
|
* Type to use when a syntax error occurs.
|
|
* @class SyntaxError
|
|
* @namespace parserlib.util
|
|
* @constructor
|
|
* @param {String} message The error message.
|
|
* @param {int} line The line at which the error occurred.
|
|
* @param {int} col The column at which the error occurred.
|
|
*/
|
|
function SyntaxError(message, line, col){
|
|
|
|
/**
|
|
* The column at which the error occurred.
|
|
* @type int
|
|
* @property col
|
|
*/
|
|
this.col = col;
|
|
|
|
/**
|
|
* The line at which the error occurred.
|
|
* @type int
|
|
* @property line
|
|
*/
|
|
this.line = line;
|
|
|
|
/**
|
|
* The text representation of the unit.
|
|
* @type String
|
|
* @property text
|
|
*/
|
|
this.message = message;
|
|
|
|
}
|
|
|
|
//inherit from Error
|
|
SyntaxError.prototype = new Error();
|
|
/**
|
|
* Base type to represent a single syntactic unit.
|
|
* @class SyntaxUnit
|
|
* @namespace parserlib.util
|
|
* @constructor
|
|
* @param {String} text The text 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 SyntaxUnit(text, line, col, type){
|
|
|
|
|
|
/**
|
|
* The column of text on which the unit resides.
|
|
* @type int
|
|
* @property col
|
|
*/
|
|
this.col = col;
|
|
|
|
/**
|
|
* The line of text on which the unit resides.
|
|
* @type int
|
|
* @property line
|
|
*/
|
|
this.line = line;
|
|
|
|
/**
|
|
* The text representation of the unit.
|
|
* @type String
|
|
* @property text
|
|
*/
|
|
this.text = text;
|
|
|
|
/**
|
|
* The type of syntax unit.
|
|
* @type int
|
|
* @property type
|
|
*/
|
|
this.type = type;
|
|
}
|
|
|
|
/**
|
|
* Create a new syntax unit based solely on the given token.
|
|
* Convenience method for creating a new syntax unit when
|
|
* it represents a single token instead of multiple.
|
|
* @param {Object} token The token object to represent.
|
|
* @return {parserlib.util.SyntaxUnit} The object representing the token.
|
|
* @static
|
|
* @method fromToken
|
|
*/
|
|
SyntaxUnit.fromToken = function(token){
|
|
return new SyntaxUnit(token.value, token.startLine, token.startCol);
|
|
};
|
|
|
|
SyntaxUnit.prototype = {
|
|
|
|
//restore constructor
|
|
constructor: SyntaxUnit,
|
|
|
|
/**
|
|
* Returns the text representation of the unit.
|
|
* @return {String} The text representation of the unit.
|
|
* @method valueOf
|
|
*/
|
|
valueOf: function(){
|
|
return this.toString();
|
|
},
|
|
|
|
/**
|
|
* Returns the text representation of the unit.
|
|
* @return {String} The text representation of the unit.
|
|
* @method toString
|
|
*/
|
|
toString: function(){
|
|
return this.text;
|
|
}
|
|
|
|
};
|
|
/*global StringReader, SyntaxError*/
|
|
|
|
/**
|
|
* Generic TokenStream providing base functionality.
|
|
* @class TokenStreamBase
|
|
* @namespace parserlib.util
|
|
* @constructor
|
|
* @param {String|StringReader} input The text to tokenize or a reader from
|
|
* which to read the input.
|
|
*/
|
|
function TokenStreamBase(input, tokenData){
|
|
|
|
/**
|
|
* The string reader for easy access to the text.
|
|
* @type StringReader
|
|
* @property _reader
|
|
* @private
|
|
*/
|
|
this._reader = input ? new StringReader(input.toString()) : null;
|
|
|
|
/**
|
|
* Token object for the last consumed token.
|
|
* @type Token
|
|
* @property _token
|
|
* @private
|
|
*/
|
|
this._token = null;
|
|
|
|
/**
|
|
* The array of token information.
|
|
* @type Array
|
|
* @property _tokenData
|
|
* @private
|
|
*/
|
|
this._tokenData = tokenData;
|
|
|
|
/**
|
|
* Lookahead token buffer.
|
|
* @type Array
|
|
* @property _lt
|
|
* @private
|
|
*/
|
|
this._lt = [];
|
|
|
|
/**
|
|
* Lookahead token buffer index.
|
|
* @type int
|
|
* @property _ltIndex
|
|
* @private
|
|
*/
|
|
this._ltIndex = 0;
|
|
|
|
this._ltIndexCache = [];
|
|
}
|
|
|
|
/**
|
|
* Accepts an array of token information and outputs
|
|
* an array of token data containing key-value mappings
|
|
* and matching functions that the TokenStream needs.
|
|
* @param {Array} tokens An array of token descriptors.
|
|
* @return {Array} An array of processed token data.
|
|
* @method createTokenData
|
|
* @static
|
|
*/
|
|
TokenStreamBase.createTokenData = function(tokens){
|
|
|
|
var nameMap = [],
|
|
typeMap = {},
|
|
tokenData = tokens.concat([]),
|
|
i = 0,
|
|
len = tokenData.length+1;
|
|
|
|
tokenData.UNKNOWN = -1;
|
|
tokenData.unshift({name:"EOF"});
|
|
|
|
for (; i < len; i++){
|
|
nameMap.push(tokenData[i].name);
|
|
tokenData[tokenData[i].name] = i;
|
|
if (tokenData[i].text){
|
|
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,
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Matching methods
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Determines if the next token matches the given token type.
|
|
* If so, that token is consumed; if not, the token is placed
|
|
* back onto the token stream. You can pass in any number of
|
|
* token types and this will return true if any of the token
|
|
* types is found.
|
|
* @param {int|int[]} tokenTypes Either a single token type or an array of
|
|
* token types that the next token might be. If an array is passed,
|
|
* it's assumed that the token can be any of these.
|
|
* @param {variant} channel (Optional) The channel to read from. If not
|
|
* provided, reads from the default (unnamed) channel.
|
|
* @return {Boolean} True if the token type matches, false if not.
|
|
* @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.
|
|
* @param {int|int[]} tokenTypes Either a single token type or an array of
|
|
* token types that the next token should be. If an array is passed,
|
|
* it's assumed that the token must be one of these.
|
|
* @param {variant} channel (Optional) The channel to read from. If not
|
|
* provided, reads from the default (unnamed) channel.
|
|
* @return {void}
|
|
* @method mustMatch
|
|
*/
|
|
mustMatch: function(tokenTypes, channel){
|
|
|
|
var token;
|
|
|
|
//always convert to an array, makes things easier
|
|
if (!(tokenTypes instanceof Array)){
|
|
tokenTypes = [tokenTypes];
|
|
}
|
|
|
|
if (!this.match.apply(this, arguments)){
|
|
token = this.LT(1);
|
|
throw new SyntaxError("Expected " + this._tokenData[tokenTypes[0]].name +
|
|
" at line " + token.startLine + ", col " + 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.
|
|
* @param {int|int[]} tokenTypes Either a single token type or an array of
|
|
* token types that the next token should be. If an array is passed,
|
|
* it's assumed that the token must be one of these.
|
|
* @param {variant} channel (Optional) The channel to read from. If not
|
|
* provided, reads from the default (unnamed) channel.
|
|
* @return {void}
|
|
* @method advance
|
|
*/
|
|
advance: function(tokenTypes, channel){
|
|
|
|
while(this.LA(0) !== 0 && !this.match(tokenTypes, channel)){
|
|
this.get();
|
|
}
|
|
|
|
return this.LA(0);
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
i =0,
|
|
len = tokenInfo.length,
|
|
found = false,
|
|
token,
|
|
info;
|
|
|
|
//check the lookahead buffer first
|
|
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){
|
|
this._token = this._lt[this._ltIndex++];
|
|
info = tokenInfo[this._token.type];
|
|
i++;
|
|
}
|
|
|
|
//here be dragons
|
|
if ((info.channel === undefined || channel === info.channel) &&
|
|
this._ltIndex <= this._lt.length){
|
|
this._ltIndexCache.push(i);
|
|
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);
|
|
|
|
//keep the buffer under 5 items
|
|
if (this._lt.length > 5){
|
|
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 ||
|
|
(info.channel !== undefined && channel !== info.channel))){
|
|
return this.get(channel);
|
|
} else {
|
|
//return just the type
|
|
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
|
|
* end of input, past the size of the lookahead buffer, or back past
|
|
* the first token in the lookahead buffer.
|
|
* @param {int} The index of the token type to retrieve. 0 for the
|
|
* current token, 1 for the next, -1 for the previous, etc.
|
|
* @return {int} The token type of the token in the given position.
|
|
* @method LA
|
|
*/
|
|
LA: function(index){
|
|
var total = index,
|
|
tt;
|
|
if (index > 0){
|
|
//TODO: Store 5 somewhere
|
|
if (index > 5){
|
|
throw new Error("Too much lookahead.");
|
|
}
|
|
|
|
//get all those tokens
|
|
while(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
|
|
* end of input, past the size of the lookahead buffer, or back past
|
|
* the first token in the lookahead buffer.
|
|
* @param {int} The index of the token type to retrieve. 0 for the
|
|
* 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];
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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.
|
|
* @method token
|
|
*/
|
|
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.
|
|
* @return {String} The name of the token or "UNKNOWN_TOKEN" for any
|
|
* invalid token type.
|
|
* @method tokenName
|
|
*/
|
|
tokenName: function(tokenType){
|
|
if (tokenType < 0 || tokenType > this._tokenData.length){
|
|
return "UNKNOWN_TOKEN";
|
|
} else {
|
|
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){
|
|
this._ltIndex -= this._ltIndexCache.pop();//--;
|
|
this._token = this._lt[this._ltIndex - 1];
|
|
} else {
|
|
throw new Error("Too much lookahead.");
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
parserlib.util = {
|
|
StringReader: StringReader,
|
|
SyntaxError : SyntaxError,
|
|
SyntaxUnit : SyntaxUnit,
|
|
EventTarget : EventTarget,
|
|
TokenStreamBase : TokenStreamBase
|
|
};
|
|
})();
|
|
|
|
|
|
/*
|
|
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
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
*/
|
|
/* Version v0.1.7, Build time: 4-May-2012 03:57:04 */
|
|
(function(){
|
|
var EventTarget = parserlib.util.EventTarget,
|
|
TokenStreamBase = parserlib.util.TokenStreamBase,
|
|
StringReader = parserlib.util.StringReader,
|
|
SyntaxError = parserlib.util.SyntaxError,
|
|
SyntaxUnit = parserlib.util.SyntaxUnit;
|
|
|
|
|
|
var Colors = {
|
|
aliceblue :"#f0f8ff",
|
|
antiquewhite :"#faebd7",
|
|
aqua :"#00ffff",
|
|
aquamarine :"#7fffd4",
|
|
azure :"#f0ffff",
|
|
beige :"#f5f5dc",
|
|
bisque :"#ffe4c4",
|
|
black :"#000000",
|
|
blanchedalmond :"#ffebcd",
|
|
blue :"#0000ff",
|
|
blueviolet :"#8a2be2",
|
|
brown :"#a52a2a",
|
|
burlywood :"#deb887",
|
|
cadetblue :"#5f9ea0",
|
|
chartreuse :"#7fff00",
|
|
chocolate :"#d2691e",
|
|
coral :"#ff7f50",
|
|
cornflowerblue :"#6495ed",
|
|
cornsilk :"#fff8dc",
|
|
crimson :"#dc143c",
|
|
cyan :"#00ffff",
|
|
darkblue :"#00008b",
|
|
darkcyan :"#008b8b",
|
|
darkgoldenrod :"#b8860b",
|
|
darkgray :"#a9a9a9",
|
|
darkgreen :"#006400",
|
|
darkkhaki :"#bdb76b",
|
|
darkmagenta :"#8b008b",
|
|
darkolivegreen :"#556b2f",
|
|
darkorange :"#ff8c00",
|
|
darkorchid :"#9932cc",
|
|
darkred :"#8b0000",
|
|
darksalmon :"#e9967a",
|
|
darkseagreen :"#8fbc8f",
|
|
darkslateblue :"#483d8b",
|
|
darkslategray :"#2f4f4f",
|
|
darkturquoise :"#00ced1",
|
|
darkviolet :"#9400d3",
|
|
deeppink :"#ff1493",
|
|
deepskyblue :"#00bfff",
|
|
dimgray :"#696969",
|
|
dodgerblue :"#1e90ff",
|
|
firebrick :"#b22222",
|
|
floralwhite :"#fffaf0",
|
|
forestgreen :"#228b22",
|
|
fuchsia :"#ff00ff",
|
|
gainsboro :"#dcdcdc",
|
|
ghostwhite :"#f8f8ff",
|
|
gold :"#ffd700",
|
|
goldenrod :"#daa520",
|
|
gray :"#808080",
|
|
green :"#008000",
|
|
greenyellow :"#adff2f",
|
|
honeydew :"#f0fff0",
|
|
hotpink :"#ff69b4",
|
|
indianred :"#cd5c5c",
|
|
indigo :"#4b0082",
|
|
ivory :"#fffff0",
|
|
khaki :"#f0e68c",
|
|
lavender :"#e6e6fa",
|
|
lavenderblush :"#fff0f5",
|
|
lawngreen :"#7cfc00",
|
|
lemonchiffon :"#fffacd",
|
|
lightblue :"#add8e6",
|
|
lightcoral :"#f08080",
|
|
lightcyan :"#e0ffff",
|
|
lightgoldenrodyellow :"#fafad2",
|
|
lightgray :"#d3d3d3",
|
|
lightgreen :"#90ee90",
|
|
lightpink :"#ffb6c1",
|
|
lightsalmon :"#ffa07a",
|
|
lightseagreen :"#20b2aa",
|
|
lightskyblue :"#87cefa",
|
|
lightslategray :"#778899",
|
|
lightsteelblue :"#b0c4de",
|
|
lightyellow :"#ffffe0",
|
|
lime :"#00ff00",
|
|
limegreen :"#32cd32",
|
|
linen :"#faf0e6",
|
|
magenta :"#ff00ff",
|
|
maroon :"#800000",
|
|
mediumaquamarine:"#66cdaa",
|
|
mediumblue :"#0000cd",
|
|
mediumorchid :"#ba55d3",
|
|
mediumpurple :"#9370d8",
|
|
mediumseagreen :"#3cb371",
|
|
mediumslateblue :"#7b68ee",
|
|
mediumspringgreen :"#00fa9a",
|
|
mediumturquoise :"#48d1cc",
|
|
mediumvioletred :"#c71585",
|
|
midnightblue :"#191970",
|
|
mintcream :"#f5fffa",
|
|
mistyrose :"#ffe4e1",
|
|
moccasin :"#ffe4b5",
|
|
navajowhite :"#ffdead",
|
|
navy :"#000080",
|
|
oldlace :"#fdf5e6",
|
|
olive :"#808000",
|
|
olivedrab :"#6b8e23",
|
|
orange :"#ffa500",
|
|
orangered :"#ff4500",
|
|
orchid :"#da70d6",
|
|
palegoldenrod :"#eee8aa",
|
|
palegreen :"#98fb98",
|
|
paleturquoise :"#afeeee",
|
|
palevioletred :"#d87093",
|
|
papayawhip :"#ffefd5",
|
|
peachpuff :"#ffdab9",
|
|
peru :"#cd853f",
|
|
pink :"#ffc0cb",
|
|
plum :"#dda0dd",
|
|
powderblue :"#b0e0e6",
|
|
purple :"#800080",
|
|
red :"#ff0000",
|
|
rosybrown :"#bc8f8f",
|
|
royalblue :"#4169e1",
|
|
saddlebrown :"#8b4513",
|
|
salmon :"#fa8072",
|
|
sandybrown :"#f4a460",
|
|
seagreen :"#2e8b57",
|
|
seashell :"#fff5ee",
|
|
sienna :"#a0522d",
|
|
silver :"#c0c0c0",
|
|
skyblue :"#87ceeb",
|
|
slateblue :"#6a5acd",
|
|
slategray :"#708090",
|
|
snow :"#fffafa",
|
|
springgreen :"#00ff7f",
|
|
steelblue :"#4682b4",
|
|
tan :"#d2b48c",
|
|
teal :"#008080",
|
|
thistle :"#d8bfd8",
|
|
tomato :"#ff6347",
|
|
turquoise :"#40e0d0",
|
|
violet :"#ee82ee",
|
|
wheat :"#f5deb3",
|
|
white :"#ffffff",
|
|
whitesmoke :"#f5f5f5",
|
|
yellow :"#ffff00",
|
|
yellowgreen :"#9acd32"
|
|
};
|
|
/*global SyntaxUnit, Parser*/
|
|
/**
|
|
* Represents a selector combinator (whitespace, +, >).
|
|
* @namespace parserlib.css
|
|
* @class Combinator
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
* @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, Parser.COMBINATOR_TYPE);
|
|
|
|
/**
|
|
* The type of modifier.
|
|
* @type String
|
|
* @property type
|
|
*/
|
|
this.type = "unknown";
|
|
|
|
//pretty simple
|
|
if (/^\s+$/.test(text)){
|
|
this.type = "descendant";
|
|
} else if (text == ">"){
|
|
this.type = "child";
|
|
} else if (text == "+"){
|
|
this.type = "adjacent-sibling";
|
|
} else if (text == "~"){
|
|
this.type = "sibling";
|
|
}
|
|
|
|
}
|
|
|
|
Combinator.prototype = new SyntaxUnit();
|
|
Combinator.prototype.constructor = Combinator;
|
|
|
|
|
|
/*global SyntaxUnit, Parser*/
|
|
/**
|
|
* Represents a media feature, such as max-width:500.
|
|
* @namespace parserlib.css
|
|
* @class MediaFeature
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
* @param {SyntaxUnit} name The name of the feature.
|
|
* @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, Parser.MEDIA_FEATURE_TYPE);
|
|
|
|
/**
|
|
* The name of the media feature
|
|
* @type String
|
|
* @property name
|
|
*/
|
|
this.name = name;
|
|
|
|
/**
|
|
* The value for the feature or null if there is none.
|
|
* @type SyntaxUnit
|
|
* @property value
|
|
*/
|
|
this.value = value;
|
|
}
|
|
|
|
MediaFeature.prototype = new SyntaxUnit();
|
|
MediaFeature.prototype.constructor = MediaFeature;
|
|
|
|
|
|
/*global SyntaxUnit, Parser*/
|
|
/**
|
|
* Represents an individual media query.
|
|
* @namespace parserlib.css
|
|
* @class MediaQuery
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
* @param {String} modifier The modifier "not" or "only" (or null).
|
|
* @param {String} mediaType The type of media (i.e., "print").
|
|
* @param {Array} parts Array of selectors parts making up this selector.
|
|
* @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 MediaQuery(modifier, mediaType, features, line, col){
|
|
|
|
SyntaxUnit.call(this, (modifier ? modifier + " ": "") + (mediaType ? mediaType + " " : "") + features.join(" and "), line, col, Parser.MEDIA_QUERY_TYPE);
|
|
|
|
/**
|
|
* The media modifier ("not" or "only")
|
|
* @type String
|
|
* @property modifier
|
|
*/
|
|
this.modifier = modifier;
|
|
|
|
/**
|
|
* The mediaType (i.e., "print")
|
|
* @type String
|
|
* @property mediaType
|
|
*/
|
|
this.mediaType = mediaType;
|
|
|
|
/**
|
|
* The parts that make up the selector.
|
|
* @type Array
|
|
* @property features
|
|
*/
|
|
this.features = features;
|
|
|
|
}
|
|
|
|
MediaQuery.prototype = new SyntaxUnit();
|
|
MediaQuery.prototype.constructor = MediaQuery;
|
|
|
|
|
|
/*global Tokens, TokenStream, SyntaxError, Properties, Validation, ValidationError, SyntaxUnit,
|
|
PropertyValue, PropertyValuePart, SelectorPart, SelectorSubPart, Selector,
|
|
PropertyName, Combinator, MediaFeature, MediaQuery, EventTarget */
|
|
|
|
/**
|
|
* A CSS3 parser.
|
|
* @namespace parserlib.css
|
|
* @class Parser
|
|
* @constructor
|
|
* @param {Object} options (Optional) Various options for the parser:
|
|
* starHack (true|false) to allow IE6 star hack as valid,
|
|
* underscoreHack (true|false) to interpret leading underscores
|
|
* as IE6-7 targeting for known properties, ieFilters (true|false)
|
|
* to indicate that IE < 8 filters should be accepted and not throw
|
|
* syntax errors.
|
|
*/
|
|
function Parser(options){
|
|
|
|
//inherit event functionality
|
|
EventTarget.call(this);
|
|
|
|
|
|
this.options = options || {};
|
|
|
|
this._tokenStream = null;
|
|
}
|
|
|
|
//Static constants
|
|
Parser.DEFAULT_TYPE = 0;
|
|
Parser.COMBINATOR_TYPE = 1;
|
|
Parser.MEDIA_FEATURE_TYPE = 2;
|
|
Parser.MEDIA_QUERY_TYPE = 3;
|
|
Parser.PROPERTY_NAME_TYPE = 4;
|
|
Parser.PROPERTY_VALUE_TYPE = 5;
|
|
Parser.PROPERTY_VALUE_PART_TYPE = 6;
|
|
Parser.SELECTOR_TYPE = 7;
|
|
Parser.SELECTOR_PART_TYPE = 8;
|
|
Parser.SELECTOR_SUB_PART_TYPE = 9;
|
|
|
|
Parser.prototype = function(){
|
|
|
|
var proto = new EventTarget(), //new prototype
|
|
prop,
|
|
additions = {
|
|
|
|
//restore constructor
|
|
constructor: Parser,
|
|
|
|
//instance constants - yuck
|
|
DEFAULT_TYPE : 0,
|
|
COMBINATOR_TYPE : 1,
|
|
MEDIA_FEATURE_TYPE : 2,
|
|
MEDIA_QUERY_TYPE : 3,
|
|
PROPERTY_NAME_TYPE : 4,
|
|
PROPERTY_VALUE_TYPE : 5,
|
|
PROPERTY_VALUE_PART_TYPE : 6,
|
|
SELECTOR_TYPE : 7,
|
|
SELECTOR_PART_TYPE : 8,
|
|
SELECTOR_SUB_PART_TYPE : 9,
|
|
|
|
//-----------------------------------------------------------------
|
|
// Grammar
|
|
//-----------------------------------------------------------------
|
|
|
|
_stylesheet: function(){
|
|
|
|
/*
|
|
* stylesheet
|
|
* : [ CHARSET_SYM S* STRING S* ';' ]?
|
|
* [S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
|
|
* [ namespace [S|CDO|CDC]* ]*
|
|
* [ [ ruleset | media | page | font_face | keyframes ] [S|CDO|CDC]* ]*
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
charset = null,
|
|
count,
|
|
token,
|
|
tt;
|
|
|
|
this.fire("startstylesheet");
|
|
|
|
//try to read character set
|
|
this._charset();
|
|
|
|
this._skipCruft();
|
|
|
|
//try to read imports - may be more than one
|
|
while (tokenStream.peek() == Tokens.IMPORT_SYM){
|
|
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._skipCruft();
|
|
break;
|
|
case Tokens.FONT_FACE_SYM:
|
|
this._font_face();
|
|
this._skipCruft();
|
|
break;
|
|
case Tokens.KEYFRAMES_SYM:
|
|
this._keyframes();
|
|
this._skipCruft();
|
|
break;
|
|
case Tokens.UNKNOWN_SYM: //unknown @ rule
|
|
tokenStream.get();
|
|
if (!this.options.strict){
|
|
|
|
//fire error event
|
|
this.fire({
|
|
type: "error",
|
|
error: null,
|
|
message: "Unknown @ rule: " + tokenStream.LT(0).value + ".",
|
|
line: tokenStream.LT(0).startLine,
|
|
col: tokenStream.LT(0).startCol
|
|
});
|
|
|
|
//skip braces
|
|
count=0;
|
|
while (tokenStream.advance([Tokens.LBRACE, Tokens.RBRACE]) == Tokens.LBRACE){
|
|
count++; //keep track of nesting depth
|
|
}
|
|
|
|
while(count){
|
|
tokenStream.advance([Tokens.RBRACE]);
|
|
count--;
|
|
}
|
|
|
|
} else {
|
|
//not a syntax error, rethrow it
|
|
throw new SyntaxError("Unknown @ rule.", tokenStream.LT(0).startLine, tokenStream.LT(0).startCol);
|
|
}
|
|
break;
|
|
case Tokens.S:
|
|
this._readWhitespace();
|
|
break;
|
|
default:
|
|
if(!this._ruleset()){
|
|
|
|
//error handling for known issues
|
|
switch(tt){
|
|
case Tokens.CHARSET_SYM:
|
|
token = tokenStream.LT(1);
|
|
this._charset(false);
|
|
throw new SyntaxError("@charset not allowed here.", token.startLine, token.startCol);
|
|
case Tokens.IMPORT_SYM:
|
|
token = tokenStream.LT(1);
|
|
this._import(false);
|
|
throw new SyntaxError("@import not allowed here.", token.startLine, token.startCol);
|
|
case Tokens.NAMESPACE_SYM:
|
|
token = tokenStream.LT(1);
|
|
this._namespace(false);
|
|
throw new SyntaxError("@namespace not allowed here.", token.startLine, token.startCol);
|
|
default:
|
|
tokenStream.get(); //get the last token
|
|
this._unexpectedToken(tokenStream.token());
|
|
}
|
|
|
|
}
|
|
}
|
|
} catch(ex) {
|
|
if (ex instanceof SyntaxError && !this.options.strict){
|
|
this.fire({
|
|
type: "error",
|
|
error: ex,
|
|
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,
|
|
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({
|
|
type: "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");
|
|
|
|
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,
|
|
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");
|
|
|
|
this._readWhitespace();
|
|
|
|
//must end with a semicolon
|
|
tokenStream.mustMatch(Tokens.SEMICOLON);
|
|
this._readWhitespace();
|
|
|
|
if (emit !== false){
|
|
this.fire({
|
|
type: "namespace",
|
|
prefix: prefix,
|
|
uri: uri,
|
|
line: line,
|
|
col: col
|
|
});
|
|
}
|
|
|
|
},
|
|
|
|
_media: function(){
|
|
/*
|
|
* media
|
|
* : MEDIA_SYM S* media_query_list S* '{' S* ruleset* '}' S*
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
line,
|
|
col,
|
|
mediaList;// = [];
|
|
|
|
//look for @media
|
|
tokenStream.mustMatch(Tokens.MEDIA_SYM);
|
|
line = tokenStream.token().startLine;
|
|
col = tokenStream.token().startCol;
|
|
|
|
this._readWhitespace();
|
|
|
|
mediaList = this._media_query_list();
|
|
|
|
tokenStream.mustMatch(Tokens.LBRACE);
|
|
this._readWhitespace();
|
|
|
|
this.fire({
|
|
type: "startmedia",
|
|
media: mediaList,
|
|
line: line,
|
|
col: col
|
|
});
|
|
|
|
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,
|
|
line: line,
|
|
col: col
|
|
});
|
|
},
|
|
|
|
|
|
//CSS3 Media Queries
|
|
_media_query_list: function(){
|
|
/*
|
|
* media_query_list
|
|
* : S* [media_query [ ',' S* media_query ]* ]?
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
mediaList = [];
|
|
|
|
|
|
this._readWhitespace();
|
|
|
|
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(){
|
|
/*
|
|
* media_query
|
|
* : [ONLY | NOT]? S* media_type S* [ AND S* expression ]*
|
|
* | expression [ AND S* expression ]*
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
type = null,
|
|
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();
|
|
ident = null;
|
|
} else {
|
|
token = tokenStream.token();
|
|
}
|
|
}
|
|
|
|
this._readWhitespace();
|
|
|
|
if (tokenStream.peek() == Tokens.IDENT){
|
|
type = this._media_type();
|
|
if (token === null){
|
|
token = tokenStream.token();
|
|
}
|
|
} else if (tokenStream.peek() == Tokens.LPAREN){
|
|
if (token === null){
|
|
token = tokenStream.LT(1);
|
|
}
|
|
expressions.push(this._media_expression());
|
|
}
|
|
|
|
if (type === null && expressions.length === 0){
|
|
return null;
|
|
} 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());
|
|
}
|
|
}
|
|
|
|
return new MediaQuery(ident, type, expressions, token.startLine, token.startCol);
|
|
},
|
|
|
|
//CSS3 Media Queries
|
|
_media_type: function(){
|
|
/*
|
|
* media_type
|
|
* : IDENT
|
|
* ;
|
|
*/
|
|
return this._media_feature();
|
|
},
|
|
|
|
/**
|
|
* Note: in CSS3 Media Queries, this is called "expression".
|
|
* Renamed here to avoid conflict with CSS3 Selectors
|
|
* definition of "expression". Also note that "expr" in the
|
|
* grammar now maps to "expression" from CSS3 selectors.
|
|
* @method _media_expression
|
|
* @private
|
|
*/
|
|
_media_expression: function(){
|
|
/*
|
|
* expression
|
|
* : '(' S* media_feature S* [ ':' S* expr ]? ')' S*
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
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));
|
|
},
|
|
|
|
//CSS3 Media Queries
|
|
_media_feature: function(){
|
|
/*
|
|
* media_feature
|
|
* : IDENT
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream;
|
|
|
|
tokenStream.mustMatch(Tokens.IDENT);
|
|
|
|
return SyntaxUnit.fromToken(tokenStream.token());
|
|
},
|
|
|
|
//CSS3 Paged Media
|
|
_page: function(){
|
|
/*
|
|
* page:
|
|
* 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;
|
|
|
|
//The value 'auto' may not be used as a page name and MUST be treated as a syntax error.
|
|
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,
|
|
line: line,
|
|
col: col
|
|
});
|
|
|
|
this._readDeclarations(true, true);
|
|
|
|
this.fire({
|
|
type: "endpage",
|
|
id: identifier,
|
|
pseudo: pseudoPage,
|
|
line: line,
|
|
col: col
|
|
});
|
|
|
|
},
|
|
|
|
//CSS3 Paged Media
|
|
_margin: function(){
|
|
/*
|
|
* margin :
|
|
* margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
|
|
* ;
|
|
*/
|
|
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,
|
|
line: line,
|
|
col: col
|
|
});
|
|
|
|
this._readDeclarations(true);
|
|
|
|
this.fire({
|
|
type: "endpagemargin",
|
|
margin: marginSym,
|
|
line: line,
|
|
col: col
|
|
});
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
//CSS3 Paged Media
|
|
_margin_sym: function(){
|
|
|
|
/*
|
|
* margin_sym :
|
|
* TOPLEFTCORNER_SYM |
|
|
* TOPLEFT_SYM |
|
|
* TOPCENTER_SYM |
|
|
* TOPRIGHT_SYM |
|
|
* TOPRIGHTCORNER_SYM |
|
|
* BOTTOMLEFTCORNER_SYM |
|
|
* BOTTOMLEFT_SYM |
|
|
* BOTTOMCENTER_SYM |
|
|
* BOTTOMRIGHT_SYM |
|
|
* BOTTOMRIGHTCORNER_SYM |
|
|
* LEFTTOP_SYM |
|
|
* LEFTMIDDLE_SYM |
|
|
* LEFTBOTTOM_SYM |
|
|
* RIGHTTOP_SYM |
|
|
* RIGHTMIDDLE_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.BOTTOMCENTER_SYM, Tokens.BOTTOMRIGHT_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());
|
|
} 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*
|
|
* '{' S* declaration [ ';' S* declaration ]* '}' S*
|
|
* ;
|
|
*/
|
|
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",
|
|
line: line,
|
|
col: col
|
|
});
|
|
|
|
this._readDeclarations(true);
|
|
|
|
this.fire({
|
|
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])){
|
|
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,
|
|
tokenValue,
|
|
token,
|
|
line,
|
|
col;
|
|
|
|
//check for star hack - throws error if not allowed
|
|
if (tokenStream.peek() == Tokens.STAR && this.options.starHack){
|
|
tokenStream.get();
|
|
token = tokenStream.token();
|
|
hack = token.value;
|
|
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;
|
|
|
|
|
|
/*
|
|
* Error Recovery: If even a single selector fails to parse,
|
|
* then the entire ruleset should be thrown away.
|
|
*/
|
|
try {
|
|
selectors = this._selectors_group();
|
|
} catch (ex){
|
|
if (ex instanceof SyntaxError && !this.options.strict){
|
|
|
|
//fire error event
|
|
this.fire({
|
|
type: "error",
|
|
error: ex,
|
|
message: ex.message,
|
|
line: ex.line,
|
|
col: ex.col
|
|
});
|
|
|
|
//skip over everything until closing brace
|
|
tt = tokenStream.advance([Tokens.RBRACE]);
|
|
if (tt == Tokens.RBRACE){
|
|
//if there's a right brace, the rule is finished so don't do anything
|
|
} 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){
|
|
|
|
this.fire({
|
|
type: "startrule",
|
|
selectors: selectors,
|
|
line: selectors[0].line,
|
|
col: selectors[0].col
|
|
});
|
|
|
|
this._readDeclarations(true);
|
|
|
|
this.fire({
|
|
type: "endrule",
|
|
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();
|
|
selector = this._selector();
|
|
if (selector !== null){
|
|
selectors.push(selector);
|
|
} else {
|
|
this._unexpectedToken(tokenStream.LT(1));
|
|
}
|
|
}
|
|
}
|
|
|
|
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()){
|
|
|
|
//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 (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(){
|
|
/*
|
|
* simple_selector_sequence
|
|
* : [ type_selector | universal ]
|
|
* [ HASH | class | attrib | pseudo | negation ]*
|
|
* | [ HASH | class | attrib | pseudo | negation ]+
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
|
|
//parts of a simple selector
|
|
elementName = null,
|
|
modifiers = [],
|
|
|
|
//complete selector text
|
|
selectorText= "",
|
|
|
|
//the different parts after the element name to search for
|
|
components = [
|
|
//HASH
|
|
function(){
|
|
return tokenStream.match(Tokens.HASH) ?
|
|
new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) :
|
|
null;
|
|
},
|
|
this._class,
|
|
this._attrib,
|
|
this._pseudo,
|
|
this._negation
|
|
],
|
|
i = 0,
|
|
len = components.length,
|
|
component = null,
|
|
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;
|
|
} else {
|
|
break;
|
|
}
|
|
} else {
|
|
i = 0;
|
|
modifiers.push(component);
|
|
selectorText += component.toString();
|
|
component = null;
|
|
}
|
|
}
|
|
|
|
|
|
return selectorText !== "" ?
|
|
new SelectorPart(elementName, modifiers, selectorText, line, col) :
|
|
null;
|
|
},
|
|
|
|
//CSS3 Selectors
|
|
_type_selector: function(){
|
|
/*
|
|
* type_selector
|
|
* : [ namespace_prefix ]? element_name
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
ns = this._namespace_prefix(),
|
|
elementName = this._element_name();
|
|
|
|
if (!elementName){
|
|
/*
|
|
* Need to back out the namespace that was read due to both
|
|
* type_selector and universal reading namespace_prefix
|
|
* first. Kind of hacky, but only way I can figure out
|
|
* right now how to not change the grammar.
|
|
*/
|
|
if (ns){
|
|
tokenStream.unget();
|
|
if (ns.length > 1){
|
|
tokenStream.unget();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} else {
|
|
if (ns){
|
|
elementName.text = ns + elementName.text;
|
|
elementName.col -= ns.length;
|
|
}
|
|
return elementName;
|
|
}
|
|
},
|
|
|
|
//CSS3 Selectors
|
|
_class: function(){
|
|
/*
|
|
* class
|
|
* : '.' IDENT
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
token;
|
|
|
|
if (tokenStream.match(Tokens.DOT)){
|
|
tokenStream.mustMatch(Tokens.IDENT);
|
|
token = tokenStream.token();
|
|
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);
|
|
|
|
} 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;
|
|
},
|
|
|
|
//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(){
|
|
/*
|
|
* attrib
|
|
* : '[' S* [ namespace_prefix ]? IDENT S*
|
|
* [ [ PREFIXMATCH |
|
|
* SUFFIXMATCH |
|
|
* SUBSTRINGMATCH |
|
|
* '=' |
|
|
* 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 += this._readWhitespace();
|
|
|
|
if(tokenStream.match([Tokens.PREFIXMATCH, Tokens.SUFFIXMATCH, Tokens.SUBSTRINGMATCH,
|
|
Tokens.EQUALS, Tokens.INCLUDES, Tokens.DASHMATCH])){
|
|
|
|
value += tokenStream.token().value;
|
|
value += this._readWhitespace();
|
|
|
|
tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]);
|
|
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;
|
|
col = tokenStream.token().startCol - colons.length;
|
|
} else if (tokenStream.peek() == Tokens.FUNCTION){
|
|
line = tokenStream.LT(1).startLine;
|
|
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();
|
|
value += this._expression();
|
|
tokenStream.mustMatch(Tokens.RPAREN);
|
|
value += ")";
|
|
}
|
|
|
|
return value;
|
|
},
|
|
|
|
//CSS3 Selectors
|
|
_expression: function(){
|
|
/*
|
|
* expression
|
|
* : [ [ 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.ANGLE, Tokens.TIME,
|
|
Tokens.RESOLUTION])){
|
|
|
|
value += tokenStream.token().value;
|
|
value += this._readWhitespace();
|
|
}
|
|
|
|
return value.length ? value : null;
|
|
|
|
},
|
|
|
|
//CSS3 Selectors
|
|
_negation: function(){
|
|
/*
|
|
* negation
|
|
* : NOT S* negation_arg S* ')'
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
line,
|
|
col,
|
|
value = "",
|
|
arg,
|
|
subpart = null;
|
|
|
|
if (tokenStream.match(Tokens.NOT)){
|
|
value = tokenStream.token().value;
|
|
line = tokenStream.token().startLine;
|
|
col = tokenStream.token().startCol;
|
|
value += this._readWhitespace();
|
|
arg = this._negation_arg();
|
|
value += arg;
|
|
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
|
|
* : type_selector | universal | HASH | class | attrib | pseudo
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
args = [
|
|
this._type_selector,
|
|
this._universal,
|
|
function(){
|
|
return tokenStream.match(Tokens.HASH) ?
|
|
new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) :
|
|
null;
|
|
},
|
|
this._class,
|
|
this._attrib,
|
|
this._pseudo
|
|
],
|
|
arg = null,
|
|
i = 0,
|
|
len = args.length,
|
|
elementName,
|
|
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;
|
|
},
|
|
|
|
_declaration: function(){
|
|
|
|
/*
|
|
* declaration
|
|
* : property ':' S* expr prio?
|
|
* | /( empty )/
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
property = null,
|
|
expr = null,
|
|
prio = null,
|
|
error = null,
|
|
invalid = null,
|
|
propertyName= "";
|
|
|
|
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();
|
|
|
|
/*
|
|
* If hacks should be allowed, then only check the root
|
|
* property. If hacks should not be allowed, treat
|
|
* _property or *property as invalid properties.
|
|
*/
|
|
propertyName = property.toString();
|
|
if (this.options.starHack && property.hack == "*" ||
|
|
this.options.underscoreHack && property.hack == "_") {
|
|
|
|
propertyName = property.text;
|
|
}
|
|
|
|
try {
|
|
this._validateProperty(propertyName, expr);
|
|
} catch (ex) {
|
|
invalid = ex;
|
|
}
|
|
|
|
this.fire({
|
|
type: "property",
|
|
property: property,
|
|
value: expr,
|
|
important: prio,
|
|
line: property.line,
|
|
col: property.col,
|
|
invalid: invalid
|
|
});
|
|
|
|
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);
|
|
} /*else {
|
|
//if there's not an operator, you have a full value
|
|
values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col));
|
|
valueParts = [];
|
|
}*/
|
|
|
|
value = this._term();
|
|
|
|
if (value === null){
|
|
break;
|
|
} else {
|
|
values.push(value);
|
|
}
|
|
} 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].line, values[0].col) : null;
|
|
},
|
|
|
|
_term: function(){
|
|
|
|
/*
|
|
* term
|
|
* : unary_operator?
|
|
* [ NUMBER S* | PERCENTAGE S* | LENGTH S* | ANGLE S* |
|
|
* TIME S* | FREQ S* | function | ie_function ]
|
|
* | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
unary = null,
|
|
value = null,
|
|
token,
|
|
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.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;
|
|
col = tokenStream.token().startCol;
|
|
}
|
|
this._readWhitespace();
|
|
} else {
|
|
|
|
//see if it's a color
|
|
token = this._hexcolor();
|
|
if (token === 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.
|
|
*/
|
|
if (tokenStream.LA(3) == Tokens.EQUALS && this.options.ieFilters){
|
|
value = this._ie_function();
|
|
} else {
|
|
value = this._function();
|
|
}
|
|
}
|
|
|
|
/*if (value === null){
|
|
return null;
|
|
//throw new Error("Expected identifier at line " + tokenStream.token().startLine + ", character " + tokenStream.token().startCol + ".");
|
|
}*/
|
|
|
|
} else {
|
|
value = token.value;
|
|
if (unary === null){
|
|
line = token.startLine;
|
|
col = 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,
|
|
lt;
|
|
|
|
if (tokenStream.match(Tokens.FUNCTION)){
|
|
functionText = tokenStream.token().value;
|
|
this._readWhitespace();
|
|
expr = this._expr();
|
|
functionText += expr;
|
|
|
|
//START: Horrible hack in case it's an IE filter
|
|
if (this.options.ieFilters && tokenStream.peek() == Tokens.EQUALS){
|
|
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){
|
|
tokenStream.get();
|
|
functionText += tokenStream.token().value;
|
|
lt = tokenStream.peek();
|
|
}
|
|
} while(tokenStream.match([Tokens.COMMA, Tokens.S]));
|
|
}
|
|
|
|
//END: Horrible Hack
|
|
|
|
tokenStream.match(Tokens.RPAREN);
|
|
functionText += ")";
|
|
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){
|
|
tokenStream.get();
|
|
functionText += tokenStream.token().value;
|
|
lt = tokenStream.peek();
|
|
}
|
|
} 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
|
|
* have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
|
|
* after the "#"; e.g., "#000" is OK, but "#abcd" is not.
|
|
*
|
|
* hexcolor
|
|
* : HASH S*
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
token = null,
|
|
color;
|
|
|
|
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)){
|
|
throw new SyntaxError("Expected a hex color but found '" + color + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol);
|
|
}
|
|
this._readWhitespace();
|
|
}
|
|
|
|
return token;
|
|
},
|
|
|
|
//-----------------------------------------------------------------
|
|
// Animations methods
|
|
//-----------------------------------------------------------------
|
|
|
|
_keyframes: function(){
|
|
|
|
/*
|
|
* keyframes:
|
|
* : KEYFRAMES_SYM S* keyframe_name S* '{' S* keyframe_rule* '}' {
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
token,
|
|
tt,
|
|
name;
|
|
|
|
tokenStream.mustMatch(Tokens.KEYFRAMES_SYM);
|
|
this._readWhitespace();
|
|
name = this._keyframe_name();
|
|
|
|
this._readWhitespace();
|
|
tokenStream.mustMatch(Tokens.LBRACE);
|
|
|
|
this.fire({
|
|
type: "startkeyframes",
|
|
name: name,
|
|
line: name.line,
|
|
col: name.col
|
|
});
|
|
|
|
this._readWhitespace();
|
|
tt = tokenStream.peek();
|
|
|
|
//check for key
|
|
while(tt == Tokens.IDENT || tt == Tokens.PERCENTAGE) {
|
|
this._keyframe_rule();
|
|
this._readWhitespace();
|
|
tt = tokenStream.peek();
|
|
}
|
|
|
|
this.fire({
|
|
type: "endkeyframes",
|
|
name: name,
|
|
line: name.line,
|
|
col: name.col
|
|
});
|
|
|
|
this._readWhitespace();
|
|
tokenStream.mustMatch(Tokens.RBRACE);
|
|
|
|
},
|
|
|
|
_keyframe_name: function(){
|
|
|
|
/*
|
|
* keyframe_name:
|
|
* : IDENT
|
|
* | STRING
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
token;
|
|
|
|
tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]);
|
|
return SyntaxUnit.fromToken(tokenStream.token());
|
|
},
|
|
|
|
_keyframe_rule: function(){
|
|
|
|
/*
|
|
* keyframe_rule:
|
|
* : key_list S*
|
|
* '{' S* declaration [ ';' S* declaration ]* '}' S*
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
token,
|
|
keyList = this._key_list();
|
|
|
|
this.fire({
|
|
type: "startkeyframerule",
|
|
keys: keyList,
|
|
line: keyList[0].line,
|
|
col: keyList[0].col
|
|
});
|
|
|
|
this._readDeclarations(true);
|
|
|
|
this.fire({
|
|
type: "endkeyframerule",
|
|
keys: keyList,
|
|
line: keyList[0].line,
|
|
col: keyList[0].col
|
|
});
|
|
|
|
},
|
|
|
|
_key_list: function(){
|
|
|
|
/*
|
|
* key_list:
|
|
* : key [ S* ',' S* key]*
|
|
* ;
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
token,
|
|
key,
|
|
keyList = [];
|
|
|
|
//must be least one key
|
|
keyList.push(this._key());
|
|
|
|
this._readWhitespace();
|
|
|
|
while(tokenStream.match(Tokens.COMMA)){
|
|
this._readWhitespace();
|
|
keyList.push(this._key());
|
|
this._readWhitespace();
|
|
}
|
|
|
|
return keyList;
|
|
},
|
|
|
|
_key: function(){
|
|
/*
|
|
* There is a restriction that IDENT can be only "from" or "to".
|
|
*
|
|
* key
|
|
* : PERCENTAGE
|
|
* | IDENT
|
|
* ;
|
|
*/
|
|
|
|
var tokenStream = this._tokenStream,
|
|
token;
|
|
|
|
if (tokenStream.match(Tokens.PERCENTAGE)){
|
|
return SyntaxUnit.fromToken(tokenStream.token());
|
|
} else if (tokenStream.match(Tokens.IDENT)){
|
|
token = tokenStream.token();
|
|
|
|
if (/from|to/i.test(token.value)){
|
|
return SyntaxUnit.fromToken(token);
|
|
}
|
|
|
|
tokenStream.unget();
|
|
}
|
|
|
|
//if it gets here, there wasn't a valid token, so time to explode
|
|
this._unexpectedToken(tokenStream.LT(1));
|
|
},
|
|
|
|
//-----------------------------------------------------------------
|
|
// Helper methods
|
|
//-----------------------------------------------------------------
|
|
|
|
/**
|
|
* Not part of CSS grammar, but useful for skipping over
|
|
* combination of white space and HTML-style comments.
|
|
* @return {void}
|
|
* @method _skipCruft
|
|
* @private
|
|
*/
|
|
_skipCruft: function(){
|
|
while(this._tokenStream.match([Tokens.S, Tokens.CDO, Tokens.CDC])){
|
|
//noop
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Not part of CSS grammar, but this pattern occurs frequently
|
|
* in the official CSS grammar. Split out here to eliminate
|
|
* duplicate code.
|
|
* @param {Boolean} checkStart Indicates if the rule should check
|
|
* for the left brace at the beginning.
|
|
* @param {Boolean} readMargins Indicates if the rule should check
|
|
* for margin patterns.
|
|
* @return {void}
|
|
* @method _readDeclarations
|
|
* @private
|
|
*/
|
|
_readDeclarations: function(checkStart, readMargins){
|
|
/*
|
|
* Reads the pattern
|
|
* S* '{' S* declaration [ ';' S* declaration ]* '}' S*
|
|
* or
|
|
* 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.
|
|
*/
|
|
var tokenStream = this._tokenStream,
|
|
tt;
|
|
|
|
|
|
this._readWhitespace();
|
|
|
|
if (checkStart){
|
|
tokenStream.mustMatch(Tokens.LBRACE);
|
|
}
|
|
|
|
this._readWhitespace();
|
|
|
|
try {
|
|
|
|
while(true){
|
|
|
|
if (tokenStream.match(Tokens.SEMICOLON) || (readMargins && this._margin())){
|
|
//noop
|
|
} else if (this._declaration()){
|
|
if (!tokenStream.match(Tokens.SEMICOLON)){
|
|
break;
|
|
}
|
|
} 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",
|
|
error: ex,
|
|
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){
|
|
//if there's a semicolon, then there might be another declaration
|
|
this._readDeclarations(false, readMargins);
|
|
} else if (tt != Tokens.RBRACE){
|
|
//if there's a right brace, the rule is finished so don't do anything
|
|
//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
|
|
* white space, this function is used to match as much white space
|
|
* as necessary.
|
|
* @method _readWhitespace
|
|
* @return {String} The white space if found, empty string if not.
|
|
* @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.
|
|
* @param {Object} token The token that was found.
|
|
* @method _unexpectedToken
|
|
* @return {void}
|
|
* @private
|
|
*/
|
|
_unexpectedToken: function(token){
|
|
throw new SyntaxError("Unexpected token '" + token.value + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol);
|
|
},
|
|
|
|
/**
|
|
* Helper method used for parsing subparts of a style sheet.
|
|
* @return {void}
|
|
* @method _verifyEnd
|
|
* @private
|
|
*/
|
|
_verifyEnd: function(){
|
|
if (this._tokenStream.LA(1) != Tokens.EOF){
|
|
this._unexpectedToken(this._tokenStream.LT(1));
|
|
}
|
|
},
|
|
|
|
//-----------------------------------------------------------------
|
|
// Validation methods
|
|
//-----------------------------------------------------------------
|
|
_validateProperty: function(property, value){
|
|
Validation.validate(property, value);
|
|
},
|
|
|
|
//-----------------------------------------------------------------
|
|
// Parsing methods
|
|
//-----------------------------------------------------------------
|
|
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
* @param {String} input The text to parser.
|
|
* @return {Boolean} True if the parse completed successfully, false if not.
|
|
* @method parseRule
|
|
*/
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Parses a single CSS selector (no comma)
|
|
* @param {String} input The text to parse as a CSS selector.
|
|
* @return {Selector} An object representing the selector.
|
|
* @throws parserlib.util.SyntaxError If an unexpected token is found.
|
|
* @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;
|
|
},
|
|
|
|
/**
|
|
* Parses an HTML style attribute: a set of CSS declarations
|
|
* separated by semicolons.
|
|
* @param {String} input The text to parse as a style attribute
|
|
* @return {void}
|
|
* @method parseStyleAttribute
|
|
*/
|
|
parseStyleAttribute: function(input){
|
|
input += "}"; // for error recovery in _readDeclarations()
|
|
this._tokenStream = new TokenStream(input, Tokens);
|
|
this._readDeclarations();
|
|
}
|
|
};
|
|
|
|
//copy over onto prototype
|
|
for (prop in additions){
|
|
if (additions.hasOwnProperty(prop)){
|
|
proto[prop] = additions[prop];
|
|
}
|
|
}
|
|
|
|
return proto;
|
|
}();
|
|
|
|
|
|
/*
|
|
nth
|
|
: S* [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]? |
|
|
['-'|'+']? INTEGER | {O}{D}{D} | {E}{V}{E}{N} ] S*
|
|
;
|
|
*/
|
|
/*global Validation, ValidationTypes, ValidationError*/
|
|
var Properties = {
|
|
|
|
//A
|
|
"alignment-adjust" : "auto | baseline | before-edge | text-before-edge | middle | central | after-edge | text-after-edge | ideographic | alphabetic | hanging | mathematical | <percentage> | <length>",
|
|
"alignment-baseline" : "baseline | use-script | before-edge | text-before-edge | after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | mathematical",
|
|
"animation" : 1,
|
|
"animation-delay" : { multi: "<time>", comma: true },
|
|
"animation-direction" : { multi: "normal | alternate", comma: true },
|
|
"animation-duration" : { multi: "<time>", comma: true },
|
|
"animation-iteration-count" : { multi: "<number> | infinite", comma: true },
|
|
"animation-name" : { multi: "none | <ident>", comma: true },
|
|
"animation-play-state" : { multi: "running | paused", comma: true },
|
|
"animation-timing-function" : 1,
|
|
|
|
//vendor prefixed
|
|
"-moz-animation-delay" : { multi: "<time>", comma: true },
|
|
"-moz-animation-direction" : { multi: "normal | alternate", comma: true },
|
|
"-moz-animation-duration" : { multi: "<time>", comma: true },
|
|
"-moz-animation-iteration-count" : { multi: "<number> | infinite", comma: true },
|
|
"-moz-animation-name" : { multi: "none | <ident>", comma: true },
|
|
"-moz-animation-play-state" : { multi: "running | paused", comma: true },
|
|
|
|
"-ms-animation-delay" : { multi: "<time>", comma: true },
|
|
"-ms-animation-direction" : { multi: "normal | alternate", comma: true },
|
|
"-ms-animation-duration" : { multi: "<time>", comma: true },
|
|
"-ms-animation-iteration-count" : { multi: "<number> | infinite", comma: true },
|
|
"-ms-animation-name" : { multi: "none | <ident>", comma: true },
|
|
"-ms-animation-play-state" : { multi: "running | paused", comma: true },
|
|
|
|
"-webkit-animation-delay" : { multi: "<time>", comma: true },
|
|
"-webkit-animation-direction" : { multi: "normal | alternate", comma: true },
|
|
"-webkit-animation-duration" : { multi: "<time>", comma: true },
|
|
"-webkit-animation-iteration-count" : { multi: "<number> | infinite", comma: true },
|
|
"-webkit-animation-name" : { multi: "none | <ident>", comma: true },
|
|
"-webkit-animation-play-state" : { multi: "running | paused", comma: true },
|
|
|
|
"-o-animation-delay" : { multi: "<time>", comma: true },
|
|
"-o-animation-direction" : { multi: "normal | alternate", comma: true },
|
|
"-o-animation-duration" : { multi: "<time>", comma: true },
|
|
"-o-animation-iteration-count" : { multi: "<number> | infinite", comma: true },
|
|
"-o-animation-name" : { multi: "none | <ident>", comma: true },
|
|
"-o-animation-play-state" : { multi: "running | paused", comma: true },
|
|
|
|
"appearance" : "icon | window | desktop | workspace | document | tooltip | dialog | button | push-button | hyperlink | radio-button | checkbox | menu-item | tab | menu | menubar | pull-down-menu | pop-up-menu | list-menu | radio-group | checkbox-group | outline-tree | range | field | combo-box | signature | password | normal | inherit",
|
|
"azimuth" : function (expression) {
|
|
var simple = "<angle> | leftwards | rightwards | inherit",
|
|
direction = "left-side | far-left | left | center-left | center | center-right | right | far-right | right-side",
|
|
behind = false,
|
|
valid = false,
|
|
part;
|
|
|
|
if (!ValidationTypes.isAny(expression, simple)) {
|
|
if (ValidationTypes.isAny(expression, "behind")) {
|
|
behind = true;
|
|
valid = true;
|
|
}
|
|
|
|
if (ValidationTypes.isAny(expression, direction)) {
|
|
valid = true;
|
|
if (!behind) {
|
|
ValidationTypes.isAny(expression, "behind");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (expression.hasNext()) {
|
|
part = expression.next();
|
|
if (valid) {
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
} else {
|
|
throw new ValidationError("Expected (<'azimuth'>) but found '" + part + "'.", part.line, part.col);
|
|
}
|
|
}
|
|
},
|
|
|
|
//B
|
|
"backface-visibility" : "visible | hidden",
|
|
"background" : 1,
|
|
"background-attachment" : { multi: "<attachment>", comma: true },
|
|
"background-clip" : { multi: "<box>", comma: true },
|
|
"background-color" : "<color> | inherit",
|
|
"background-image" : { multi: "<bg-image>", comma: true },
|
|
"background-origin" : { multi: "<box>", comma: true },
|
|
"background-position" : { multi: "<bg-position>", comma: true },
|
|
"background-repeat" : { multi: "<repeat-style>" },
|
|
"background-size" : { multi: "<bg-size>", comma: true },
|
|
"baseline-shift" : "baseline | sub | super | <percentage> | <length>",
|
|
"behavior" : 1,
|
|
"binding" : 1,
|
|
"bleed" : "<length>",
|
|
"bookmark-label" : "<content> | <attr> | <string>",
|
|
"bookmark-level" : "none | <integer>",
|
|
"bookmark-state" : "open | closed",
|
|
"bookmark-target" : "none | <uri> | <attr>",
|
|
"border" : "<border-width> || <border-style> || <color>",
|
|
"border-bottom" : "<border-width> || <border-style> || <color>",
|
|
"border-bottom-color" : "<color>",
|
|
"border-bottom-left-radius" : "<x-one-radius>",
|
|
"border-bottom-right-radius" : "<x-one-radius>",
|
|
"border-bottom-style" : "<border-style>",
|
|
"border-bottom-width" : "<border-width>",
|
|
"border-collapse" : "collapse | separate | inherit",
|
|
"border-color" : { multi: "<color> | inherit", max: 4 },
|
|
"border-image" : 1,
|
|
"border-image-outset" : { multi: "<length> | <number>", max: 4 },
|
|
"border-image-repeat" : { multi: "stretch | repeat | round", max: 2 },
|
|
"border-image-slice" : function(expression) {
|
|
|
|
var valid = false,
|
|
numeric = "<number> | <percentage>",
|
|
fill = false,
|
|
count = 0,
|
|
max = 4,
|
|
part;
|
|
|
|
if (ValidationTypes.isAny(expression, "fill")) {
|
|
fill = true;
|
|
valid = true;
|
|
}
|
|
|
|
while (expression.hasNext() && count < max) {
|
|
valid = ValidationTypes.isAny(expression, numeric);
|
|
if (!valid) {
|
|
break;
|
|
}
|
|
count++;
|
|
}
|
|
|
|
|
|
if (!fill) {
|
|
ValidationTypes.isAny(expression, "fill");
|
|
} else {
|
|
valid = true;
|
|
}
|
|
|
|
if (expression.hasNext()) {
|
|
part = expression.next();
|
|
if (valid) {
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
} else {
|
|
throw new ValidationError("Expected ([<number> | <percentage>]{1,4} && fill?) but found '" + part + "'.", part.line, part.col);
|
|
}
|
|
}
|
|
},
|
|
"border-image-source" : "<image> | none",
|
|
"border-image-width" : { multi: "<length> | <percentage> | <number> | auto", max: 4 },
|
|
"border-left" : "<border-width> || <border-style> || <color>",
|
|
"border-left-color" : "<color> | inherit",
|
|
"border-left-style" : "<border-style>",
|
|
"border-left-width" : "<border-width>",
|
|
"border-radius" : function(expression) {
|
|
|
|
var valid = false,
|
|
numeric = "<length> | <percentage>",
|
|
slash = false,
|
|
fill = false,
|
|
count = 0,
|
|
max = 8,
|
|
part;
|
|
|
|
while (expression.hasNext() && count < max) {
|
|
valid = ValidationTypes.isAny(expression, numeric);
|
|
if (!valid) {
|
|
|
|
if (expression.peek() == "/" && count > 1 && !slash) {
|
|
slash = true;
|
|
max = count + 5;
|
|
expression.next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
count++;
|
|
}
|
|
|
|
if (expression.hasNext()) {
|
|
part = expression.next();
|
|
if (valid) {
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
} else {
|
|
throw new ValidationError("Expected (<'border-radius'>) but found '" + part + "'.", part.line, part.col);
|
|
}
|
|
}
|
|
},
|
|
"border-right" : "<border-width> || <border-style> || <color>",
|
|
"border-right-color" : "<color> | inherit",
|
|
"border-right-style" : "<border-style>",
|
|
"border-right-width" : "<border-width>",
|
|
"border-spacing" : { multi: "<length> | inherit", max: 2 },
|
|
"border-style" : { multi: "<border-style>", max: 4 },
|
|
"border-top" : "<border-width> || <border-style> || <color>",
|
|
"border-top-color" : "<color> | inherit",
|
|
"border-top-left-radius" : "<x-one-radius>",
|
|
"border-top-right-radius" : "<x-one-radius>",
|
|
"border-top-style" : "<border-style>",
|
|
"border-top-width" : "<border-width>",
|
|
"border-width" : { multi: "<border-width>", max: 4 },
|
|
"bottom" : "<margin-width> | inherit",
|
|
"box-align" : "start | end | center | baseline | stretch", //http://www.w3.org/TR/2009/WD-css3-flexbox-20090723/
|
|
"box-decoration-break" : "slice |clone",
|
|
"box-direction" : "normal | reverse | inherit",
|
|
"box-flex" : "<number>",
|
|
"box-flex-group" : "<integer>",
|
|
"box-lines" : "single | multiple",
|
|
"box-ordinal-group" : "<integer>",
|
|
"box-orient" : "horizontal | vertical | inline-axis | block-axis | inherit",
|
|
"box-pack" : "start | end | center | justify",
|
|
"box-shadow" : function (expression) {
|
|
var result = false,
|
|
part;
|
|
|
|
if (!ValidationTypes.isAny(expression, "none")) {
|
|
Validation.multiProperty("<shadow>", expression, true, Infinity);
|
|
} else {
|
|
if (expression.hasNext()) {
|
|
part = expression.next();
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
}
|
|
}
|
|
},
|
|
"box-sizing" : "content-box | border-box | inherit",
|
|
"break-after" : "auto | always | avoid | left | right | page | column | avoid-page | avoid-column",
|
|
"break-before" : "auto | always | avoid | left | right | page | column | avoid-page | avoid-column",
|
|
"break-inside" : "auto | avoid | avoid-page | avoid-column",
|
|
|
|
//C
|
|
"caption-side" : "top | bottom | inherit",
|
|
"clear" : "none | right | left | both | inherit",
|
|
"clip" : 1,
|
|
"color" : "<color> | inherit",
|
|
"color-profile" : 1,
|
|
"column-count" : "<integer> | auto", //http://www.w3.org/TR/css3-multicol/
|
|
"column-fill" : "auto | balance",
|
|
"column-gap" : "<length> | normal",
|
|
"column-rule" : "<border-width> || <border-style> || <color>",
|
|
"column-rule-color" : "<color>",
|
|
"column-rule-style" : "<border-style>",
|
|
"column-rule-width" : "<border-width>",
|
|
"column-span" : "none | all",
|
|
"column-width" : "<length> | auto",
|
|
"columns" : 1,
|
|
"content" : 1,
|
|
"counter-increment" : 1,
|
|
"counter-reset" : 1,
|
|
"crop" : "<shape> | auto",
|
|
"cue" : "cue-after | cue-before | inherit",
|
|
"cue-after" : 1,
|
|
"cue-before" : 1,
|
|
"cursor" : 1,
|
|
|
|
//D
|
|
"direction" : "ltr | rtl | inherit",
|
|
"display" : "inline | block | list-item | inline-block | table | inline-table | table-row-group | table-header-group | table-footer-group | table-row | table-column-group | table-column | table-cell | table-caption | box | inline-box | grid | inline-grid | none | inherit",
|
|
"dominant-baseline" : 1,
|
|
"drop-initial-after-adjust" : "central | middle | after-edge | text-after-edge | ideographic | alphabetic | mathematical | <percentage> | <length>",
|
|
"drop-initial-after-align" : "baseline | use-script | before-edge | text-before-edge | after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | mathematical",
|
|
"drop-initial-before-adjust" : "before-edge | text-before-edge | central | middle | hanging | mathematical | <percentage> | <length>",
|
|
"drop-initial-before-align" : "caps-height | baseline | use-script | before-edge | text-before-edge | after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | mathematical",
|
|
"drop-initial-size" : "auto | line | <length> | <percentage>",
|
|
"drop-initial-value" : "initial | <integer>",
|
|
|
|
//E
|
|
"elevation" : "<angle> | below | level | above | higher | lower | inherit",
|
|
"empty-cells" : "show | hide | inherit",
|
|
|
|
//F
|
|
"filter" : 1,
|
|
"fit" : "fill | hidden | meet | slice",
|
|
"fit-position" : 1,
|
|
"float" : "left | right | none | inherit",
|
|
"float-offset" : 1,
|
|
"font" : 1,
|
|
"font-family" : 1,
|
|
"font-size" : "<absolute-size> | <relative-size> | <length> | <percentage> | inherit",
|
|
"font-size-adjust" : "<number> | none | inherit",
|
|
"font-stretch" : "normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded | inherit",
|
|
"font-style" : "normal | italic | oblique | inherit",
|
|
"font-variant" : "normal | small-caps | inherit",
|
|
"font-weight" : "normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | inherit",
|
|
|
|
//G
|
|
"grid-cell-stacking" : "columns | rows | layer",
|
|
"grid-column" : 1,
|
|
"grid-columns" : 1,
|
|
"grid-column-align" : "start | end | center | stretch",
|
|
"grid-column-sizing" : 1,
|
|
"grid-column-span" : "<integer>",
|
|
"grid-flow" : "none | rows | columns",
|
|
"grid-layer" : "<integer>",
|
|
"grid-row" : 1,
|
|
"grid-rows" : 1,
|
|
"grid-row-align" : "start | end | center | stretch",
|
|
"grid-row-span" : "<integer>",
|
|
"grid-row-sizing" : 1,
|
|
|
|
//H
|
|
"hanging-punctuation" : 1,
|
|
"height" : "<margin-width> | inherit",
|
|
"hyphenate-after" : "<integer> | auto",
|
|
"hyphenate-before" : "<integer> | auto",
|
|
"hyphenate-character" : "<string> | auto",
|
|
"hyphenate-lines" : "no-limit | <integer>",
|
|
"hyphenate-resource" : 1,
|
|
"hyphens" : "none | manual | auto",
|
|
|
|
//I
|
|
"icon" : 1,
|
|
"image-orientation" : "angle | auto",
|
|
"image-rendering" : 1,
|
|
"image-resolution" : 1,
|
|
"inline-box-align" : "initial | last | <integer>",
|
|
|
|
//L
|
|
"left" : "<margin-width> | inherit",
|
|
"letter-spacing" : "<length> | normal | inherit",
|
|
"line-height" : "<number> | <length> | <percentage> | normal | inherit",
|
|
"line-break" : "auto | loose | normal | strict",
|
|
"line-stacking" : 1,
|
|
"line-stacking-ruby" : "exclude-ruby | include-ruby",
|
|
"line-stacking-shift" : "consider-shifts | disregard-shifts",
|
|
"line-stacking-strategy" : "inline-line-height | block-line-height | max-height | grid-height",
|
|
"list-style" : 1,
|
|
"list-style-image" : "<uri> | none | inherit",
|
|
"list-style-position" : "inside | outside | inherit",
|
|
"list-style-type" : "disc | circle | square | decimal | decimal-leading-zero | lower-roman | upper-roman | lower-greek | lower-latin | upper-latin | armenian | georgian | lower-alpha | upper-alpha | none | inherit",
|
|
|
|
//M
|
|
"margin" : { multi: "<margin-width> | inherit", max: 4 },
|
|
"margin-bottom" : "<margin-width> | inherit",
|
|
"margin-left" : "<margin-width> | inherit",
|
|
"margin-right" : "<margin-width> | inherit",
|
|
"margin-top" : "<margin-width> | inherit",
|
|
"mark" : 1,
|
|
"mark-after" : 1,
|
|
"mark-before" : 1,
|
|
"marks" : 1,
|
|
"marquee-direction" : 1,
|
|
"marquee-play-count" : 1,
|
|
"marquee-speed" : 1,
|
|
"marquee-style" : 1,
|
|
"max-height" : "<length> | <percentage> | none | inherit",
|
|
"max-width" : "<length> | <percentage> | none | inherit",
|
|
"min-height" : "<length> | <percentage> | inherit",
|
|
"min-width" : "<length> | <percentage> | inherit",
|
|
"move-to" : 1,
|
|
|
|
//N
|
|
"nav-down" : 1,
|
|
"nav-index" : 1,
|
|
"nav-left" : 1,
|
|
"nav-right" : 1,
|
|
"nav-up" : 1,
|
|
|
|
//O
|
|
"opacity" : "<number> | inherit",
|
|
"orphans" : "<integer> | inherit",
|
|
"outline" : 1,
|
|
"outline-color" : "<color> | invert | inherit",
|
|
"outline-offset" : 1,
|
|
"outline-style" : "<border-style> | inherit",
|
|
"outline-width" : "<border-width> | inherit",
|
|
"overflow" : "visible | hidden | scroll | auto | inherit",
|
|
"overflow-style" : 1,
|
|
"overflow-x" : 1,
|
|
"overflow-y" : 1,
|
|
|
|
//P
|
|
"padding" : { multi: "<padding-width> | inherit", max: 4 },
|
|
"padding-bottom" : "<padding-width> | inherit",
|
|
"padding-left" : "<padding-width> | inherit",
|
|
"padding-right" : "<padding-width> | inherit",
|
|
"padding-top" : "<padding-width> | inherit",
|
|
"page" : 1,
|
|
"page-break-after" : "auto | always | avoid | left | right | inherit",
|
|
"page-break-before" : "auto | always | avoid | left | right | inherit",
|
|
"page-break-inside" : "auto | avoid | inherit",
|
|
"page-policy" : 1,
|
|
"pause" : 1,
|
|
"pause-after" : 1,
|
|
"pause-before" : 1,
|
|
"perspective" : 1,
|
|
"perspective-origin" : 1,
|
|
"phonemes" : 1,
|
|
"pitch" : 1,
|
|
"pitch-range" : 1,
|
|
"play-during" : 1,
|
|
"position" : "static | relative | absolute | fixed | inherit",
|
|
"presentation-level" : 1,
|
|
"punctuation-trim" : 1,
|
|
|
|
//Q
|
|
"quotes" : 1,
|
|
|
|
//R
|
|
"rendering-intent" : 1,
|
|
"resize" : 1,
|
|
"rest" : 1,
|
|
"rest-after" : 1,
|
|
"rest-before" : 1,
|
|
"richness" : 1,
|
|
"right" : "<margin-width> | inherit",
|
|
"rotation" : 1,
|
|
"rotation-point" : 1,
|
|
"ruby-align" : 1,
|
|
"ruby-overhang" : 1,
|
|
"ruby-position" : 1,
|
|
"ruby-span" : 1,
|
|
|
|
//S
|
|
"size" : 1,
|
|
"speak" : "normal | none | spell-out | inherit",
|
|
"speak-header" : "once | always | inherit",
|
|
"speak-numeral" : "digits | continuous | inherit",
|
|
"speak-punctuation" : "code | none | inherit",
|
|
"speech-rate" : 1,
|
|
"src" : 1,
|
|
"stress" : 1,
|
|
"string-set" : 1,
|
|
|
|
"table-layout" : "auto | fixed | inherit",
|
|
"tab-size" : "<integer> | <length>",
|
|
"target" : 1,
|
|
"target-name" : 1,
|
|
"target-new" : 1,
|
|
"target-position" : 1,
|
|
"text-align" : "left | right | center | justify | inherit" ,
|
|
"text-align-last" : 1,
|
|
"text-decoration" : 1,
|
|
"text-emphasis" : 1,
|
|
"text-height" : 1,
|
|
"text-indent" : "<length> | <percentage> | inherit",
|
|
"text-justify" : "auto | none | inter-word | inter-ideograph | inter-cluster | distribute | kashida",
|
|
"text-outline" : 1,
|
|
"text-overflow" : 1,
|
|
"text-rendering" : "auto | optimizeSpeed | optimizeLegibility | geometricPrecision | inherit",
|
|
"text-shadow" : 1,
|
|
"text-transform" : "capitalize | uppercase | lowercase | none | inherit",
|
|
"text-wrap" : "normal | none | avoid",
|
|
"top" : "<margin-width> | inherit",
|
|
"transform" : 1,
|
|
"transform-origin" : 1,
|
|
"transform-style" : 1,
|
|
"transition" : 1,
|
|
"transition-delay" : 1,
|
|
"transition-duration" : 1,
|
|
"transition-property" : 1,
|
|
"transition-timing-function" : 1,
|
|
|
|
//U
|
|
"unicode-bidi" : "normal | embed | bidi-override | inherit",
|
|
"user-modify" : "read-only | read-write | write-only | inherit",
|
|
"user-select" : "none | text | toggle | element | elements | all | inherit",
|
|
|
|
//V
|
|
"vertical-align" : "<percentage> | <length> | baseline | sub | super | top | text-top | middle | bottom | text-bottom | inherit",
|
|
"visibility" : "visible | hidden | collapse | inherit",
|
|
"voice-balance" : 1,
|
|
"voice-duration" : 1,
|
|
"voice-family" : 1,
|
|
"voice-pitch" : 1,
|
|
"voice-pitch-range" : 1,
|
|
"voice-rate" : 1,
|
|
"voice-stress" : 1,
|
|
"voice-volume" : 1,
|
|
"volume" : 1,
|
|
|
|
//W
|
|
"white-space" : "normal | pre | nowrap | pre-wrap | pre-line | inherit",
|
|
"white-space-collapse" : 1,
|
|
"widows" : "<integer> | inherit",
|
|
"width" : "<length> | <percentage> | auto | inherit" ,
|
|
"word-break" : "normal | keep-all | break-all",
|
|
"word-spacing" : "<length> | normal | inherit",
|
|
"word-wrap" : 1,
|
|
|
|
//Z
|
|
"z-index" : "<integer> | auto | inherit",
|
|
"zoom" : "<number> | <percentage> | normal"
|
|
};
|
|
/*global SyntaxUnit, Parser*/
|
|
/**
|
|
* Represents a selector combinator (whitespace, +, >).
|
|
* @namespace parserlib.css
|
|
* @class PropertyName
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
* @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, text, line, col, Parser.PROPERTY_NAME_TYPE);
|
|
|
|
/**
|
|
* The type of IE hack applied ("*", "_", or null).
|
|
* @type String
|
|
* @property hack
|
|
*/
|
|
this.hack = hack;
|
|
|
|
}
|
|
|
|
PropertyName.prototype = new SyntaxUnit();
|
|
PropertyName.prototype.constructor = PropertyName;
|
|
PropertyName.prototype.toString = function(){
|
|
return (this.hack ? this.hack : "") + this.text;
|
|
};
|
|
|
|
/*global SyntaxUnit, Parser*/
|
|
/**
|
|
* Represents a single part of a CSS property value, meaning that it represents
|
|
* just everything single part between ":" and ";". If there are multiple values
|
|
* separated by commas, this type represents just one of the values.
|
|
* @param {String[]} parts An array of value parts making up this value.
|
|
* @param {int} line The line of text on which the unit resides.
|
|
* @param {int} col The column of text on which the unit resides.
|
|
* @namespace parserlib.css
|
|
* @class PropertyValue
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
*/
|
|
function PropertyValue(parts, line, col){
|
|
|
|
SyntaxUnit.call(this, parts.join(" "), line, col, Parser.PROPERTY_VALUE_TYPE);
|
|
|
|
/**
|
|
* The parts that make up the selector.
|
|
* @type Array
|
|
* @property parts
|
|
*/
|
|
this.parts = parts;
|
|
|
|
}
|
|
|
|
PropertyValue.prototype = new SyntaxUnit();
|
|
PropertyValue.prototype.constructor = PropertyValue;
|
|
|
|
|
|
/*global SyntaxUnit, Parser*/
|
|
/**
|
|
* A utility class that allows for easy iteration over the various parts of a
|
|
* property value.
|
|
* @param {parserlib.css.PropertyValue} value The property value to iterate over.
|
|
* @namespace parserlib.css
|
|
* @class PropertyValueIterator
|
|
* @constructor
|
|
*/
|
|
function PropertyValueIterator(value){
|
|
|
|
/**
|
|
* Iterator value
|
|
* @type int
|
|
* @property _i
|
|
* @private
|
|
*/
|
|
this._i = 0;
|
|
|
|
/**
|
|
* The parts that make up the value.
|
|
* @type Array
|
|
* @property _parts
|
|
* @private
|
|
*/
|
|
this._parts = value.parts;
|
|
|
|
/**
|
|
* Keeps track of bookmarks along the way.
|
|
* @type Array
|
|
* @property _marks
|
|
* @private
|
|
*/
|
|
this._marks = [];
|
|
|
|
/**
|
|
* Holds the original property value.
|
|
* @type parserlib.css.PropertyValue
|
|
* @property value
|
|
*/
|
|
this.value = value;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns the total number of parts in the value.
|
|
* @return {int} The total number of parts in the value.
|
|
* @method count
|
|
*/
|
|
PropertyValueIterator.prototype.count = function(){
|
|
return this._parts.length;
|
|
};
|
|
|
|
/**
|
|
* Indicates if the iterator is positioned at the first item.
|
|
* @return {Boolean} True if positioned at first item, false if not.
|
|
* @method isFirst
|
|
*/
|
|
PropertyValueIterator.prototype.isFirst = function(){
|
|
return this._i === 0;
|
|
};
|
|
|
|
/**
|
|
* Indicates if there are more parts of the property value.
|
|
* @return {Boolean} True if there are more parts, false if not.
|
|
* @method hasNext
|
|
*/
|
|
PropertyValueIterator.prototype.hasNext = function(){
|
|
return (this._i < this._parts.length);
|
|
};
|
|
|
|
/**
|
|
* Marks the current spot in the iteration so it can be restored to
|
|
* later on.
|
|
* @return {void}
|
|
* @method mark
|
|
*/
|
|
PropertyValueIterator.prototype.mark = function(){
|
|
this._marks.push(this._i);
|
|
};
|
|
|
|
/**
|
|
* Returns the next part of the property value or null if there is no next
|
|
* part. Does not move the internal counter forward.
|
|
* @return {parserlib.css.PropertyValuePart} The next part of the property value or null if there is no next
|
|
* part.
|
|
* @method peek
|
|
*/
|
|
PropertyValueIterator.prototype.peek = function(count){
|
|
return this.hasNext() ? this._parts[this._i + (count || 0)] : null;
|
|
};
|
|
|
|
/**
|
|
* Returns the next part of the property value or null if there is no next
|
|
* part.
|
|
* @return {parserlib.css.PropertyValuePart} The next part of the property value or null if there is no next
|
|
* part.
|
|
* @method next
|
|
*/
|
|
PropertyValueIterator.prototype.next = function(){
|
|
return this.hasNext() ? this._parts[this._i++] : null;
|
|
};
|
|
|
|
/**
|
|
* Returns the previous part of the property value or null if there is no
|
|
* previous part.
|
|
* @return {parserlib.css.PropertyValuePart} The previous part of the
|
|
* property value or null if there is no next part.
|
|
* @method previous
|
|
*/
|
|
PropertyValueIterator.prototype.previous = function(){
|
|
return this._i > 0 ? this._parts[--this._i] : null;
|
|
};
|
|
|
|
/**
|
|
* Restores the last saved bookmark.
|
|
* @return {void}
|
|
* @method restore
|
|
*/
|
|
PropertyValueIterator.prototype.restore = function(){
|
|
if (this._marks.length){
|
|
this._i = this._marks.pop();
|
|
}
|
|
};
|
|
|
|
|
|
/*global SyntaxUnit, Parser, Colors*/
|
|
/**
|
|
* Represents a single part of a CSS property value, meaning that it represents
|
|
* just one part of the data between ":" and ";".
|
|
* @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.
|
|
* @namespace parserlib.css
|
|
* @class PropertyValuePart
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
*/
|
|
function PropertyValuePart(text, line, col){
|
|
|
|
SyntaxUnit.call(this, text, line, col, Parser.PROPERTY_VALUE_PART_TYPE);
|
|
|
|
/**
|
|
* Indicates the type of value unit.
|
|
* @type String
|
|
* @property type
|
|
*/
|
|
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":
|
|
case "px":
|
|
case "cm":
|
|
case "mm":
|
|
case "in":
|
|
case "pt":
|
|
case "pc":
|
|
case "ch":
|
|
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;
|
|
} else if (/^([+\-]?[\d\.]+)%$/i.test(text)){ //percentage
|
|
this.type = "percentage";
|
|
this.value = +RegExp.$1;
|
|
} else if (/^([+\-]?\d+)$/i.test(text)){ //integer
|
|
this.type = "integer";
|
|
this.value = +RegExp.$1;
|
|
} 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);
|
|
} 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);
|
|
}
|
|
} else if (/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i.test(text)){ //rgb() color with absolute numbers
|
|
this.type = "color";
|
|
this.red = +RegExp.$1;
|
|
this.green = +RegExp.$2;
|
|
this.blue = +RegExp.$3;
|
|
} else if (/^rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i.test(text)){ //rgb() color with percentages
|
|
this.type = "color";
|
|
this.red = +RegExp.$1 * 255 / 100;
|
|
this.green = +RegExp.$2 * 255 / 100;
|
|
this.blue = +RegExp.$3 * 255 / 100;
|
|
} else if (/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d\.]+)\s*\)/i.test(text)){ //rgba() color with absolute numbers
|
|
this.type = "color";
|
|
this.red = +RegExp.$1;
|
|
this.green = +RegExp.$2;
|
|
this.blue = +RegExp.$3;
|
|
this.alpha = +RegExp.$4;
|
|
} else if (/^rgba\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([\d\.]+)\s*\)/i.test(text)){ //rgba() color with percentages
|
|
this.type = "color";
|
|
this.red = +RegExp.$1 * 255 / 100;
|
|
this.green = +RegExp.$2 * 255 / 100;
|
|
this.blue = +RegExp.$3 * 255 / 100;
|
|
this.alpha = +RegExp.$4;
|
|
} else if (/^hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i.test(text)){ //hsl()
|
|
this.type = "color";
|
|
this.hue = +RegExp.$1;
|
|
this.saturation = +RegExp.$2 / 100;
|
|
this.lightness = +RegExp.$3 / 100;
|
|
} else if (/^hsla\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([\d\.]+)\s*\)/i.test(text)){ //hsla() color with percentages
|
|
this.type = "color";
|
|
this.hue = +RegExp.$1;
|
|
this.saturation = +RegExp.$2 / 100;
|
|
this.lightness = +RegExp.$3 / 100;
|
|
this.alpha = +RegExp.$4;
|
|
} else if (/^url\(["']?([^\)"']+)["']?\)/i.test(text)){ //URI
|
|
this.type = "uri";
|
|
this.uri = RegExp.$1;
|
|
} else if (/^([^\(]+)\(/i.test(text)){
|
|
this.type = "function";
|
|
this.name = RegExp.$1;
|
|
this.value = text;
|
|
} else if (/^["'][^"']*["']/.test(text)){ //string
|
|
this.type = "string";
|
|
this.value = eval(text);
|
|
} else if (Colors[text.toLowerCase()]){ //named color
|
|
this.type = "color";
|
|
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);
|
|
} else if (/^[\,\/]$/.test(text)){
|
|
this.type = "operator";
|
|
this.value = text;
|
|
} else if (/^[a-z\-\u0080-\uFFFF][a-z0-9\-\u0080-\uFFFF]*$/i.test(text)){
|
|
this.type = "identifier";
|
|
this.value = text;
|
|
}
|
|
|
|
}
|
|
|
|
PropertyValuePart.prototype = new SyntaxUnit();
|
|
PropertyValuePart.prototype.constructor = PropertyValuePart;
|
|
|
|
/**
|
|
* Create a new syntax unit based solely on the given token.
|
|
* Convenience method for creating a new syntax unit when
|
|
* it represents a single token instead of multiple.
|
|
* @param {Object} token The token object to represent.
|
|
* @return {parserlib.css.PropertyValuePart} The object representing the token.
|
|
* @static
|
|
* @method fromToken
|
|
*/
|
|
PropertyValuePart.fromToken = function(token){
|
|
return new PropertyValuePart(token.value, token.startLine, token.startCol);
|
|
};
|
|
var Pseudos = {
|
|
":first-letter": 1,
|
|
":first-line": 1,
|
|
":before": 1,
|
|
":after": 1
|
|
};
|
|
|
|
Pseudos.ELEMENT = 1;
|
|
Pseudos.CLASS = 2;
|
|
|
|
Pseudos.isElement = function(pseudo){
|
|
return pseudo.indexOf("::") === 0 || Pseudos[pseudo.toLowerCase()] == Pseudos.ELEMENT;
|
|
};
|
|
/*global SyntaxUnit, Parser, Specificity*/
|
|
/**
|
|
* Represents an entire single selector, including all parts but not
|
|
* including multiple selectors (those separated by commas).
|
|
* @namespace parserlib.css
|
|
* @class Selector
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
* @param {Array} parts Array of selectors parts making up this selector.
|
|
* @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 Selector(parts, line, col){
|
|
|
|
SyntaxUnit.call(this, parts.join(" "), line, col, Parser.SELECTOR_TYPE);
|
|
|
|
/**
|
|
* The parts that make up the selector.
|
|
* @type Array
|
|
* @property parts
|
|
*/
|
|
this.parts = parts;
|
|
|
|
/**
|
|
* The specificity of the selector.
|
|
* @type parserlib.css.Specificity
|
|
* @property specificity
|
|
*/
|
|
this.specificity = Specificity.calculate(this);
|
|
|
|
}
|
|
|
|
Selector.prototype = new SyntaxUnit();
|
|
Selector.prototype.constructor = Selector;
|
|
|
|
|
|
/*global SyntaxUnit, Parser*/
|
|
/**
|
|
* Represents a single part of a selector string, meaning a single set of
|
|
* element name and modifiers. This does not include combinators such as
|
|
* spaces, +, >, etc.
|
|
* @namespace parserlib.css
|
|
* @class SelectorPart
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
* @param {String} elementName The element name in the selector or null
|
|
* 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 {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, Parser.SELECTOR_PART_TYPE);
|
|
|
|
/**
|
|
* The tag name of the element to which this part
|
|
* of the selector affects.
|
|
* @type String
|
|
* @property elementName
|
|
*/
|
|
this.elementName = elementName;
|
|
|
|
/**
|
|
* The parts that come after the element name, such as class names, IDs,
|
|
* pseudo classes/elements, etc.
|
|
* @type Array
|
|
* @property modifiers
|
|
*/
|
|
this.modifiers = modifiers;
|
|
|
|
}
|
|
|
|
SelectorPart.prototype = new SyntaxUnit();
|
|
SelectorPart.prototype.constructor = SelectorPart;
|
|
|
|
|
|
/*global SyntaxUnit, Parser*/
|
|
/**
|
|
* Represents a selector modifier string, meaning a class name, element name,
|
|
* element ID, pseudo rule, etc.
|
|
* @namespace parserlib.css
|
|
* @class SelectorSubPart
|
|
* @extends parserlib.util.SyntaxUnit
|
|
* @constructor
|
|
* @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, Parser.SELECTOR_SUB_PART_TYPE);
|
|
|
|
/**
|
|
* The type of modifier.
|
|
* @type String
|
|
* @property type
|
|
*/
|
|
this.type = type;
|
|
|
|
/**
|
|
* Some subparts have arguments, this represents them.
|
|
* @type Array
|
|
* @property args
|
|
*/
|
|
this.args = [];
|
|
|
|
}
|
|
|
|
SelectorSubPart.prototype = new SyntaxUnit();
|
|
SelectorSubPart.prototype.constructor = SelectorSubPart;
|
|
|
|
|
|
/*global Pseudos, SelectorPart*/
|
|
/**
|
|
* Represents a selector's specificity.
|
|
* @namespace parserlib.css
|
|
* @class Specificity
|
|
* @constructor
|
|
* @param {int} a Should be 1 for inline styles, zero for stylesheet styles
|
|
* @param {int} b Number of ID selectors
|
|
* @param {int} c Number of classes and pseudo classes
|
|
* @param {int} d Number of element names and pseudo elements
|
|
*/
|
|
function Specificity(a, b, c, d){
|
|
this.a = a;
|
|
this.b = b;
|
|
this.c = c;
|
|
this.d = d;
|
|
}
|
|
|
|
Specificity.prototype = {
|
|
constructor: Specificity,
|
|
|
|
/**
|
|
* Compare this specificity to another.
|
|
* @param {Specificity} other The other specificity to compare to.
|
|
* @return {int} -1 if the other specificity is larger, 1 if smaller, 0 if equal.
|
|
* @method compare
|
|
*/
|
|
compare: function(other){
|
|
var comps = ["a", "b", "c", "d"],
|
|
i, len;
|
|
|
|
for (i=0, len=comps.length; i < len; i++){
|
|
if (this[comps[i]] < other[comps[i]]){
|
|
return -1;
|
|
} else if (this[comps[i]] > other[comps[i]]){
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* Creates a numeric value for the specificity.
|
|
* @return {int} The numeric value for the specificity.
|
|
* @method valueOf
|
|
*/
|
|
valueOf: function(){
|
|
return (this.a * 1000) + (this.b * 100) + (this.c * 10) + this.d;
|
|
},
|
|
|
|
/**
|
|
* Returns a string representation for specificity.
|
|
* @return {String} The string representation of specificity.
|
|
* @method toString
|
|
*/
|
|
toString: function(){
|
|
return this.a + "," + this.b + "," + this.c + "," + this.d;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Calculates the specificity of the given selector.
|
|
* @param {parserlib.css.Selector} The selector to calculate specificity for.
|
|
* @return {parserlib.css.Specificity} The specificity of the selector.
|
|
* @static
|
|
* @method calculate
|
|
*/
|
|
Specificity.calculate = function(selector){
|
|
|
|
var i, len,
|
|
part,
|
|
b=0, c=0, d=0;
|
|
|
|
function updateValues(part){
|
|
|
|
var i, j, len, num,
|
|
elementName = part.elementName ? part.elementName.text : "",
|
|
modifier;
|
|
|
|
if (elementName && elementName.charAt(elementName.length-1) != "*") {
|
|
d++;
|
|
}
|
|
|
|
for (i=0, len=part.modifiers.length; i < len; i++){
|
|
modifier = part.modifiers[i];
|
|
switch(modifier.type){
|
|
case "class":
|
|
case "attribute":
|
|
c++;
|
|
break;
|
|
|
|
case "id":
|
|
b++;
|
|
break;
|
|
|
|
case "pseudo":
|
|
if (Pseudos.isElement(modifier.text)){
|
|
d++;
|
|
} else {
|
|
c++;
|
|
}
|
|
break;
|
|
|
|
case "not":
|
|
for (j=0, num=modifier.args.length; j < num; j++){
|
|
updateValues(modifier.args[j]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i=0, len=selector.parts.length; i < len; i++){
|
|
part = selector.parts[i];
|
|
|
|
if (part instanceof SelectorPart){
|
|
updateValues(part);
|
|
}
|
|
}
|
|
|
|
return new Specificity(0, b, c, d);
|
|
};
|
|
|
|
/*global Tokens, TokenStreamBase*/
|
|
|
|
var h = /^[0-9a-fA-F]$/,
|
|
nonascii = /^[\u0080-\uFFFF]$/,
|
|
nl = /\n|\r\n|\r|\f/;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Helper functions
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
function isHexDigit(c){
|
|
return c !== null && h.test(c);
|
|
}
|
|
|
|
function isDigit(c){
|
|
return c !== null && /\d/.test(c);
|
|
}
|
|
|
|
function isWhitespace(c){
|
|
return c !== null && /\s/.test(c);
|
|
}
|
|
|
|
function isNewLine(c){
|
|
return c !== null && nl.test(c);
|
|
}
|
|
|
|
function isNameStart(c){
|
|
return c !== null && (/[a-z_\u0080-\uFFFF\\]/i.test(c));
|
|
}
|
|
|
|
function isNameChar(c){
|
|
return c !== null && (isNameStart(c) || /[0-9\-\\]/.test(c));
|
|
}
|
|
|
|
function isIdentStart(c){
|
|
return c !== null && (isNameStart(c) || /\-\\/.test(c));
|
|
}
|
|
|
|
function mix(receiver, supplier){
|
|
for (var prop in supplier){
|
|
if (supplier.hasOwnProperty(prop)){
|
|
receiver[prop] = supplier[prop];
|
|
}
|
|
}
|
|
return receiver;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// CSS Token Stream
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
/**
|
|
* A token stream that produces CSS tokens.
|
|
* @param {String|Reader} input The source of text to tokenize.
|
|
* @constructor
|
|
* @class TokenStream
|
|
* @namespace parserlib.css
|
|
*/
|
|
function TokenStream(input){
|
|
TokenStreamBase.call(this, input, Tokens);
|
|
}
|
|
|
|
TokenStream.prototype = mix(new TokenStreamBase(), {
|
|
|
|
/**
|
|
* Overrides the TokenStreamBase method of the same name
|
|
* to produce CSS tokens.
|
|
* @param {variant} channel The name of the channel to use
|
|
* for the next token.
|
|
* @return {Object} A token object representing the next token.
|
|
* @method _getToken
|
|
* @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
|
|
* - SLASH
|
|
* - CHAR
|
|
*/
|
|
case "/":
|
|
|
|
if(reader.peek() == "*"){
|
|
token = this.commentToken(c, startLine, startCol);
|
|
} else {
|
|
token = this.charToken(c, startLine, startCol);
|
|
}
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - DASHMATCH
|
|
* - INCLUDES
|
|
* - PREFIXMATCH
|
|
* - SUFFIXMATCH
|
|
* - SUBSTRINGMATCH
|
|
* - CHAR
|
|
*/
|
|
case "|":
|
|
case "~":
|
|
case "^":
|
|
case "$":
|
|
case "*":
|
|
if(reader.peek() == "="){
|
|
token = this.comparisonToken(c, startLine, startCol);
|
|
} else {
|
|
token = this.charToken(c, startLine, startCol);
|
|
}
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - STRING
|
|
* - INVALID
|
|
*/
|
|
case "\"":
|
|
case "'":
|
|
token = this.stringToken(c, startLine, startCol);
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - HASH
|
|
* - CHAR
|
|
*/
|
|
case "#":
|
|
if (isNameChar(reader.peek())){
|
|
token = this.hashToken(c, startLine, startCol);
|
|
} else {
|
|
token = this.charToken(c, startLine, startCol);
|
|
}
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - DOT
|
|
* - NUMBER
|
|
* - DIMENSION
|
|
* - PERCENTAGE
|
|
*/
|
|
case ".":
|
|
if (isDigit(reader.peek())){
|
|
token = this.numberToken(c, startLine, startCol);
|
|
} else {
|
|
token = this.charToken(c, startLine, startCol);
|
|
}
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - CDC
|
|
* - MINUS
|
|
* - NUMBER
|
|
* - DIMENSION
|
|
* - PERCENTAGE
|
|
*/
|
|
case "-":
|
|
if (reader.peek() == "-"){ //could be closing HTML-style comment
|
|
token = this.htmlCommentEndToken(c, startLine, startCol);
|
|
} else if (isNameStart(reader.peek())){
|
|
token = this.identOrFunctionToken(c, startLine, startCol);
|
|
} else {
|
|
token = this.charToken(c, startLine, startCol);
|
|
}
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - IMPORTANT_SYM
|
|
* - CHAR
|
|
*/
|
|
case "!":
|
|
token = this.importantToken(c, startLine, startCol);
|
|
break;
|
|
|
|
/*
|
|
* Any at-keyword or CHAR
|
|
*/
|
|
case "@":
|
|
token = this.atRuleToken(c, startLine, startCol);
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - NOT
|
|
* - CHAR
|
|
*/
|
|
case ":":
|
|
token = this.notToken(c, startLine, startCol);
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - CDO
|
|
* - CHAR
|
|
*/
|
|
case "<":
|
|
token = this.htmlCommentStartToken(c, startLine, startCol);
|
|
break;
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - UNICODE_RANGE
|
|
* - URL
|
|
* - CHAR
|
|
*/
|
|
case "U":
|
|
case "u":
|
|
if (reader.peek() == "+"){
|
|
token = this.unicodeRangeToken(c, startLine, startCol);
|
|
break;
|
|
}
|
|
/* falls through */
|
|
default:
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - NUMBER
|
|
* - DIMENSION
|
|
* - LENGTH
|
|
* - FREQ
|
|
* - TIME
|
|
* - EMS
|
|
* - EXS
|
|
* - ANGLE
|
|
*/
|
|
if (isDigit(c)){
|
|
token = this.numberToken(c, startLine, startCol);
|
|
} else
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - S
|
|
*/
|
|
if (isWhitespace(c)){
|
|
token = this.whitespaceToken(c, startLine, startCol);
|
|
} else
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - IDENT
|
|
*/
|
|
if (isIdentStart(c)){
|
|
token = this.identOrFunctionToken(c, startLine, startCol);
|
|
} else
|
|
|
|
/*
|
|
* Potential tokens:
|
|
* - CHAR
|
|
* - PLUS
|
|
*/
|
|
{
|
|
token = this.charToken(c, startLine, startCol);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
//make sure this token is wanted
|
|
//TODO: check channel
|
|
break;
|
|
}
|
|
|
|
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
|
|
* private methods to create tokens and is never called directly.
|
|
* @param {int} tt The token type.
|
|
* @param {String} value The text value of the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @param {Object} options (Optional) Specifies a channel property
|
|
* to indicate that a different channel should be scanned
|
|
* and/or a hide property indicating that the token should
|
|
* 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,
|
|
channel: options.channel,
|
|
hide: options.hide || false,
|
|
startLine: startLine,
|
|
startCol: startCol,
|
|
endLine: reader.getLine(),
|
|
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.
|
|
* @param {String} first The first character for the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method atRuleToken
|
|
*/
|
|
atRuleToken: function(first, startLine, startCol){
|
|
var rule = first,
|
|
reader = this._reader,
|
|
tt = Tokens.CHAR,
|
|
valid = false,
|
|
ident,
|
|
c;
|
|
|
|
/*
|
|
* First, mark where we are. There are only four @ rules,
|
|
* so anything else is really just an invalid token.
|
|
* Basically, if this doesn't match one of the known @
|
|
* rules, just return '@' as an unknown token and allow
|
|
* parsing to continue after that point.
|
|
*/
|
|
reader.mark();
|
|
|
|
//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){
|
|
if (rule.length > 1){
|
|
tt = Tokens.UNKNOWN_SYM;
|
|
} else {
|
|
tt = Tokens.CHAR;
|
|
rule = first;
|
|
reader.reset();
|
|
}
|
|
}
|
|
|
|
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)
|
|
* token name, this is used; otherwise CHAR is used.
|
|
* @param {String} c The character for the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method charToken
|
|
*/
|
|
charToken: function(c, startLine, startCol){
|
|
var tt = Tokens.type(c);
|
|
|
|
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)
|
|
* token name, this is used; otherwise CHAR is used.
|
|
* @param {String} first The first character for the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method commentToken
|
|
*/
|
|
commentToken: function(first, startLine, startCol){
|
|
var reader = this._reader,
|
|
comment = this.readComment(first);
|
|
|
|
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
|
|
* read and is already known to be an equals sign.
|
|
* @param {String} c The character for the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method comparisonToken
|
|
*/
|
|
comparisonToken: function(c, startLine, startCol){
|
|
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
|
|
* method reads a name afterward.
|
|
* @param {String} first The first character (#) in the hash name.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method hashToken
|
|
*/
|
|
hashToken: function(first, startLine, startCol){
|
|
var reader = this._reader,
|
|
name = this.readName(first);
|
|
|
|
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
|
|
* the correct token to create.
|
|
* @param {String} first The first character in the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method htmlCommentStartToken
|
|
*/
|
|
htmlCommentStartToken: function(first, startLine, startCol){
|
|
var reader = this._reader,
|
|
text = first;
|
|
|
|
reader.mark();
|
|
text += reader.readCount(3);
|
|
|
|
if (text == "<!--"){
|
|
return this.createToken(Tokens.CDO, text, startLine, startCol);
|
|
} else {
|
|
reader.reset();
|
|
return this.charToken(first, startLine, startCol);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Produces a CDC or CHAR token based on the specified information. The
|
|
* first character is provided and the rest is read by the function to determine
|
|
* the correct token to create.
|
|
* @param {String} first The first character in the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method htmlCommentEndToken
|
|
*/
|
|
htmlCommentEndToken: function(first, startLine, startCol){
|
|
var reader = this._reader,
|
|
text = first;
|
|
|
|
reader.mark();
|
|
text += reader.readCount(2);
|
|
|
|
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
|
|
* the correct token to create.
|
|
* @param {String} first The first character in the identifier.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @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),
|
|
tt = Tokens.IDENT;
|
|
|
|
//if there's a left paren immediately after, it's a URI or function
|
|
if (reader.peek() == "("){
|
|
ident += reader.read();
|
|
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;
|
|
}
|
|
} else {
|
|
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("(");
|
|
tt = Tokens.IE_FUNCTION;
|
|
}
|
|
}
|
|
|
|
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
|
|
* the correct token to create.
|
|
* @param {String} first The first character in the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method importantToken
|
|
*/
|
|
importantToken: function(first, startLine, startCol){
|
|
var reader = this._reader,
|
|
important = first,
|
|
tt = Tokens.CHAR,
|
|
temp,
|
|
c;
|
|
|
|
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;
|
|
} else {
|
|
temp = this.readComment(c);
|
|
if (temp === ""){ //broken!
|
|
break;
|
|
}
|
|
}
|
|
} else if (isWhitespace(c)){
|
|
important += c + this.readWhitespace();
|
|
} else if (/i/i.test(c)){
|
|
temp = reader.readCount(8);
|
|
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);
|
|
}
|
|
|
|
|
|
},
|
|
|
|
/**
|
|
* Produces a NOT or CHAR token based on the specified information. The
|
|
* first character is provided and the rest is read by the function to determine
|
|
* the correct token to create.
|
|
* @param {String} first The first character in the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method notToken
|
|
*/
|
|
notToken: function(first, startLine, startCol){
|
|
var reader = this._reader,
|
|
text = first;
|
|
|
|
reader.mark();
|
|
text += reader.readCount(4);
|
|
|
|
if (text.toLowerCase() == ":not("){
|
|
return this.createToken(Tokens.NOT, text, startLine, startCol);
|
|
} else {
|
|
reader.reset();
|
|
return this.charToken(first, startLine, startCol);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Produces a number token based on the given character
|
|
* and location in the stream. This may return a token of
|
|
* NUMBER, EMS, EXS, LENGTH, ANGLE, TIME, FREQ, DIMENSION,
|
|
* or PERCENTAGE.
|
|
* @param {String} first The first character for the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method 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;
|
|
|
|
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;
|
|
} else if (/^ms$|^s$/i.test(ident)){
|
|
tt = Tokens.TIME;
|
|
} else if (/^hz$|^khz$/i.test(ident)){
|
|
tt = Tokens.FREQ;
|
|
} else if (/^dpi$|^dpcm$/i.test(ident)){
|
|
tt = Tokens.RESOLUTION;
|
|
} else {
|
|
tt = Tokens.DIMENSION;
|
|
}
|
|
|
|
} else if (c == "%"){
|
|
value += reader.read();
|
|
tt = Tokens.PERCENTAGE;
|
|
}
|
|
|
|
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
|
|
* by single or double quotes, a failure to match starting
|
|
* and ending quotes results in an INVALID token being generated.
|
|
* The first character in the string is passed in and then
|
|
* the rest are read up to and including the final quotation mark.
|
|
* @param {String} first The first character in the string.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @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,
|
|
reader = this._reader,
|
|
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;
|
|
}
|
|
|
|
//if there's a newline without an escapement, it's an invalid string
|
|
if (isNewLine(reader.peek()) && c != "\\"){
|
|
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);
|
|
},
|
|
|
|
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();
|
|
} else {
|
|
value += temp;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
* into a single token.
|
|
* @param {String} first The first character in the token.
|
|
* @param {int} startLine The beginning line for the character.
|
|
* @param {int} startCol The beginning column for the character.
|
|
* @return {Object} A token object.
|
|
* @method whitespaceToken
|
|
*/
|
|
whitespaceToken: function(first, startLine, startCol){
|
|
var reader = this._reader,
|
|
value = first + this.readWhitespace();
|
|
return this.createToken(Tokens.S, value, startLine, startCol);
|
|
},
|
|
|
|
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Methods to read values from the string stream
|
|
//-------------------------------------------------------------------------
|
|
|
|
readUnicodeRangePart: function(allowQuestionMark){
|
|
var reader = this._reader,
|
|
part = "",
|
|
c = reader.peek();
|
|
|
|
//first read hex digits
|
|
while(isHexDigit(c) && part.length < 6){
|
|
reader.read();
|
|
part += c;
|
|
c = reader.peek();
|
|
}
|
|
|
|
//then read question marks if allowed
|
|
if (allowQuestionMark){
|
|
while(c == "?" && part.length < 6){
|
|
reader.read();
|
|
part += c;
|
|
c = reader.peek();
|
|
}
|
|
}
|
|
|
|
//there can't be any other characters after this point
|
|
|
|
return part;
|
|
},
|
|
|
|
readWhitespace: function(){
|
|
var reader = this._reader,
|
|
whitespace = "",
|
|
c = reader.peek();
|
|
|
|
while(isWhitespace(c)){
|
|
reader.read();
|
|
whitespace += c;
|
|
c = reader.peek();
|
|
}
|
|
|
|
return whitespace;
|
|
},
|
|
readNumber: function(first){
|
|
var reader = this._reader,
|
|
number = first,
|
|
hasDot = (first == "."),
|
|
c = reader.peek();
|
|
|
|
|
|
while(c){
|
|
if (isDigit(c)){
|
|
number += reader.read();
|
|
} else if (c == "."){
|
|
if (hasDot){
|
|
break;
|
|
} else {
|
|
hasDot = true;
|
|
number += reader.read();
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
c = reader.peek();
|
|
}
|
|
|
|
return number;
|
|
},
|
|
readString: function(){
|
|
var reader = this._reader,
|
|
delim = reader.read(),
|
|
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;
|
|
}
|
|
|
|
//if there's a newline without an escapement, it's an invalid string
|
|
if (isNewLine(reader.peek()) && c != "\\"){
|
|
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){
|
|
var reader = this._reader,
|
|
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;
|
|
reader.reset();
|
|
} 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(true){
|
|
if (c == "\\"){
|
|
ident += this.readEscape(reader.read());
|
|
c = reader.peek();
|
|
} else if(c && isNameChar(c)){
|
|
ident += reader.read();
|
|
c = reader.peek();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ident;
|
|
},
|
|
|
|
readEscape: function(first){
|
|
var reader = this._reader,
|
|
cssEscape = first || "",
|
|
i = 0,
|
|
c = reader.peek();
|
|
|
|
if (isHexDigit(c)){
|
|
do {
|
|
cssEscape += reader.read();
|
|
c = reader.peek();
|
|
} while(c && isHexDigit(c) && ++i < 6);
|
|
}
|
|
|
|
if (cssEscape.length == 3 && /\s/.test(c) ||
|
|
cssEscape.length == 7 || cssEscape.length == 1){
|
|
reader.read();
|
|
} else {
|
|
c = "";
|
|
}
|
|
|
|
return cssEscape + c;
|
|
},
|
|
|
|
readComment: function(first){
|
|
var reader = this._reader,
|
|
comment = first || "",
|
|
c = reader.read();
|
|
|
|
if (c == "*"){
|
|
while(c){
|
|
comment += c;
|
|
|
|
//look for end of comment
|
|
if (comment.length > 2 && 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"},
|
|
|
|
//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: "IDENT"},
|
|
{ name: "HASH"},
|
|
|
|
//at-keywords
|
|
{ name: "IMPORT_SYM", text: "@import"},
|
|
{ name: "PAGE_SYM", text: "@page"},
|
|
{ name: "MEDIA_SYM", text: "@media"},
|
|
{ name: "FONT_FACE_SYM", text: "@font-face"},
|
|
{ name: "CHARSET_SYM", text: "@charset"},
|
|
{ name: "NAMESPACE_SYM", text: "@namespace"},
|
|
{ name: "UNKNOWN_SYM" },
|
|
//{ name: "ATKEYWORD"},
|
|
|
|
//CSS3 animations
|
|
{ name: "KEYFRAMES_SYM", text: [ "@keyframes", "@-webkit-keyframes", "@-moz-keyframes", "@-ms-keyframes" ] },
|
|
|
|
//important symbol
|
|
{ name: "IMPORTANT_SYM"},
|
|
|
|
//measurements
|
|
{ name: "LENGTH"},
|
|
{ name: "ANGLE"},
|
|
{ name: "TIME"},
|
|
{ name: "FREQ"},
|
|
{ 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"},
|
|
|
|
/*
|
|
* Defined in CSS3 Paged Media
|
|
*/
|
|
{ name: "TOPLEFTCORNER_SYM", text: "@top-left-corner"},
|
|
{ name: "TOPLEFT_SYM", text: "@top-left"},
|
|
{ name: "TOPCENTER_SYM", text: "@top-center"},
|
|
{ name: "TOPRIGHT_SYM", text: "@top-right"},
|
|
{ name: "TOPRIGHTCORNER_SYM", text: "@top-right-corner"},
|
|
{ name: "BOTTOMLEFTCORNER_SYM", text: "@bottom-left-corner"},
|
|
{ name: "BOTTOMLEFT_SYM", text: "@bottom-left"},
|
|
{ name: "BOTTOMCENTER_SYM", text: "@bottom-center"},
|
|
{ name: "BOTTOMRIGHT_SYM", text: "@bottom-right"},
|
|
{ name: "BOTTOMRIGHTCORNER_SYM", text: "@bottom-right-corner"},
|
|
{ name: "LEFTTOP_SYM", text: "@left-top"},
|
|
{ name: "LEFTMIDDLE_SYM", text: "@left-middle"},
|
|
{ name: "LEFTBOTTOM_SYM", text: "@left-bottom"},
|
|
{ name: "RIGHTTOP_SYM", text: "@right-top"},
|
|
{ name: "RIGHTMIDDLE_SYM", text: "@right-middle"},
|
|
{ name: "RIGHTBOTTOM_SYM", text: "@right-bottom"},
|
|
|
|
/*
|
|
* The following token names are defined in CSS3 Media Queries: http://www.w3.org/TR/css3-mediaqueries/#syntax
|
|
*/
|
|
/*{ name: "MEDIA_ONLY", state: "media"},
|
|
{ name: "MEDIA_NOT", state: "media"},
|
|
{ name: "MEDIA_AND", state: "media"},*/
|
|
{ name: "RESOLUTION", state: "media"},
|
|
|
|
/*
|
|
* 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
|
|
{
|
|
name: "PIPE",
|
|
text: "|"
|
|
},
|
|
{
|
|
name: "SLASH",
|
|
text: "/"
|
|
},
|
|
{
|
|
name: "MINUS",
|
|
text: "-"
|
|
},
|
|
{
|
|
name: "STAR",
|
|
text: "*"
|
|
},
|
|
|
|
{
|
|
name: "LBRACE",
|
|
text: "{"
|
|
},
|
|
{
|
|
name: "RBRACE",
|
|
text: "}"
|
|
},
|
|
{
|
|
name: "LBRACKET",
|
|
text: "["
|
|
},
|
|
{
|
|
name: "RBRACKET",
|
|
text: "]"
|
|
},
|
|
{
|
|
name: "EQUALS",
|
|
text: "="
|
|
},
|
|
{
|
|
name: "COLON",
|
|
text: ":"
|
|
},
|
|
{
|
|
name: "SEMICOLON",
|
|
text: ";"
|
|
},
|
|
|
|
{
|
|
name: "LPAREN",
|
|
text: "("
|
|
},
|
|
{
|
|
name: "RPAREN",
|
|
text: ")"
|
|
},
|
|
{
|
|
name: "DOT",
|
|
text: "."
|
|
}
|
|
];
|
|
|
|
(function(){
|
|
|
|
var nameMap = [],
|
|
typeMap = {};
|
|
|
|
Tokens.UNKNOWN = -1;
|
|
Tokens.unshift({name:"EOF"});
|
|
for (var i=0, len = Tokens.length; i < len; i++){
|
|
nameMap.push(Tokens[i].name);
|
|
Tokens[Tokens[i].name] = i;
|
|
if (Tokens[i].text){
|
|
if (Tokens[i].text instanceof Array){
|
|
for (var j=0; j < Tokens[i].text.length; j++){
|
|
typeMap[Tokens[i].text[j]] = i;
|
|
}
|
|
} else {
|
|
typeMap[Tokens[i].text] = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
Tokens.name = function(tt){
|
|
return nameMap[tt];
|
|
};
|
|
|
|
Tokens.type = function(c){
|
|
return typeMap[c] || -1;
|
|
};
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
//This file will likely change a lot! Very experimental!
|
|
/*global Properties, ValidationTypes, ValidationError, PropertyValueIterator */
|
|
var Validation = {
|
|
|
|
validate: function(property, value){
|
|
|
|
//normalize name
|
|
var name = property.toString().toLowerCase(),
|
|
parts = value.parts,
|
|
expression = new PropertyValueIterator(value),
|
|
spec = Properties[name],
|
|
part,
|
|
valid,
|
|
j, count,
|
|
msg,
|
|
types,
|
|
last,
|
|
literals,
|
|
max, multi, group;
|
|
|
|
if (!spec) {
|
|
if (name.indexOf("-") !== 0){ //vendor prefixed are ok
|
|
throw new ValidationError("Unknown property '" + property + "'.", property.line, property.col);
|
|
}
|
|
} else if (typeof spec != "number"){
|
|
|
|
//initialization
|
|
if (typeof spec == "string"){
|
|
if (spec.indexOf("||") > -1) {
|
|
this.groupProperty(spec, expression);
|
|
} else {
|
|
this.singleProperty(spec, expression, 1);
|
|
}
|
|
|
|
} else if (spec.multi) {
|
|
this.multiProperty(spec.multi, expression, spec.comma, spec.max || Infinity);
|
|
} else if (typeof spec == "function") {
|
|
spec(expression);
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
singleProperty: function(types, expression, max, partial) {
|
|
|
|
var result = false,
|
|
value = expression.value,
|
|
count = 0,
|
|
part;
|
|
|
|
while (expression.hasNext() && count < max) {
|
|
result = ValidationTypes.isAny(expression, types);
|
|
if (!result) {
|
|
break;
|
|
}
|
|
count++;
|
|
}
|
|
|
|
if (!result) {
|
|
if (expression.hasNext() && !expression.isFirst()) {
|
|
part = expression.peek();
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
} else {
|
|
throw new ValidationError("Expected (" + types + ") but found '" + value + "'.", value.line, value.col);
|
|
}
|
|
} else if (expression.hasNext()) {
|
|
part = expression.next();
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
}
|
|
|
|
},
|
|
|
|
multiProperty: function (types, expression, comma, max) {
|
|
|
|
var result = false,
|
|
value = expression.value,
|
|
count = 0,
|
|
sep = false,
|
|
part;
|
|
|
|
while(expression.hasNext() && !result && count < max) {
|
|
if (ValidationTypes.isAny(expression, types)) {
|
|
count++;
|
|
if (!expression.hasNext()) {
|
|
result = true;
|
|
|
|
} else if (comma) {
|
|
if (expression.peek() == ",") {
|
|
part = expression.next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
if (!result) {
|
|
if (expression.hasNext() && !expression.isFirst()) {
|
|
part = expression.peek();
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
} else {
|
|
part = expression.previous();
|
|
if (comma && part == ",") {
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
} else {
|
|
throw new ValidationError("Expected (" + types + ") but found '" + value + "'.", value.line, value.col);
|
|
}
|
|
}
|
|
|
|
} else if (expression.hasNext()) {
|
|
part = expression.next();
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
}
|
|
|
|
},
|
|
|
|
groupProperty: function (types, expression, comma) {
|
|
|
|
var result = false,
|
|
value = expression.value,
|
|
typeCount = types.split("||").length,
|
|
groups = { count: 0 },
|
|
partial = false,
|
|
name,
|
|
part;
|
|
|
|
while(expression.hasNext() && !result) {
|
|
name = ValidationTypes.isAnyOfGroup(expression, types);
|
|
if (name) {
|
|
|
|
//no dupes
|
|
if (groups[name]) {
|
|
break;
|
|
} else {
|
|
groups[name] = 1;
|
|
groups.count++;
|
|
partial = true;
|
|
|
|
if (groups.count == typeCount || !expression.hasNext()) {
|
|
result = true;
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!result) {
|
|
if (partial && expression.hasNext()) {
|
|
part = expression.peek();
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
} else {
|
|
throw new ValidationError("Expected (" + types + ") but found '" + value + "'.", value.line, value.col);
|
|
}
|
|
} else if (expression.hasNext()) {
|
|
part = expression.next();
|
|
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
};
|
|
/**
|
|
* Type to use when a validation error occurs.
|
|
* @class ValidationError
|
|
* @namespace parserlib.util
|
|
* @constructor
|
|
* @param {String} message The error message.
|
|
* @param {int} line The line at which the error occurred.
|
|
* @param {int} col The column at which the error occurred.
|
|
*/
|
|
function ValidationError(message, line, col){
|
|
|
|
/**
|
|
* The column at which the error occurred.
|
|
* @type int
|
|
* @property col
|
|
*/
|
|
this.col = col;
|
|
|
|
/**
|
|
* The line at which the error occurred.
|
|
* @type int
|
|
* @property line
|
|
*/
|
|
this.line = line;
|
|
|
|
/**
|
|
* The text representation of the unit.
|
|
* @type String
|
|
* @property text
|
|
*/
|
|
this.message = message;
|
|
|
|
}
|
|
|
|
//inherit from Error
|
|
ValidationError.prototype = new Error();
|
|
//This file will likely change a lot! Very experimental!
|
|
/*global Properties, Validation, ValidationError, PropertyValueIterator, console*/
|
|
var ValidationTypes = {
|
|
|
|
isLiteral: function (part, literals) {
|
|
var text = part.text.toString().toLowerCase(),
|
|
args = literals.split(" | "),
|
|
i, len, found = false;
|
|
|
|
for (i=0,len=args.length; i < len && !found; i++){
|
|
if (text == args[i].toLowerCase()){
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
return found;
|
|
},
|
|
|
|
isSimple: function(type) {
|
|
return !!this.simple[type];
|
|
},
|
|
|
|
isComplex: function(type) {
|
|
return !!this.complex[type];
|
|
},
|
|
|
|
/**
|
|
* Determines if the next part(s) of the given expression
|
|
* are any of the given types.
|
|
*/
|
|
isAny: function (expression, types) {
|
|
var args = types.split(" | "),
|
|
i, len, found = false;
|
|
|
|
for (i=0,len=args.length; i < len && !found && expression.hasNext(); i++){
|
|
found = this.isType(expression, args[i]);
|
|
}
|
|
|
|
return found;
|
|
},
|
|
|
|
/**
|
|
* Determines if the next part(s) of the given expresion
|
|
* are one of a group.
|
|
*/
|
|
isAnyOfGroup: function(expression, types) {
|
|
var args = types.split(" || "),
|
|
i, len, found = false;
|
|
|
|
for (i=0,len=args.length; i < len && !found; i++){
|
|
found = this.isType(expression, args[i]);
|
|
}
|
|
|
|
return found ? args[i-1] : false;
|
|
},
|
|
|
|
/**
|
|
* Determines if the next part(s) of the given expression
|
|
* are of a given type.
|
|
*/
|
|
isType: function (expression, type) {
|
|
var part = expression.peek(),
|
|
result = false;
|
|
|
|
if (type.charAt(0) != "<") {
|
|
result = this.isLiteral(part, type);
|
|
if (result) {
|
|
expression.next();
|
|
}
|
|
} else if (this.simple[type]) {
|
|
result = this.simple[type](part);
|
|
if (result) {
|
|
expression.next();
|
|
}
|
|
} else {
|
|
result = this.complex[type](expression);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
|
|
|
|
simple: {
|
|
|
|
"<absolute-size>": function(part){
|
|
return ValidationTypes.isLiteral(part, "xx-small | x-small | small | medium | large | x-large | xx-large");
|
|
},
|
|
|
|
"<attachment>": function(part){
|
|
return ValidationTypes.isLiteral(part, "scroll | fixed | local");
|
|
},
|
|
|
|
"<attr>": function(part){
|
|
return part.type == "function" && part.name == "attr";
|
|
},
|
|
|
|
"<bg-image>": function(part){
|
|
return this["<image>"](part) || this["<gradient>"](part) || part == "none";
|
|
},
|
|
|
|
"<gradient>": function(part) {
|
|
return part.type == "function" && /^(?:\-(?:ms|moz|o|webkit)\-)?(?:repeating\-)?(?:radial\-|linear\-)?gradient/i.test(part);
|
|
},
|
|
|
|
"<box>": function(part){
|
|
return ValidationTypes.isLiteral(part, "padding-box | border-box | content-box");
|
|
},
|
|
|
|
"<content>": function(part){
|
|
return part.type == "function" && part.name == "content";
|
|
},
|
|
|
|
"<relative-size>": function(part){
|
|
return ValidationTypes.isLiteral(part, "smaller | larger");
|
|
},
|
|
|
|
//any identifier
|
|
"<ident>": function(part){
|
|
return part.type == "identifier";
|
|
},
|
|
|
|
"<length>": function(part){
|
|
return part.type == "length" || part.type == "number" || part.type == "integer" || part == "0";
|
|
},
|
|
|
|
"<color>": function(part){
|
|
return part.type == "color" || part == "transparent";
|
|
},
|
|
|
|
"<number>": function(part){
|
|
return part.type == "number" || this["<integer>"](part);
|
|
},
|
|
|
|
"<integer>": function(part){
|
|
return part.type == "integer";
|
|
},
|
|
|
|
"<line>": function(part){
|
|
return part.type == "integer";
|
|
},
|
|
|
|
"<angle>": function(part){
|
|
return part.type == "angle";
|
|
},
|
|
|
|
"<uri>": function(part){
|
|
return part.type == "uri";
|
|
},
|
|
|
|
"<image>": function(part){
|
|
return this["<uri>"](part);
|
|
},
|
|
|
|
"<percentage>": function(part){
|
|
return part.type == "percentage" || part == "0";
|
|
},
|
|
|
|
"<border-width>": function(part){
|
|
return this["<length>"](part) || ValidationTypes.isLiteral(part, "thin | medium | thick");
|
|
},
|
|
|
|
"<border-style>": function(part){
|
|
return ValidationTypes.isLiteral(part, "none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset");
|
|
},
|
|
|
|
"<margin-width>": function(part){
|
|
return this["<length>"](part) || this["<percentage>"](part) || ValidationTypes.isLiteral(part, "auto");
|
|
},
|
|
|
|
"<padding-width>": function(part){
|
|
return this["<length>"](part) || this["<percentage>"](part);
|
|
},
|
|
|
|
"<shape>": function(part){
|
|
return part.type == "function" && (part.name == "rect" || part.name == "inset-rect");
|
|
},
|
|
|
|
"<time>": function(part) {
|
|
return part.type == "time";
|
|
}
|
|
},
|
|
|
|
complex: {
|
|
|
|
"<bg-position>": function(expression){
|
|
var types = this,
|
|
result = false,
|
|
numeric = "<percentage> | <length>",
|
|
xDir = "left | center | right",
|
|
yDir = "top | center | bottom",
|
|
part,
|
|
i, len;
|
|
|
|
/*
|
|
<position> = [
|
|
[ left | center | right | top | bottom | <percentage> | <length> ]
|
|
|
|
|
[ left | center | right | <percentage> | <length> ]
|
|
[ top | center | bottom | <percentage> | <length> ]
|
|
|
|
|
[ center | [ left | right ] [ <percentage> | <length> ]? ] &&
|
|
[ center | [ top | bottom ] [ <percentage> | <length> ]? ]
|
|
]
|
|
|
|
*/
|
|
|
|
if (ValidationTypes.isAny(expression, "top | bottom")) {
|
|
result = true;
|
|
} else {
|
|
|
|
//must be two-part
|
|
if (ValidationTypes.isAny(expression, numeric)){
|
|
if (expression.hasNext()){
|
|
result = ValidationTypes.isAny(expression, numeric + " | " + yDir);
|
|
}
|
|
} else if (ValidationTypes.isAny(expression, xDir)){
|
|
if (expression.hasNext()){
|
|
|
|
//two- or three-part
|
|
if (ValidationTypes.isAny(expression, yDir)){
|
|
result = true;
|
|
|
|
ValidationTypes.isAny(expression, numeric);
|
|
|
|
} else if (ValidationTypes.isAny(expression, numeric)){
|
|
|
|
//could also be two-part, so check the next part
|
|
if (ValidationTypes.isAny(expression, yDir)){
|
|
ValidationTypes.isAny(expression, numeric);
|
|
}
|
|
|
|
result = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return result;
|
|
},
|
|
|
|
"<bg-size>": function(expression){
|
|
//<bg-size> = [ <length> | <percentage> | auto ]{1,2} | cover | contain
|
|
var types = this,
|
|
result = false,
|
|
numeric = "<percentage> | <length> | auto",
|
|
part,
|
|
i, len;
|
|
|
|
if (ValidationTypes.isAny(expression, "cover | contain")) {
|
|
result = true;
|
|
} else if (ValidationTypes.isAny(expression, numeric)) {
|
|
result = true;
|
|
ValidationTypes.isAny(expression, numeric);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
"<repeat-style>": function(expression){
|
|
//repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2}
|
|
var result = false,
|
|
values = "repeat | space | round | no-repeat",
|
|
part;
|
|
|
|
if (expression.hasNext()){
|
|
part = expression.next();
|
|
|
|
if (ValidationTypes.isLiteral(part, "repeat-x | repeat-y")) {
|
|
result = true;
|
|
} else if (ValidationTypes.isLiteral(part, values)) {
|
|
result = true;
|
|
|
|
if (expression.hasNext() && ValidationTypes.isLiteral(expression.peek(), values)) {
|
|
expression.next();
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
"<shadow>": function(expression) {
|
|
//inset? && [ <length>{2,4} && <color>? ]
|
|
var result = false,
|
|
count = 0,
|
|
inset = false,
|
|
color = false,
|
|
part;
|
|
|
|
if (expression.hasNext()) {
|
|
|
|
if (ValidationTypes.isAny(expression, "inset")){
|
|
inset = true;
|
|
}
|
|
|
|
if (ValidationTypes.isAny(expression, "<color>")) {
|
|
color = true;
|
|
}
|
|
|
|
while (ValidationTypes.isAny(expression, "<length>") && count < 4) {
|
|
count++;
|
|
}
|
|
|
|
|
|
if (expression.hasNext()) {
|
|
if (!color) {
|
|
ValidationTypes.isAny(expression, "<color>");
|
|
}
|
|
|
|
if (!inset) {
|
|
ValidationTypes.isAny(expression, "inset");
|
|
}
|
|
|
|
}
|
|
|
|
result = (count >= 2 && count <= 4);
|
|
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
"<x-one-radius>": function(expression) {
|
|
//[ <length> | <percentage> ] [ <length> | <percentage> ]?
|
|
var result = false,
|
|
count = 0,
|
|
numeric = "<length> | <percentage>",
|
|
part;
|
|
|
|
if (ValidationTypes.isAny(expression, numeric)){
|
|
result = true;
|
|
|
|
ValidationTypes.isAny(expression, numeric);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
parserlib.css = {
|
|
Colors :Colors,
|
|
Combinator :Combinator,
|
|
Parser :Parser,
|
|
PropertyName :PropertyName,
|
|
PropertyValue :PropertyValue,
|
|
PropertyValuePart :PropertyValuePart,
|
|
MediaFeature :MediaFeature,
|
|
MediaQuery :MediaQuery,
|
|
Selector :Selector,
|
|
SelectorPart :SelectorPart,
|
|
SelectorSubPart :SelectorSubPart,
|
|
Specificity :Specificity,
|
|
TokenStream :TokenStream,
|
|
Tokens :Tokens,
|
|
ValidationError :ValidationError
|
|
};
|
|
})();
|
|
|
|
|
|
|
|
/**
|
|
* Main CSSLint object.
|
|
* @class CSSLint
|
|
* @static
|
|
* @extends parserlib.util.EventTarget
|
|
*/
|
|
/*global parserlib, Reporter*/
|
|
var CSSLint = (function(){
|
|
|
|
var rules = [],
|
|
formatters = [],
|
|
api = new parserlib.util.EventTarget();
|
|
|
|
api.version = "0.9.8";
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Rule Management
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Adds a new rule to the engine.
|
|
* @param {Object} rule The rule to add.
|
|
* @method addRule
|
|
*/
|
|
api.addRule = function(rule){
|
|
rules.push(rule);
|
|
rules[rule.id] = rule;
|
|
};
|
|
|
|
/**
|
|
* Clears all rule from the engine.
|
|
* @method clearRules
|
|
*/
|
|
api.clearRules = function(){
|
|
rules = [];
|
|
};
|
|
|
|
/**
|
|
* Returns the rule objects.
|
|
* @return An array of rule objects.
|
|
* @method getRules
|
|
*/
|
|
api.getRules = function(){
|
|
return [].concat(rules).sort(function(a,b){
|
|
return a.id > b.id ? 1 : 0;
|
|
});
|
|
};
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Formatters
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Adds a new formatter to the engine.
|
|
* @param {Object} formatter The formatter to add.
|
|
* @method addFormatter
|
|
*/
|
|
api.addFormatter = function(formatter) {
|
|
// formatters.push(formatter);
|
|
formatters[formatter.id] = formatter;
|
|
};
|
|
|
|
/**
|
|
* Retrieves a formatter for use.
|
|
* @param {String} formatId The name of the format to retrieve.
|
|
* @return {Object} The formatter or undefined.
|
|
* @method getFormatter
|
|
*/
|
|
api.getFormatter = function(formatId){
|
|
return formatters[formatId];
|
|
};
|
|
|
|
/**
|
|
* Formats the results in a particular format for a single file.
|
|
* @param {Object} result The results returned from CSSLint.verify().
|
|
* @param {String} filename The filename for which the results apply.
|
|
* @param {String} formatId The name of the formatter to use.
|
|
* @param {Object} options (Optional) for special output handling.
|
|
* @return {String} A formatted string for the results.
|
|
* @method format
|
|
*/
|
|
api.format = function(results, filename, formatId, options) {
|
|
var formatter = this.getFormatter(formatId),
|
|
result = null;
|
|
|
|
if (formatter){
|
|
result = formatter.startFormat();
|
|
result += formatter.formatResults(results, filename, options || {});
|
|
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. If a rule has a value of 1 then it's a warning,
|
|
* a value of 2 means it's an error.
|
|
* @return {Object} Results of the verification.
|
|
* @method verify
|
|
*/
|
|
api.verify = function(text, ruleset){
|
|
|
|
var i = 0,
|
|
len = rules.length,
|
|
reporter,
|
|
lines,
|
|
report,
|
|
parser = new parserlib.css.Parser({ starHack: true, ieFilters: true,
|
|
underscoreHack: true, strict: false });
|
|
|
|
lines = text.replace(/\n\r?/g, "$split$").split('$split$');
|
|
|
|
if (!ruleset){
|
|
ruleset = {};
|
|
while (i < len){
|
|
ruleset[rules[i++].id] = 1; //by default, everything is a warning
|
|
}
|
|
}
|
|
|
|
reporter = new Reporter(lines, ruleset);
|
|
|
|
ruleset.errors = 2; //always report parsing errors as errors
|
|
for (i in ruleset){
|
|
if(ruleset.hasOwnProperty(i)){
|
|
if (rules[i]){
|
|
rules[i].init(parser, reporter);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//capture most horrible error type
|
|
try {
|
|
parser.parse(text);
|
|
} catch (ex) {
|
|
reporter.error("Fatal error, cannot continue: " + ex.message, ex.line, ex.col, {});
|
|
}
|
|
|
|
report = {
|
|
messages : reporter.messages,
|
|
stats : reporter.stats
|
|
};
|
|
|
|
//sort by line numbers, rollups at the bottom
|
|
report.messages.sort(function (a, b){
|
|
if (a.rollup && !b.rollup){
|
|
return 1;
|
|
} else if (!a.rollup && b.rollup){
|
|
return -1;
|
|
} else {
|
|
return a.line - b.line;
|
|
}
|
|
});
|
|
|
|
return report;
|
|
};
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Publish the API
|
|
//-------------------------------------------------------------------------
|
|
|
|
return api;
|
|
|
|
})();
|
|
|
|
/*global CSSLint*/
|
|
/**
|
|
* An instance of Report is used to report results of the
|
|
* verification back to the main API.
|
|
* @class Reporter
|
|
* @constructor
|
|
* @param {String[]} lines The text lines of the source.
|
|
* @param {Object} ruleset The set of rules to work with, including if
|
|
* they are errors or warnings.
|
|
*/
|
|
function Reporter(lines, ruleset){
|
|
|
|
/**
|
|
* List of messages being reported.
|
|
* @property messages
|
|
* @type String[]
|
|
*/
|
|
this.messages = [];
|
|
|
|
/**
|
|
* List of statistics being reported.
|
|
* @property stats
|
|
* @type String[]
|
|
*/
|
|
this.stats = [];
|
|
|
|
/**
|
|
* Lines of code being reported on. Used to provide contextual information
|
|
* for messages.
|
|
* @property lines
|
|
* @type String[]
|
|
*/
|
|
this.lines = lines;
|
|
|
|
/**
|
|
* Information about the rules. Used to determine whether an issue is an
|
|
* error or warning.
|
|
* @property ruleset
|
|
* @type Object
|
|
*/
|
|
this.ruleset = ruleset;
|
|
}
|
|
|
|
Reporter.prototype = {
|
|
|
|
//restore constructor
|
|
constructor: Reporter,
|
|
|
|
/**
|
|
* Report an error.
|
|
* @param {String} message The message to store.
|
|
* @param {int} line The line number.
|
|
* @param {int} col The column number.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method error
|
|
*/
|
|
error: function(message, line, col, rule){
|
|
this.messages.push({
|
|
type : "error",
|
|
line : line,
|
|
col : col,
|
|
message : message,
|
|
evidence: this.lines[line-1],
|
|
rule : rule || {}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report an warning.
|
|
* @param {String} message The message to store.
|
|
* @param {int} line The line number.
|
|
* @param {int} col The column number.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method warn
|
|
* @deprecated Use report instead.
|
|
*/
|
|
warn: function(message, line, col, rule){
|
|
this.report(message, line, col, rule);
|
|
},
|
|
|
|
/**
|
|
* Report an issue.
|
|
* @param {String} message The message to store.
|
|
* @param {int} line The line number.
|
|
* @param {int} col The column number.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method report
|
|
*/
|
|
report: function(message, line, col, rule){
|
|
this.messages.push({
|
|
type : this.ruleset[rule.id] == 2 ? "error" : "warning",
|
|
line : line,
|
|
col : col,
|
|
message : message,
|
|
evidence: this.lines[line-1],
|
|
rule : rule
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report some informational text.
|
|
* @param {String} message The message to store.
|
|
* @param {int} line The line number.
|
|
* @param {int} col The column number.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method info
|
|
*/
|
|
info: function(message, line, col, rule){
|
|
this.messages.push({
|
|
type : "info",
|
|
line : line,
|
|
col : col,
|
|
message : message,
|
|
evidence: this.lines[line-1],
|
|
rule : rule
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report some rollup error information.
|
|
* @param {String} message The message to store.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method rollupError
|
|
*/
|
|
rollupError: function(message, rule){
|
|
this.messages.push({
|
|
type : "error",
|
|
rollup : true,
|
|
message : message,
|
|
rule : rule
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report some rollup warning information.
|
|
* @param {String} message The message to store.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method rollupWarn
|
|
*/
|
|
rollupWarn: function(message, rule){
|
|
this.messages.push({
|
|
type : "warning",
|
|
rollup : true,
|
|
message : message,
|
|
rule : rule
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report a statistic.
|
|
* @param {String} name The name of the stat to store.
|
|
* @param {Variant} value The value of the stat.
|
|
* @method stat
|
|
*/
|
|
stat: function(name, value){
|
|
this.stats[name] = value;
|
|
}
|
|
};
|
|
|
|
//expose for testing purposes
|
|
CSSLint._Reporter = Reporter;
|
|
|
|
/*global CSSLint*/
|
|
|
|
/*
|
|
* Utility functions that make life easier.
|
|
*/
|
|
CSSLint.Util = {
|
|
/*
|
|
* Adds all properties from supplier onto receiver,
|
|
* overwriting if the same name already exists on
|
|
* reciever.
|
|
* @param {Object} The object to receive the properties.
|
|
* @param {Object} The object to provide the properties.
|
|
* @return {Object} The receiver
|
|
*/
|
|
mix: function(receiver, supplier){
|
|
var prop;
|
|
|
|
for (prop in supplier){
|
|
if (supplier.hasOwnProperty(prop)){
|
|
receiver[prop] = supplier[prop];
|
|
}
|
|
}
|
|
|
|
return prop;
|
|
},
|
|
|
|
/*
|
|
* Polyfill for array indexOf() method.
|
|
* @param {Array} values The array to search.
|
|
* @param {Variant} value The value to search for.
|
|
* @return {int} The index of the value if found, -1 if not.
|
|
*/
|
|
indexOf: function(values, value){
|
|
if (values.indexOf){
|
|
return values.indexOf(value);
|
|
} else {
|
|
for (var i=0, len=values.length; i < len; i++){
|
|
if (values[i] === value){
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Polyfill for array forEach() method.
|
|
* @param {Array} values The array to operate on.
|
|
* @param {Function} func The function to call on each item.
|
|
* @return {void}
|
|
*/
|
|
forEach: function(values, func) {
|
|
if (values.forEach){
|
|
return values.forEach(func);
|
|
} else {
|
|
for (var i=0, len=values.length; i < len; i++){
|
|
func(values[i], i, values);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
/*global CSSLint*/
|
|
/*
|
|
* Rule: Don't use adjoining classes (.foo.bar).
|
|
*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "adjoining-classes",
|
|
name: "Disallow adjoining classes",
|
|
desc: "Don't use adjoining classes.",
|
|
browsers: "IE6",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
modifier,
|
|
classCount,
|
|
i, j, k;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
for (j=0; j < selector.parts.length; j++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
classCount = 0;
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (modifier.type == "class"){
|
|
classCount++;
|
|
}
|
|
if (classCount > 1){
|
|
reporter.report("Don't use adjoining classes.", part.line, part.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*global CSSLint*/
|
|
|
|
/*
|
|
* Rule: Don't use width or height when using padding or border.
|
|
*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "box-model",
|
|
name: "Beware of broken box size",
|
|
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,
|
|
padding: 1,
|
|
"padding-left": 1,
|
|
"padding-right": 1
|
|
},
|
|
heightProperties = {
|
|
border: 1,
|
|
"border-bottom": 1,
|
|
"border-top": 1,
|
|
padding: 1,
|
|
"padding-bottom": 1,
|
|
"padding-top": 1
|
|
},
|
|
properties;
|
|
|
|
function startRule(){
|
|
properties = {};
|
|
}
|
|
|
|
function endRule(){
|
|
var prop;
|
|
if (properties.height){
|
|
for (prop in heightProperties){
|
|
if (heightProperties.hasOwnProperty(prop) && properties[prop]){
|
|
|
|
//special case for padding
|
|
if (!(prop == "padding" && properties[prop].value.parts.length === 2 && properties[prop].value.parts[0].value === 0)){
|
|
reporter.report("Using height with " + prop + " can sometimes make elements larger than you expect.", properties[prop].line, properties[prop].col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (properties.width){
|
|
for (prop in widthProperties){
|
|
if (widthProperties.hasOwnProperty(prop) && properties[prop]){
|
|
|
|
if (!(prop == "padding" && properties[prop].value.parts.length === 2 && properties[prop].value.parts[1].value === 0)){
|
|
reporter.report("Using width with " + prop + " can sometimes make elements larger than you expect.", properties[prop].line, properties[prop].col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase();
|
|
|
|
if (heightProperties[name] || widthProperties[name]){
|
|
if (!/^0\S*$/.test(event.value) && !(name == "border" && event.value == "none")){
|
|
properties[name] = { line: event.property.line, col: event.property.col, value: event.value };
|
|
}
|
|
} else {
|
|
if (name == "width" || name == "height"){
|
|
properties[name] = 1;
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
parser.addListener("endpage", endRule);
|
|
parser.addListener("endpagemargin", endRule);
|
|
parser.addListener("endkeyframerule", endRule);
|
|
}
|
|
|
|
});
|
|
/*global CSSLint*/
|
|
|
|
/*
|
|
* Rule: box-sizing doesn't work in IE6 and IE7.
|
|
*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "box-sizing",
|
|
name: "Disallow use of box-sizing",
|
|
desc: "The box-sizing properties isn't supported in IE6 and IE7.",
|
|
browsers: "IE6, IE7",
|
|
tags: ["Compatibility"],
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase();
|
|
|
|
if (name == "box-sizing"){
|
|
reporter.report("The box-sizing property isn't supported in IE6 and IE7.", event.line, event.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: "Require 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 ms",
|
|
"animation-delay" : "webkit moz ms",
|
|
"animation-direction" : "webkit moz ms",
|
|
"animation-duration" : "webkit moz ms",
|
|
"animation-fill-mode" : "webkit moz ms",
|
|
"animation-iteration-count" : "webkit moz ms",
|
|
"animation-name" : "webkit moz ms",
|
|
"animation-play-state" : "webkit moz ms",
|
|
"animation-timing-function" : "webkit moz ms",
|
|
"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 ms",
|
|
"column-gap" : "webkit moz ms",
|
|
"column-rule" : "webkit moz ms",
|
|
"column-rule-color" : "webkit moz ms",
|
|
"column-rule-style" : "webkit moz ms",
|
|
"column-rule-width" : "webkit moz ms",
|
|
"column-width" : "webkit moz ms",
|
|
"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 ms",
|
|
"transition-delay" : "webkit moz o ms",
|
|
"transition-duration" : "webkit moz o ms",
|
|
"transition-property" : "webkit moz o ms",
|
|
"transition-timing-function" : "webkit moz o ms",
|
|
"user-modify" : "webkit moz",
|
|
"user-select" : "webkit moz ms",
|
|
"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;
|
|
if (CSSLint.Util.indexOf(applyTo, name.text) > -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 (CSSLint.Util.indexOf(variations, name.text) > -1) {
|
|
if (!propertyGroups[prop]) {
|
|
propertyGroups[prop] = {
|
|
full : variations.slice(0),
|
|
actual : [],
|
|
actualNodes: []
|
|
};
|
|
}
|
|
if (CSSLint.Util.indexOf(propertyGroups[prop].actual, name.text) === -1) {
|
|
propertyGroups[prop].actual.push(name.text);
|
|
propertyGroups[prop].actualNodes.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 (CSSLint.Util.indexOf(actual, item) === -1) {
|
|
propertiesSpecified = (actual.length === 1) ? actual[0] : (actual.length == 2) ? actual.join(" and ") : actual.join(", ");
|
|
reporter.report("The property " + item + " is compatible with " + propertiesSpecified + " and should be included as well.", value.actualNodes[0].line, value.actualNodes[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-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-*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "display-property-grouping",
|
|
name: "Require properties appropriate for display",
|
|
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",
|
|
height: 1,
|
|
width: 1,
|
|
margin: 1,
|
|
"margin-left": 1,
|
|
"margin-right": 1,
|
|
"margin-bottom": 1,
|
|
"margin-top": 1,
|
|
padding: 1,
|
|
"padding-left": 1,
|
|
"padding-right": 1,
|
|
"padding-bottom": 1,
|
|
"padding-top": 1,
|
|
"vertical-align": 1
|
|
},
|
|
properties;
|
|
|
|
function reportProperty(name, display, msg){
|
|
if (properties[name]){
|
|
if (typeof propertiesToCheck[name] != "string" || properties[name].value.toLowerCase() != propertiesToCheck[name]){
|
|
reporter.report(msg || name + " can't be used with display: " + display + ".", properties[name].line, properties[name].col, rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
function startRule(){
|
|
properties = {};
|
|
}
|
|
|
|
function endRule(){
|
|
|
|
var display = properties.display ? properties.display.value : null;
|
|
if (display){
|
|
switch(display){
|
|
|
|
case "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-top", 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){
|
|
reportProperty("margin", display);
|
|
reportProperty("margin-left", display);
|
|
reportProperty("margin-right", display);
|
|
reportProperty("margin-top", display);
|
|
reportProperty("margin-bottom", display);
|
|
reportProperty("float", display);
|
|
}
|
|
|
|
//otherwise do nothing
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase();
|
|
|
|
if (propertiesToCheck[name]){
|
|
properties[name] = { value: event.value.text, line: event.property.line, col: event.property.col };
|
|
}
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
parser.addListener("endkeyframerule", endRule);
|
|
parser.addListener("endpagemargin", endRule);
|
|
parser.addListener("endpage", endRule);
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Disallow duplicate background-images (using url).
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "duplicate-background-images",
|
|
name: "Disallow duplicate background images",
|
|
desc: "Every background-image should be unique. Use a common class for e.g. sprites.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
stack = {};
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text,
|
|
value = event.value,
|
|
i, len;
|
|
|
|
if (name.match(/background/i)) {
|
|
for (i=0, len=value.parts.length; i < len; i++) {
|
|
if (value.parts[i].type == 'uri') {
|
|
if (typeof stack[value.parts[i].uri] === 'undefined') {
|
|
stack[value.parts[i].uri] = event;
|
|
}
|
|
else {
|
|
reporter.report("Background image '" + value.parts[i].uri + "' was used multiple times, first declared at line " + stack[value.parts[i].uri].line + ", col " + stack[value.parts[i].uri].col + ".", event.line, event.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.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "duplicate-properties",
|
|
name: "Disallow 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("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", 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.report("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.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "empty-rules",
|
|
name: "Disallow 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(){
|
|
count=0;
|
|
});
|
|
|
|
parser.addListener("property", function(){
|
|
count++;
|
|
});
|
|
|
|
parser.addListener("endrule", function(event){
|
|
var selectors = event.selectors;
|
|
if (count === 0){
|
|
reporter.report("Rule is empty.", selectors[0].line, selectors[0].col, rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: There should be no syntax errors. (Duh.)
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "errors",
|
|
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);
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "fallback-colors",
|
|
name: "Require fallback colors",
|
|
desc: "For older browsers that don't support RGBA, HSL, or HSLA, provide a fallback color.",
|
|
browsers: "IE6,IE7,IE8",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
lastProperty,
|
|
propertiesToCheck = {
|
|
color: 1,
|
|
background: 1,
|
|
"background-color": 1
|
|
},
|
|
properties;
|
|
|
|
function startRule(event){
|
|
properties = {};
|
|
lastProperty = null;
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var property = event.property,
|
|
name = property.text.toLowerCase(),
|
|
parts = event.value.parts,
|
|
i = 0,
|
|
colorType = "",
|
|
len = parts.length;
|
|
|
|
if(propertiesToCheck[name]){
|
|
while(i < len){
|
|
if (parts[i].type == "color"){
|
|
if ("alpha" in parts[i] || "hue" in parts[i]){
|
|
|
|
if (/([^\)]+)\(/.test(parts[i])){
|
|
colorType = RegExp.$1.toUpperCase();
|
|
}
|
|
|
|
if (!lastProperty || (lastProperty.property.text.toLowerCase() != name || lastProperty.colorType != "compat")){
|
|
reporter.report("Fallback " + name + " (hex or RGB) should precede " + colorType + " " + name + ".", event.line, event.col, rule);
|
|
}
|
|
} else {
|
|
event.colorType = "compat";
|
|
}
|
|
}
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
lastProperty = event;
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: You shouldn't use more than 10 floats. If you do, there's probably
|
|
* room for some abstraction.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "floats",
|
|
name: "Disallow too many 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" &&
|
|
event.value.text.toLowerCase() != "none"){
|
|
count++;
|
|
}
|
|
});
|
|
|
|
//report the results
|
|
parser.addListener("endstylesheet", function(){
|
|
reporter.stat("floats", count);
|
|
if (count >= 10){
|
|
reporter.rollupWarn("Too many floats (" + count + "), you're probably using them for layout. Consider using a grid system instead.", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Avoid too many @font-face declarations in the same stylesheet.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "font-faces",
|
|
name: "Don't use too many web fonts",
|
|
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(){
|
|
count++;
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function(){
|
|
if (count > 5){
|
|
reporter.rollupWarn("Too many @font-face declarations (" + count + ").", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: You shouldn't need more than 9 font-size declarations.
|
|
*/
|
|
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "font-sizes",
|
|
name: "Disallow too many font sizes",
|
|
desc: "Checks the number of font-size declarations.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
count = 0;
|
|
|
|
//check for use of "font-size"
|
|
parser.addListener("property", function(event){
|
|
if (event.property == "font-size"){
|
|
count++;
|
|
}
|
|
});
|
|
|
|
//report the results
|
|
parser.addListener("endstylesheet", function(){
|
|
reporter.stat("font-sizes", count);
|
|
if (count >= 10){
|
|
reporter.rollupWarn("Too many font-size declarations (" + count + "), abstraction needed.", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: When using a vendor-prefixed gradient, make sure to use them all.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "gradients",
|
|
name: "Require all gradient definitions",
|
|
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(){
|
|
gradients = {
|
|
moz: 0,
|
|
webkit: 0,
|
|
oldWebkit: 0,
|
|
ms: 0,
|
|
o: 0
|
|
};
|
|
});
|
|
|
|
parser.addListener("property", function(event){
|
|
|
|
if (/\-(moz|ms|o|webkit)(?:\-(?:linear|radial))\-gradient/i.test(event.value)){
|
|
gradients[RegExp.$1] = 1;
|
|
} else if (/\-webkit\-gradient/i.test(event.value)){
|
|
gradients.oldWebkit = 1;
|
|
}
|
|
|
|
});
|
|
|
|
parser.addListener("endrule", function(event){
|
|
var missing = [];
|
|
|
|
if (!gradients.moz){
|
|
missing.push("Firefox 3.6+");
|
|
}
|
|
|
|
if (!gradients.webkit){
|
|
missing.push("Webkit (Safari 5+, Chrome)");
|
|
}
|
|
|
|
if (!gradients.oldWebkit){
|
|
missing.push("Old Webkit (Safari 4+, Chrome)");
|
|
}
|
|
|
|
if (!gradients.ms){
|
|
missing.push("Internet Explorer 10+");
|
|
}
|
|
|
|
if (!gradients.o){
|
|
missing.push("Opera 11.1+");
|
|
}
|
|
|
|
if (missing.length && missing.length < 5){
|
|
reporter.report("Missing vendor-prefixed CSS gradients for " + missing.join(", ") + ".", event.selectors[0].line, event.selectors[0].col, rule);
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use IDs for selectors.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "ids",
|
|
name: "Disallow IDs in selectors",
|
|
desc: "Selectors should not contain IDs.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
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++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (modifier.type == "id"){
|
|
idCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (idCount == 1){
|
|
reporter.report("Don't use IDs in selectors.", selector.line, selector.col, rule);
|
|
} else if (idCount > 1){
|
|
reporter.report(idCount + " IDs in the selector, really?", selector.line, selector.col, rule);
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use @import, use <link> instead.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "import",
|
|
name: "Disallow @import",
|
|
desc: "Don't use @import, use <link> instead.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("import", function(event){
|
|
reporter.report("@import prevents parallel downloads, use <link> instead.", event.line, event.col, rule);
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Make sure !important is not overused, this could lead to specificity
|
|
* war. Display a warning on !important declarations, an error if it's
|
|
* used more at least 10 times.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "important",
|
|
name: "Disallow !important",
|
|
desc: "Be careful when using !important declaration",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
count = 0;
|
|
|
|
//warn that important is used and increment the declaration counter
|
|
parser.addListener("property", function(event){
|
|
if (event.important === true){
|
|
count++;
|
|
reporter.report("Use of !important", event.line, event.col, rule);
|
|
}
|
|
});
|
|
|
|
//if there are more than 10, show an error
|
|
parser.addListener("endstylesheet", function(){
|
|
reporter.stat("important", count);
|
|
if (count >= 10){
|
|
reporter.rollupWarn("Too many !important declarations (" + count + "), try to use less than 10 to avoid specificity issues.", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Properties should be known (listed in CSS3 specification) or
|
|
* be a vendor-prefixed property.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "known-properties",
|
|
name: "Require use of known properties",
|
|
desc: "Properties should be known (listed in CSS specification) or be a vendor-prefixed property.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
properties = {
|
|
|
|
"alignment-adjust": 1,
|
|
"alignment-baseline": 1,
|
|
"animation": 1,
|
|
"animation-delay": 1,
|
|
"animation-direction": 1,
|
|
"animation-duration": 1,
|
|
"animation-fill-mode": 1,
|
|
"animation-iteration-count": 1,
|
|
"animation-name": 1,
|
|
"animation-play-state": 1,
|
|
"animation-timing-function": 1,
|
|
"appearance": 1,
|
|
"azimuth": 1,
|
|
"backface-visibility": 1,
|
|
"background": 1,
|
|
"background-attachment": 1,
|
|
"background-break": 1,
|
|
"background-clip": 1,
|
|
"background-color": 1,
|
|
"background-image": 1,
|
|
"background-origin": 1,
|
|
"background-position": 1,
|
|
"background-repeat": 1,
|
|
"background-size": 1,
|
|
"baseline-shift": 1,
|
|
"binding": 1,
|
|
"bleed": 1,
|
|
"bookmark-label": 1,
|
|
"bookmark-level": 1,
|
|
"bookmark-state": 1,
|
|
"bookmark-target": 1,
|
|
"border": 1,
|
|
"border-bottom": 1,
|
|
"border-bottom-color": 1,
|
|
"border-bottom-left-radius": 1,
|
|
"border-bottom-right-radius": 1,
|
|
"border-bottom-style": 1,
|
|
"border-bottom-width": 1,
|
|
"border-collapse": 1,
|
|
"border-color": 1,
|
|
"border-image": 1,
|
|
"border-image-outset": 1,
|
|
"border-image-repeat": 1,
|
|
"border-image-slice": 1,
|
|
"border-image-source": 1,
|
|
"border-image-width": 1,
|
|
"border-left": 1,
|
|
"border-left-color": 1,
|
|
"border-left-style": 1,
|
|
"border-left-width": 1,
|
|
"border-radius": 1,
|
|
"border-right": 1,
|
|
"border-right-color": 1,
|
|
"border-right-style": 1,
|
|
"border-right-width": 1,
|
|
"border-spacing": 1,
|
|
"border-style": 1,
|
|
"border-top": 1,
|
|
"border-top-color": 1,
|
|
"border-top-left-radius": 1,
|
|
"border-top-right-radius": 1,
|
|
"border-top-style": 1,
|
|
"border-top-width": 1,
|
|
"border-width": 1,
|
|
"bottom": 1,
|
|
"box-align": 1,
|
|
"box-decoration-break": 1,
|
|
"box-direction": 1,
|
|
"box-flex": 1,
|
|
"box-flex-group": 1,
|
|
"box-lines": 1,
|
|
"box-ordinal-group": 1,
|
|
"box-orient": 1,
|
|
"box-pack": 1,
|
|
"box-shadow": 1,
|
|
"box-sizing": 1,
|
|
"break-after": 1,
|
|
"break-before": 1,
|
|
"break-inside": 1,
|
|
"caption-side": 1,
|
|
"clear": 1,
|
|
"clip": 1,
|
|
"color": 1,
|
|
"color-profile": 1,
|
|
"column-count": 1,
|
|
"column-fill": 1,
|
|
"column-gap": 1,
|
|
"column-rule": 1,
|
|
"column-rule-color": 1,
|
|
"column-rule-style": 1,
|
|
"column-rule-width": 1,
|
|
"column-span": 1,
|
|
"column-width": 1,
|
|
"columns": 1,
|
|
"content": 1,
|
|
"counter-increment": 1,
|
|
"counter-reset": 1,
|
|
"crop": 1,
|
|
"cue": 1,
|
|
"cue-after": 1,
|
|
"cue-before": 1,
|
|
"cursor": 1,
|
|
"direction": 1,
|
|
"display": 1,
|
|
"dominant-baseline": 1,
|
|
"drop-initial-after-adjust": 1,
|
|
"drop-initial-after-align": 1,
|
|
"drop-initial-before-adjust": 1,
|
|
"drop-initial-before-align": 1,
|
|
"drop-initial-size": 1,
|
|
"drop-initial-value": 1,
|
|
"elevation": 1,
|
|
"empty-cells": 1,
|
|
"fit": 1,
|
|
"fit-position": 1,
|
|
"float": 1,
|
|
"float-offset": 1,
|
|
"font": 1,
|
|
"font-family": 1,
|
|
"font-size": 1,
|
|
"font-size-adjust": 1,
|
|
"font-stretch": 1,
|
|
"font-style": 1,
|
|
"font-variant": 1,
|
|
"font-weight": 1,
|
|
"grid-columns": 1,
|
|
"grid-rows": 1,
|
|
"hanging-punctuation": 1,
|
|
"height": 1,
|
|
"hyphenate-after": 1,
|
|
"hyphenate-before": 1,
|
|
"hyphenate-character": 1,
|
|
"hyphenate-lines": 1,
|
|
"hyphenate-resource": 1,
|
|
"hyphens": 1,
|
|
"icon": 1,
|
|
"image-orientation": 1,
|
|
"image-rendering": 1,
|
|
"image-resolution": 1,
|
|
"inline-box-align": 1,
|
|
"left": 1,
|
|
"letter-spacing": 1,
|
|
"line-height": 1,
|
|
"line-stacking": 1,
|
|
"line-stacking-ruby": 1,
|
|
"line-stacking-shift": 1,
|
|
"line-stacking-strategy": 1,
|
|
"list-style": 1,
|
|
"list-style-image": 1,
|
|
"list-style-position": 1,
|
|
"list-style-type": 1,
|
|
"margin": 1,
|
|
"margin-bottom": 1,
|
|
"margin-left": 1,
|
|
"margin-right": 1,
|
|
"margin-top": 1,
|
|
"mark": 1,
|
|
"mark-after": 1,
|
|
"mark-before": 1,
|
|
"marks": 1,
|
|
"marquee-direction": 1,
|
|
"marquee-play-count": 1,
|
|
"marquee-speed": 1,
|
|
"marquee-style": 1,
|
|
"max-height": 1,
|
|
"max-width": 1,
|
|
"min-height": 1,
|
|
"min-width": 1,
|
|
"move-to": 1,
|
|
"nav-down": 1,
|
|
"nav-index": 1,
|
|
"nav-left": 1,
|
|
"nav-right": 1,
|
|
"nav-up": 1,
|
|
"opacity": 1,
|
|
"orphans": 1,
|
|
"outline": 1,
|
|
"outline-color": 1,
|
|
"outline-offset": 1,
|
|
"outline-style": 1,
|
|
"outline-width": 1,
|
|
"overflow": 1,
|
|
"overflow-style": 1,
|
|
"overflow-x": 1,
|
|
"overflow-y": 1,
|
|
"padding": 1,
|
|
"padding-bottom": 1,
|
|
"padding-left": 1,
|
|
"padding-right": 1,
|
|
"padding-top": 1,
|
|
"page": 1,
|
|
"page-break-after": 1,
|
|
"page-break-before": 1,
|
|
"page-break-inside": 1,
|
|
"page-policy": 1,
|
|
"pause": 1,
|
|
"pause-after": 1,
|
|
"pause-before": 1,
|
|
"perspective": 1,
|
|
"perspective-origin": 1,
|
|
"phonemes": 1,
|
|
"pitch": 1,
|
|
"pitch-range": 1,
|
|
"play-during": 1,
|
|
"position": 1,
|
|
"presentation-level": 1,
|
|
"punctuation-trim": 1,
|
|
"quotes": 1,
|
|
"rendering-intent": 1,
|
|
"resize": 1,
|
|
"rest": 1,
|
|
"rest-after": 1,
|
|
"rest-before": 1,
|
|
"richness": 1,
|
|
"right": 1,
|
|
"rotation": 1,
|
|
"rotation-point": 1,
|
|
"ruby-align": 1,
|
|
"ruby-overhang": 1,
|
|
"ruby-position": 1,
|
|
"ruby-span": 1,
|
|
"size": 1,
|
|
"speak": 1,
|
|
"speak-header": 1,
|
|
"speak-numeral": 1,
|
|
"speak-punctuation": 1,
|
|
"speech-rate": 1,
|
|
"stress": 1,
|
|
"string-set": 1,
|
|
"table-layout": 1,
|
|
"target": 1,
|
|
"target-name": 1,
|
|
"target-new": 1,
|
|
"target-position": 1,
|
|
"text-align": 1,
|
|
"text-align-last": 1,
|
|
"text-decoration": 1,
|
|
"text-emphasis": 1,
|
|
"text-height": 1,
|
|
"text-indent": 1,
|
|
"text-justify": 1,
|
|
"text-outline": 1,
|
|
"text-shadow": 1,
|
|
"text-transform": 1,
|
|
"text-wrap": 1,
|
|
"top": 1,
|
|
"transform": 1,
|
|
"transform-origin": 1,
|
|
"transform-style": 1,
|
|
"transition": 1,
|
|
"transition-delay": 1,
|
|
"transition-duration": 1,
|
|
"transition-property": 1,
|
|
"transition-timing-function": 1,
|
|
"unicode-bidi": 1,
|
|
"user-modify": 1,
|
|
"user-select": 1,
|
|
"vertical-align": 1,
|
|
"visibility": 1,
|
|
"voice-balance": 1,
|
|
"voice-duration": 1,
|
|
"voice-family": 1,
|
|
"voice-pitch": 1,
|
|
"voice-pitch-range": 1,
|
|
"voice-rate": 1,
|
|
"voice-stress": 1,
|
|
"voice-volume": 1,
|
|
"volume": 1,
|
|
"white-space": 1,
|
|
"white-space-collapse": 1,
|
|
"widows": 1,
|
|
"width": 1,
|
|
"word-break": 1,
|
|
"word-spacing": 1,
|
|
"word-wrap": 1,
|
|
"z-index": 1,
|
|
|
|
//IE
|
|
"filter": 1,
|
|
"zoom": 1,
|
|
|
|
//@font-face
|
|
"src": 1
|
|
};
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase();
|
|
|
|
if (event.invalid) {
|
|
reporter.report(event.invalid.message, event.line, event.col, rule);
|
|
}
|
|
//if (!properties[name] && name.charAt(0) != "-"){
|
|
// reporter.error("Unknown property '" + event.property + "'.", event.line, event.col, rule);
|
|
//}
|
|
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: outline: none or outline: 0 should only be used in a :focus rule
|
|
* and only if there are other properties in the same rule.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "outline-none",
|
|
name: "Disallow outline: none",
|
|
desc: "Use of outline: none or outline: 0 should be limited to :focus rules.",
|
|
browsers: "All",
|
|
tags: ["Accessibility"],
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
lastRule;
|
|
|
|
function startRule(event){
|
|
if (event.selectors){
|
|
lastRule = {
|
|
line: event.line,
|
|
col: event.col,
|
|
selectors: event.selectors,
|
|
propCount: 0,
|
|
outline: false
|
|
};
|
|
} else {
|
|
lastRule = null;
|
|
}
|
|
}
|
|
|
|
function endRule(event){
|
|
if (lastRule){
|
|
if (lastRule.outline){
|
|
if (lastRule.selectors.toString().toLowerCase().indexOf(":focus") == -1){
|
|
reporter.report("Outlines should only be modified using :focus.", lastRule.line, lastRule.col, rule);
|
|
} else if (lastRule.propCount == 1) {
|
|
reporter.report("Outlines shouldn't be hidden unless other visual changes are made.", lastRule.line, lastRule.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase(),
|
|
value = event.value;
|
|
|
|
if (lastRule){
|
|
lastRule.propCount++;
|
|
if (name == "outline" && (value == "none" || value == "0")){
|
|
lastRule.outline = true;
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
parser.addListener("endpage", endRule);
|
|
parser.addListener("endpagemargin", endRule);
|
|
parser.addListener("endkeyframerule", endRule);
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use classes or IDs with elements (a.foo or a#foo).
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "overqualified-elements",
|
|
name: "Disallow 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,
|
|
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++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (part.elementName && modifier.type == "id"){
|
|
reporter.report("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.report("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);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Headings (h1-h6) should not be qualified (namespaced).
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "qualified-headings",
|
|
name: "Disallow 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,
|
|
i, j;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
|
|
for (j=0; j < selector.parts.length; j++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
if (part.elementName && /h[1-6]/.test(part.elementName.toString()) && j > 0){
|
|
reporter.report("Heading (" + part.elementName + ") should not be qualified.", part.line, part.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Selectors that look like regular expressions are slow and should be avoided.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "regex-selectors",
|
|
name: "Disallow selectors that look like regexs",
|
|
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++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (modifier.type == "attribute"){
|
|
if (/([\~\|\^\$\*]=)/.test(modifier)){
|
|
reporter.report("Attribute selectors with " + RegExp.$1 + " are slow!", modifier.line, modifier.col, rule);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Total number of rules should not exceed x.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "rules-count",
|
|
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(){
|
|
count++;
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function(){
|
|
reporter.stat("rule-count", count);
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Use shorthand properties where possible.
|
|
*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "shorthand",
|
|
name: "Require shorthand properties",
|
|
desc: "Use shorthand properties where possible.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
prop, i, len,
|
|
propertiesToCheck = {},
|
|
properties,
|
|
mapping = {
|
|
"margin": [
|
|
"margin-top",
|
|
"margin-bottom",
|
|
"margin-left",
|
|
"margin-right"
|
|
],
|
|
"padding": [
|
|
"padding-top",
|
|
"padding-bottom",
|
|
"padding-left",
|
|
"padding-right"
|
|
]
|
|
};
|
|
|
|
//initialize propertiesToCheck
|
|
for (prop in mapping){
|
|
if (mapping.hasOwnProperty(prop)){
|
|
for (i=0, len=mapping[prop].length; i < len; i++){
|
|
propertiesToCheck[mapping[prop][i]] = prop;
|
|
}
|
|
}
|
|
}
|
|
|
|
function startRule(event){
|
|
properties = {};
|
|
}
|
|
|
|
//event handler for end of rules
|
|
function endRule(event){
|
|
|
|
var prop, i, len, total;
|
|
|
|
//check which properties this rule has
|
|
for (prop in mapping){
|
|
if (mapping.hasOwnProperty(prop)){
|
|
total=0;
|
|
|
|
for (i=0, len=mapping[prop].length; i < len; i++){
|
|
total += properties[mapping[prop][i]] ? 1 : 0;
|
|
}
|
|
|
|
if (total == mapping[prop].length){
|
|
reporter.report("The properties " + mapping[prop].join(", ") + " can be replaced by " + prop + ".", event.line, event.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
|
|
//check for use of "font-size"
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.toString().toLowerCase(),
|
|
value = event.value.parts[0].value;
|
|
|
|
if (propertiesToCheck[name]){
|
|
properties[name] = 1;
|
|
}
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use properties with a star prefix.
|
|
*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "star-property-hack",
|
|
name: "Disallow properties with a star prefix",
|
|
desc: "Checks for the star property hack (targets IE6/7)",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
//check if property name starts with "*"
|
|
parser.addListener("property", function(event){
|
|
var property = event.property;
|
|
|
|
if (property.hack == "*") {
|
|
reporter.report("Property with star prefix found.", event.property.line, event.property.col, rule);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*
|
|
* Rule: Don't use text-indent for image replacement if you need to support rtl.
|
|
*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "text-indent",
|
|
name: "Disallow negative text-indent",
|
|
desc: "Checks for text indent less than -99px",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
textIndent,
|
|
direction;
|
|
|
|
|
|
function startRule(event){
|
|
textIndent = false;
|
|
direction = "inherit";
|
|
}
|
|
|
|
//event handler for end of rules
|
|
function endRule(event){
|
|
if (textIndent && direction != "ltr"){
|
|
reporter.report("Negative text-indent doesn't work well with RTL. If you use text-indent for image replacement explicitly set direction for that item to ltr.", textIndent.line, textIndent.col, rule);
|
|
}
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
|
|
//check for use of "font-size"
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.toString().toLowerCase(),
|
|
value = event.value;
|
|
|
|
if (name == "text-indent" && value.parts[0].value < -99){
|
|
textIndent = event.property;
|
|
} else if (name == "direction" && value == "ltr"){
|
|
direction = "ltr";
|
|
}
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use properties with a underscore prefix.
|
|
*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "underscore-property-hack",
|
|
name: "Disallow properties with an underscore prefix",
|
|
desc: "Checks for the underscore property hack (targets IE6)",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
//check if property name starts with "_"
|
|
parser.addListener("property", function(event){
|
|
var property = event.property;
|
|
|
|
if (property.hack == "_") {
|
|
reporter.report("Property with underscore prefix found.", event.property.line, event.property.col, rule);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*
|
|
* Rule: Headings (h1-h6) should be defined only once.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "unique-headings",
|
|
name: "Headings should only be defined once",
|
|
desc: "Headings should be defined only once.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
var headings = {
|
|
h1: 0,
|
|
h2: 0,
|
|
h3: 0,
|
|
h4: 0,
|
|
h5: 0,
|
|
h6: 0
|
|
};
|
|
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
pseudo,
|
|
i, j;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
part = selector.parts[selector.parts.length-1];
|
|
|
|
if (part.elementName && /(h[1-6])/i.test(part.elementName.toString())){
|
|
|
|
for (j=0; j < part.modifiers.length; j++){
|
|
if (part.modifiers[j].type == "pseudo"){
|
|
pseudo = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!pseudo){
|
|
headings[RegExp.$1]++;
|
|
if (headings[RegExp.$1] > 1) {
|
|
reporter.report("Heading (" + part.elementName + ") has already been defined.", part.line, part.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function(event){
|
|
var prop,
|
|
messages = [];
|
|
|
|
for (prop in headings){
|
|
if (headings.hasOwnProperty(prop)){
|
|
if (headings[prop] > 1){
|
|
messages.push(headings[prop] + " " + prop + "s");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (messages.length){
|
|
reporter.rollupWarn("You have " + messages.join(", ") + " defined in this stylesheet.", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use universal selector because it's slow.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "universal-selector",
|
|
name: "Disallow universal selector",
|
|
desc: "The universal selector (*) is known to be slow.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
modifier,
|
|
i, j, k;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
|
|
part = selector.parts[selector.parts.length-1];
|
|
if (part.elementName == "*"){
|
|
reporter.report(rule.desc, part.line, part.col, rule);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use unqualified attribute selectors because they're just like universal selectors.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "unqualified-attributes",
|
|
name: "Disallow unqualified attribute selectors",
|
|
desc: "Unqualified attribute selectors are known to be slow.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("startrule", function(event){
|
|
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
modifier,
|
|
i, j, k;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
|
|
part = selector.parts[selector.parts.length-1];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (modifier.type == "attribute" && (!part.elementName || part.elementName == "*")){
|
|
reporter.report(rule.desc, part.line, part.col, rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: When using a vendor-prefixed property, make sure to
|
|
* include the standard one.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "vendor-prefix",
|
|
name: "Require standard property with 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,
|
|
propertiesToCheck = {
|
|
"-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",
|
|
|
|
"-o-border-radius": "border-radius",
|
|
"-o-border-top-left-radius": "border-top-left-radius",
|
|
"-o-border-top-right-radius": "border-top-right-radius",
|
|
"-o-border-bottom-left-radius": "border-bottom-left-radius",
|
|
"-o-border-bottom-right-radius": "border-bottom-right-radius",
|
|
|
|
"-moz-border-radius": "border-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-column-count": "column-count",
|
|
"-webkit-column-count": "column-count",
|
|
|
|
"-moz-column-gap": "column-gap",
|
|
"-webkit-column-gap": "column-gap",
|
|
|
|
"-moz-column-rule": "column-rule",
|
|
"-webkit-column-rule": "column-rule",
|
|
|
|
"-moz-column-rule-style": "column-rule-style",
|
|
"-webkit-column-rule-style": "column-rule-style",
|
|
|
|
"-moz-column-rule-color": "column-rule-color",
|
|
"-webkit-column-rule-color": "column-rule-color",
|
|
|
|
"-moz-column-rule-width": "column-rule-width",
|
|
"-webkit-column-rule-width": "column-rule-width",
|
|
|
|
"-moz-column-width": "column-width",
|
|
"-webkit-column-width": "column-width",
|
|
|
|
"-webkit-column-span": "column-span",
|
|
"-webkit-columns": "columns",
|
|
|
|
"-moz-box-shadow": "box-shadow",
|
|
"-webkit-box-shadow": "box-shadow",
|
|
|
|
"-moz-transform" : "transform",
|
|
"-webkit-transform" : "transform",
|
|
"-o-transform" : "transform",
|
|
"-ms-transform" : "transform",
|
|
|
|
"-moz-transform-origin" : "transform-origin",
|
|
"-webkit-transform-origin" : "transform-origin",
|
|
"-o-transform-origin" : "transform-origin",
|
|
"-ms-transform-origin" : "transform-origin",
|
|
|
|
"-moz-box-sizing" : "box-sizing",
|
|
"-webkit-box-sizing" : "box-sizing",
|
|
|
|
"-moz-user-select" : "user-select",
|
|
"-khtml-user-select" : "user-select",
|
|
"-webkit-user-select" : "user-select"
|
|
};
|
|
|
|
//event handler for beginning of rules
|
|
function startRule(){
|
|
properties = {};
|
|
num=1;
|
|
}
|
|
|
|
//event handler for end of rules
|
|
function endRule(event){
|
|
var prop,
|
|
i, len,
|
|
standard,
|
|
needed,
|
|
actual,
|
|
needsStandard = [];
|
|
|
|
for (prop in properties){
|
|
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;
|
|
|
|
if (!properties[needed]){
|
|
reporter.report("Missing standard property '" + needed + "' to go along with '" + actual + "'.", properties[actual][0].name.line, properties[actual][0].name.col, rule);
|
|
} else {
|
|
//make sure standard property is last
|
|
if (properties[needed][0].pos < properties[actual][0].pos){
|
|
reporter.report("Standard property '" + needed + "' should come after vendor-prefixed property '" + actual + "'.", properties[actual][0].name.line, properties[actual][0].name.col, rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", 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);
|
|
parser.addListener("endpage", endRule);
|
|
parser.addListener("endpagemargin", endRule);
|
|
parser.addListener("endkeyframerule", endRule);
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: You don't need to specify units when a value is 0.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "zero-units",
|
|
name: "Disallow units for 0 values",
|
|
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;
|
|
|
|
while(i < len){
|
|
if ((parts[i].units || parts[i].type == "percentage") && parts[i].value === 0 && parts[i].type != "time"){
|
|
reporter.report("Values of 0 shouldn't have units specified.", parts[i].line, parts[i].col, rule);
|
|
}
|
|
i++;
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
/*global CSSLint*/
|
|
(function() {
|
|
|
|
/**
|
|
* Replace special characters before write to output.
|
|
*
|
|
* Rules:
|
|
* - single quotes is the escape sequence for double-quotes
|
|
* - & is the escape sequence for &
|
|
* - < is the escape sequence for <
|
|
* - > is the escape sequence for >
|
|
*
|
|
* @param {String} message to escape
|
|
* @return escaped message as {String}
|
|
*/
|
|
var xmlEscape = function(str) {
|
|
if (!str || str.constructor !== String) {
|
|
return "";
|
|
}
|
|
|
|
return str.replace(/[\"&><]/g, function(match) {
|
|
switch (match) {
|
|
case "\"":
|
|
return """;
|
|
case "&":
|
|
return "&";
|
|
case "<":
|
|
return "<";
|
|
case ">":
|
|
return ">";
|
|
}
|
|
});
|
|
};
|
|
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "checkstyle-xml",
|
|
name: "Checkstyle XML format",
|
|
|
|
/**
|
|
* Return opening root XML tag.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function(){
|
|
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><checkstyle>";
|
|
},
|
|
|
|
/**
|
|
* Return closing root XML tag.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function(){
|
|
return "</checkstyle>";
|
|
},
|
|
|
|
/**
|
|
* Returns message when there is a file read error.
|
|
* @param {String} filename The name of the file that caused the error.
|
|
* @param {String} message The error message
|
|
* @return {String} The error message.
|
|
*/
|
|
readError: function(filename, message) {
|
|
return "<file name=\"" + xmlEscape(filename) + "\"><error line=\"0\" column=\"0\" severty=\"error\" message=\"" + xmlEscape(message) + "\"></error></file>";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (UNUSED for now) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = [];
|
|
|
|
/**
|
|
* Generate a source string for a rule.
|
|
* Checkstyle source strings usually resemble Java class names e.g
|
|
* net.csslint.SomeRuleName
|
|
* @param {Object} rule
|
|
* @return rule source as {String}
|
|
*/
|
|
var generateSource = function(rule) {
|
|
if (!rule || !('name' in rule)) {
|
|
return "";
|
|
}
|
|
return 'net.csslint.' + rule.name.replace(/\s/g,'');
|
|
};
|
|
|
|
|
|
|
|
if (messages.length > 0) {
|
|
output.push("<file name=\""+filename+"\">");
|
|
CSSLint.Util.forEach(messages, function (message, i) {
|
|
//ignore rollups for now
|
|
if (!message.rollup) {
|
|
output.push("<error line=\"" + message.line + "\" column=\"" + message.col + "\" severity=\"" + message.type + "\"" +
|
|
" message=\"" + xmlEscape(message.message) + "\" source=\"" + generateSource(message.rule) +"\"/>");
|
|
}
|
|
});
|
|
output.push("</file>");
|
|
}
|
|
|
|
return output.join("");
|
|
}
|
|
});
|
|
|
|
}());
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "compact",
|
|
name: "Compact, 'porcelain' format",
|
|
|
|
/**
|
|
* Return content to be printed before all file results.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function() {
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Return content to be printed after all file results.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function() {
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (Optional) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = "";
|
|
options = options || {};
|
|
|
|
/**
|
|
* Capitalize and return given string.
|
|
* @param str {String} to capitalize
|
|
* @return {String} capitalized
|
|
*/
|
|
var capitalize = function(str) {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
};
|
|
|
|
if (messages.length === 0) {
|
|
return options.quiet ? "" : filename + ": Lint Free!";
|
|
}
|
|
|
|
CSSLint.Util.forEach(messages, function(message, i) {
|
|
if (message.rollup) {
|
|
output += filename + ": " + capitalize(message.type) + " - " + message.message + "\n";
|
|
} else {
|
|
output += filename + ": " + "line " + message.line +
|
|
", col " + message.col + ", " + capitalize(message.type) + " - " + message.message + "\n";
|
|
}
|
|
});
|
|
|
|
return output;
|
|
}
|
|
});
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "csslint-xml",
|
|
name: "CSSLint XML format",
|
|
|
|
/**
|
|
* Return opening root XML tag.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function(){
|
|
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><csslint>";
|
|
},
|
|
|
|
/**
|
|
* Return closing root XML tag.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function(){
|
|
return "</csslint>";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (UNUSED for now) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = [];
|
|
|
|
/**
|
|
* Replace special characters before write to output.
|
|
*
|
|
* Rules:
|
|
* - single quotes is the escape sequence for double-quotes
|
|
* - & is the escape sequence for &
|
|
* - < is the escape sequence for <
|
|
* - > is the escape sequence for >
|
|
*
|
|
* @param {String} message to escape
|
|
* @return escaped message as {String}
|
|
*/
|
|
var escapeSpecialCharacters = function(str) {
|
|
if (!str || str.constructor !== String) {
|
|
return "";
|
|
}
|
|
return str.replace(/\"/g, "'").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
};
|
|
|
|
if (messages.length > 0) {
|
|
output.push("<file name=\""+filename+"\">");
|
|
CSSLint.Util.forEach(messages, function (message, i) {
|
|
if (message.rollup) {
|
|
output.push("<issue severity=\"" + message.type + "\" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
|
|
} else {
|
|
output.push("<issue line=\"" + message.line + "\" char=\"" + message.col + "\" severity=\"" + message.type + "\"" +
|
|
" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
|
|
}
|
|
});
|
|
output.push("</file>");
|
|
}
|
|
|
|
return output.join("");
|
|
}
|
|
});
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "lint-xml",
|
|
name: "Lint XML format",
|
|
|
|
/**
|
|
* Return opening root XML tag.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function(){
|
|
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><lint>";
|
|
},
|
|
|
|
/**
|
|
* Return closing root XML tag.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function(){
|
|
return "</lint>";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (UNUSED for now) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = [];
|
|
|
|
/**
|
|
* Replace special characters before write to output.
|
|
*
|
|
* Rules:
|
|
* - single quotes is the escape sequence for double-quotes
|
|
* - & is the escape sequence for &
|
|
* - < is the escape sequence for <
|
|
* - > is the escape sequence for >
|
|
*
|
|
* @param {String} message to escape
|
|
* @return escaped message as {String}
|
|
*/
|
|
var escapeSpecialCharacters = function(str) {
|
|
if (!str || str.constructor !== String) {
|
|
return "";
|
|
}
|
|
return str.replace(/\"/g, "'").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
};
|
|
|
|
if (messages.length > 0) {
|
|
|
|
output.push("<file name=\""+filename+"\">");
|
|
CSSLint.Util.forEach(messages, function (message, i) {
|
|
if (message.rollup) {
|
|
output.push("<issue severity=\"" + message.type + "\" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
|
|
} else {
|
|
output.push("<issue line=\"" + message.line + "\" char=\"" + message.col + "\" severity=\"" + message.type + "\"" +
|
|
" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
|
|
}
|
|
});
|
|
output.push("</file>");
|
|
}
|
|
|
|
return output.join("");
|
|
}
|
|
});
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "text",
|
|
name: "Plain Text",
|
|
|
|
/**
|
|
* Return content to be printed before all file results.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function() {
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Return content to be printed after all file results.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function() {
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (Optional) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = "";
|
|
options = options || {};
|
|
|
|
if (messages.length === 0) {
|
|
return options.quiet ? "" : "\n\ncsslint: No errors in " + filename + ".";
|
|
}
|
|
|
|
output = "\n\ncsslint: There are " + messages.length + " problems in " + filename + ".";
|
|
var pos = filename.lastIndexOf("/"),
|
|
shortFilename = filename;
|
|
|
|
if (pos === -1){
|
|
pos = filename.lastIndexOf("\\");
|
|
}
|
|
if (pos > -1){
|
|
shortFilename = filename.substring(pos+1);
|
|
}
|
|
|
|
CSSLint.Util.forEach(messages, 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;
|
|
})();
|