3 Copyright (c) 2011 Nicole Sullivan and Nicholas C. Zakas. All rights reserved.
5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 of this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights
8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the Software is
10 furnished to do so, subject to the following conditions:
12 The above copyright notice and this permission notice shall be included in
13 all copies or substantial portions of the Software.
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 /* Build time: 14-May-2012 10:24:48 */
25 var CSSLint = (function(){
29 Copyright (c) 2009-2011 Nicholas C. Zakas. All rights reserved.
31 Permission is hereby granted, free of charge, to any person obtaining a copy
32 of this software and associated documentation files (the "Software"), to deal
33 in the Software without restriction, including without limitation the rights
34 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35 copies of the Software, and to permit persons to whom the Software is
36 furnished to do so, subject to the following conditions:
38 The above copyright notice and this permission notice shall be included in
39 all copies or substantial portions of the Software.
41 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
50 /* Version v0.1.7, Build time: 4-May-2012 03:57:04 */
56 * A generic base to inherit from for any object
57 * that needs event handling.
61 function EventTarget(){
64 * The array of listeners for various events.
66 * @property _listeners
72 EventTarget.prototype = {
75 constructor: EventTarget,
78 * Adds a listener for a given event type.
79 * @param {String} type The type of event to add a listener for.
80 * @param {Function} listener The function to call when the event occurs.
84 addListener: function(type, listener){
85 if (!this._listeners[type]){
86 this._listeners[type] = [];
89 this._listeners[type].push(listener);
93 * Fires an event based on the passed-in object.
94 * @param {Object|String} event An object with at least a 'type' attribute
95 * or a string indicating the event name.
99 fire: function(event){
100 if (typeof event == "string"){
101 event = { type: event };
103 if (typeof event.target != "undefined"){
107 if (typeof event.type == "undefined"){
108 throw new Error("Event object missing 'type' property.");
111 if (this._listeners[event.type]){
113 //create a copy of the array and use that so listeners can't chane
114 var listeners = this._listeners[event.type].concat();
115 for (var i=0, len=listeners.length; i < len; i++){
116 listeners[i].call(this, event);
122 * Removes a listener for a given event type.
123 * @param {String} type The type of event to remove a listener from.
124 * @param {Function} listener The function to remove from the event.
126 * @method removeListener
128 removeListener: function(type, listener){
129 if (this._listeners[type]){
130 var listeners = this._listeners[type];
131 for (var i=0, len=listeners.length; i < len; i++){
132 if (listeners[i] === listener){
133 listeners.splice(i, 1);
143 * Convenient way to read through strings.
144 * @namespace parserlib.util
145 * @class StringReader
147 * @param {String} text The text to read.
149 function StringReader(text){
152 * The input text with line endings normalized.
157 this._input = text.replace(/\n\r?/g, "\n");
161 * The row for the character to be read next.
170 * The column for the character to be read next.
178 * The index of the character in the input to be read next.
186 StringReader.prototype = {
188 //restore constructor
189 constructor: StringReader,
191 //-------------------------------------------------------------------------
193 //-------------------------------------------------------------------------
196 * Returns the column of the character to be read next.
197 * @return {int} The column of the character to be read next.
205 * Returns the row of the character to be read next.
206 * @return {int} The row of the character to be read next.
214 * Determines if you're at the end of the input.
215 * @return {Boolean} True if there's no more input, false otherwise.
219 return (this._cursor == this._input.length);
222 //-------------------------------------------------------------------------
224 //-------------------------------------------------------------------------
227 * Reads the next character without advancing the cursor.
228 * @param {int} count How many characters to look ahead (default is 1).
229 * @return {String} The next character or null if there is no next character.
232 peek: function(count){
234 count = (typeof count == "undefined" ? 1 : count);
236 //if we're not at the end of the input...
237 if (this._cursor < this._input.length){
239 //get character and increment cursor and column
240 c = this._input.charAt(this._cursor + count - 1);
247 * Reads the next character from the input and adjusts the row and column
249 * @return {String} The next character or null if there is no next character.
255 //if we're not at the end of the input...
256 if (this._cursor < this._input.length){
258 //if the last character was a newline, increment row count
259 //and reset column count
260 if (this._input.charAt(this._cursor) == "\n"){
267 //get character and increment cursor and column
268 c = this._input.charAt(this._cursor++);
274 //-------------------------------------------------------------------------
276 //-------------------------------------------------------------------------
279 * Saves the current location so it can be returned to later.
285 cursor: this._cursor,
293 this._cursor = this._bookmark.cursor;
294 this._line = this._bookmark.line;
295 this._col = this._bookmark.col;
296 delete this._bookmark;
300 //-------------------------------------------------------------------------
302 //-------------------------------------------------------------------------
305 * Reads up to and including the given string. Throws an error if that
306 * string is not found.
307 * @param {String} pattern The string to read.
308 * @return {String} The string when it is found.
309 * @throws Error when the string pattern is not found.
312 readTo: function(pattern){
318 * First, buffer must be the same length as the pattern.
319 * Then, buffer must end with the pattern or else reach the
322 while (buffer.length < pattern.length || buffer.lastIndexOf(pattern) != buffer.length - pattern.length){
327 throw new Error("Expected \"" + pattern + "\" at line " + this._line + ", col " + this._col + ".");
336 * Reads characters while each character causes the given
337 * filter function to return true. The function is passed
338 * in each character and either returns true to continue
339 * reading or false to stop.
340 * @param {Function} filter The function to read on each character.
341 * @return {String} The string made up of all characters that passed the
345 readWhile: function(filter){
350 while(c !== null && filter(c)){
360 * Reads characters that match either text or a regular expression and
361 * returns those characters. If a match is found, the row and column
362 * are adjusted; if no match is found, the reader's state is unchanged.
363 * reading or false to stop.
364 * @param {String|RegExp} matchter If a string, then the literal string
365 * value is searched for. If a regular expression, then any string
366 * matching the pattern is search for.
367 * @return {String} The string made up of all characters that matched or
368 * null if there was no match.
371 readMatch: function(matcher){
373 var source = this._input.substring(this._cursor),
376 //if it's a string, just do a straight match
377 if (typeof matcher == "string"){
378 if (source.indexOf(matcher) === 0){
379 value = this.readCount(matcher.length);
381 } else if (matcher instanceof RegExp){
382 if (matcher.test(source)){
383 value = this.readCount(RegExp.lastMatch.length);
392 * Reads a given number of characters. If the end of the input is reached,
393 * it reads only the remaining characters and does not throw an error.
394 * @param {int} count The number of characters to read.
395 * @return {String} The string made up the read characters.
398 readCount: function(count){
402 buffer += this.read();
410 * Type to use when a syntax error occurs.
412 * @namespace parserlib.util
414 * @param {String} message The error message.
415 * @param {int} line The line at which the error occurred.
416 * @param {int} col The column at which the error occurred.
418 function SyntaxError(message, line, col){
421 * The column at which the error occurred.
428 * The line at which the error occurred.
435 * The text representation of the unit.
439 this.message = message;
444 SyntaxError.prototype = new Error();
446 * Base type to represent a single syntactic unit.
448 * @namespace parserlib.util
450 * @param {String} text The text of the unit.
451 * @param {int} line The line of text on which the unit resides.
452 * @param {int} col The column of text on which the unit resides.
454 function SyntaxUnit(text, line, col, type){
458 * The column of text on which the unit resides.
465 * The line of text on which the unit resides.
472 * The text representation of the unit.
479 * The type of syntax unit.
487 * Create a new syntax unit based solely on the given token.
488 * Convenience method for creating a new syntax unit when
489 * it represents a single token instead of multiple.
490 * @param {Object} token The token object to represent.
491 * @return {parserlib.util.SyntaxUnit} The object representing the token.
495 SyntaxUnit.fromToken = function(token){
496 return new SyntaxUnit(token.value, token.startLine, token.startCol);
499 SyntaxUnit.prototype = {
501 //restore constructor
502 constructor: SyntaxUnit,
505 * Returns the text representation of the unit.
506 * @return {String} The text representation of the unit.
510 return this.toString();
514 * Returns the text representation of the unit.
515 * @return {String} The text representation of the unit.
518 toString: function(){
523 /*global StringReader, SyntaxError*/
526 * Generic TokenStream providing base functionality.
527 * @class TokenStreamBase
528 * @namespace parserlib.util
530 * @param {String|StringReader} input The text to tokenize or a reader from
531 * which to read the input.
533 function TokenStreamBase(input, tokenData){
536 * The string reader for easy access to the text.
541 this._reader = input ? new StringReader(input.toString()) : null;
544 * Token object for the last consumed token.
552 * The array of token information.
554 * @property _tokenData
557 this._tokenData = tokenData;
560 * Lookahead token buffer.
568 * Lookahead token buffer index.
575 this._ltIndexCache = [];
579 * Accepts an array of token information and outputs
580 * an array of token data containing key-value mappings
581 * and matching functions that the TokenStream needs.
582 * @param {Array} tokens An array of token descriptors.
583 * @return {Array} An array of processed token data.
584 * @method createTokenData
587 TokenStreamBase.createTokenData = function(tokens){
591 tokenData = tokens.concat([]),
593 len = tokenData.length+1;
595 tokenData.UNKNOWN = -1;
596 tokenData.unshift({name:"EOF"});
598 for (; i < len; i++){
599 nameMap.push(tokenData[i].name);
600 tokenData[tokenData[i].name] = i;
601 if (tokenData[i].text){
602 typeMap[tokenData[i].text] = i;
606 tokenData.name = function(tt){
610 tokenData.type = function(c){
617 TokenStreamBase.prototype = {
619 //restore constructor
620 constructor: TokenStreamBase,
622 //-------------------------------------------------------------------------
624 //-------------------------------------------------------------------------
627 * Determines if the next token matches the given token type.
628 * If so, that token is consumed; if not, the token is placed
629 * back onto the token stream. You can pass in any number of
630 * token types and this will return true if any of the token
632 * @param {int|int[]} tokenTypes Either a single token type or an array of
633 * token types that the next token might be. If an array is passed,
634 * it's assumed that the token can be any of these.
635 * @param {variant} channel (Optional) The channel to read from. If not
636 * provided, reads from the default (unnamed) channel.
637 * @return {Boolean} True if the token type matches, false if not.
640 match: function(tokenTypes, channel){
642 //always convert to an array, makes things easier
643 if (!(tokenTypes instanceof Array)){
644 tokenTypes = [tokenTypes];
647 var tt = this.get(channel),
649 len = tokenTypes.length;
652 if (tt == tokenTypes[i++]){
657 //no match found, put the token back
663 * Determines if the next token matches the given token type.
664 * If so, that token is consumed; if not, an error is thrown.
665 * @param {int|int[]} tokenTypes Either a single token type or an array of
666 * token types that the next token should be. If an array is passed,
667 * it's assumed that the token must be one of these.
668 * @param {variant} channel (Optional) The channel to read from. If not
669 * provided, reads from the default (unnamed) channel.
673 mustMatch: function(tokenTypes, channel){
677 //always convert to an array, makes things easier
678 if (!(tokenTypes instanceof Array)){
679 tokenTypes = [tokenTypes];
682 if (!this.match.apply(this, arguments)){
684 throw new SyntaxError("Expected " + this._tokenData[tokenTypes[0]].name +
685 " at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol);
689 //-------------------------------------------------------------------------
691 //-------------------------------------------------------------------------
694 * Keeps reading from the token stream until either one of the specified
695 * token types is found or until the end of the input is reached.
696 * @param {int|int[]} tokenTypes Either a single token type or an array of
697 * token types that the next token should be. If an array is passed,
698 * it's assumed that the token must be one of these.
699 * @param {variant} channel (Optional) The channel to read from. If not
700 * provided, reads from the default (unnamed) channel.
704 advance: function(tokenTypes, channel){
706 while(this.LA(0) !== 0 && !this.match(tokenTypes, channel)){
714 * Consumes the next token from the token stream.
715 * @return {int} The token type of the token that was just consumed.
718 get: function(channel){
720 var tokenInfo = this._tokenData,
721 reader = this._reader,
724 len = tokenInfo.length,
729 //check the lookahead buffer first
730 if (this._lt.length && this._ltIndex >= 0 && this._ltIndex < this._lt.length){
733 this._token = this._lt[this._ltIndex++];
734 info = tokenInfo[this._token.type];
736 //obey channels logic
737 while((info.channel !== undefined && channel !== info.channel) &&
738 this._ltIndex < this._lt.length){
739 this._token = this._lt[this._ltIndex++];
740 info = tokenInfo[this._token.type];
745 if ((info.channel === undefined || channel === info.channel) &&
746 this._ltIndex <= this._lt.length){
747 this._ltIndexCache.push(i);
748 return this._token.type;
752 //call token retriever method
753 token = this._getToken();
755 //if it should be hidden, don't save a token
756 if (token.type > -1 && !tokenInfo[token.type].hide){
758 //apply token channel
759 token.channel = tokenInfo[token.type].channel;
763 this._lt.push(token);
765 //save space that will be moved (must be done before array is truncated)
766 this._ltIndexCache.push(this._lt.length - this._ltIndex + i);
768 //keep the buffer under 5 items
769 if (this._lt.length > 5){
773 //also keep the shift buffer under 5 items
774 if (this._ltIndexCache.length > 5){
775 this._ltIndexCache.shift();
778 //update lookahead index
779 this._ltIndex = this._lt.length;
783 * Skip to the next token if:
784 * 1. The token type is marked as hidden.
785 * 2. The token type has a channel specified and it isn't the current channel.
787 info = tokenInfo[token.type];
790 (info.channel !== undefined && channel !== info.channel))){
791 return this.get(channel);
793 //return just the type
799 * Looks ahead a certain number of tokens and returns the token type at
800 * that position. This will throw an error if you lookahead past the
801 * end of input, past the size of the lookahead buffer, or back past
802 * the first token in the lookahead buffer.
803 * @param {int} The index of the token type to retrieve. 0 for the
804 * current token, 1 for the next, -1 for the previous, etc.
805 * @return {int} The token type of the token in the given position.
812 //TODO: Store 5 somewhere
814 throw new Error("Too much lookahead.");
817 //get all those tokens
823 //unget all those tokens
824 while(total < index){
828 } else if (index < 0){
830 if(this._lt[this._ltIndex+index]){
831 tt = this._lt[this._ltIndex+index].type;
833 throw new Error("Too much lookbehind.");
837 tt = this._token.type;
845 * Looks ahead a certain number of tokens and returns the token at
846 * that position. This will throw an error if you lookahead past the
847 * end of input, past the size of the lookahead buffer, or back past
848 * the first token in the lookahead buffer.
849 * @param {int} The index of the token type to retrieve. 0 for the
850 * current token, 1 for the next, -1 for the previous, etc.
851 * @return {Object} The token of the token in the given position.
856 //lookahead first to prime the token buffer
859 //now find the token, subtract one because _ltIndex is already at the next index
860 return this._lt[this._ltIndex+index-1];
864 * Returns the token type for the next token in the stream without
866 * @return {int} The token type of the next token in the stream.
874 * Returns the actual token object for the last consumed token.
875 * @return {Token} The token object for the last consumed token.
883 * Returns the name of the token for the given token type.
884 * @param {int} tokenType The type of token to get the name of.
885 * @return {String} The name of the token or "UNKNOWN_TOKEN" for any
886 * invalid token type.
889 tokenName: function(tokenType){
890 if (tokenType < 0 || tokenType > this._tokenData.length){
891 return "UNKNOWN_TOKEN";
893 return this._tokenData[tokenType].name;
898 * Returns the token type value for the given token name.
899 * @param {String} tokenName The name of the token whose value should be returned.
900 * @return {int} The token type value for the given token name or -1
901 * for an unknown token.
904 tokenType: function(tokenName){
905 return this._tokenData[tokenName] || -1;
909 * Returns the last consumed token to the token stream.
913 //if (this._ltIndex > -1){
914 if (this._ltIndexCache.length){
915 this._ltIndex -= this._ltIndexCache.pop();//--;
916 this._token = this._lt[this._ltIndex - 1];
918 throw new Error("Too much lookahead.");
928 StringReader: StringReader,
929 SyntaxError : SyntaxError,
930 SyntaxUnit : SyntaxUnit,
931 EventTarget : EventTarget,
932 TokenStreamBase : TokenStreamBase
939 Copyright (c) 2009-2011 Nicholas C. Zakas. All rights reserved.
941 Permission is hereby granted, free of charge, to any person obtaining a copy
942 of this software and associated documentation files (the "Software"), to deal
943 in the Software without restriction, including without limitation the rights
944 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
945 copies of the Software, and to permit persons to whom the Software is
946 furnished to do so, subject to the following conditions:
948 The above copyright notice and this permission notice shall be included in
949 all copies or substantial portions of the Software.
951 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
952 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
953 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
954 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
955 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
956 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
960 /* Version v0.1.7, Build time: 4-May-2012 03:57:04 */
962 var EventTarget = parserlib.util.EventTarget,
963 TokenStreamBase = parserlib.util.TokenStreamBase,
964 StringReader = parserlib.util.StringReader,
965 SyntaxError = parserlib.util.SyntaxError,
966 SyntaxUnit = parserlib.util.SyntaxUnit;
970 aliceblue :"#f0f8ff",
971 antiquewhite :"#faebd7",
973 aquamarine :"#7fffd4",
978 blanchedalmond :"#ffebcd",
980 blueviolet :"#8a2be2",
982 burlywood :"#deb887",
983 cadetblue :"#5f9ea0",
984 chartreuse :"#7fff00",
985 chocolate :"#d2691e",
987 cornflowerblue :"#6495ed",
993 darkgoldenrod :"#b8860b",
995 darkgreen :"#006400",
996 darkkhaki :"#bdb76b",
997 darkmagenta :"#8b008b",
998 darkolivegreen :"#556b2f",
999 darkorange :"#ff8c00",
1000 darkorchid :"#9932cc",
1002 darksalmon :"#e9967a",
1003 darkseagreen :"#8fbc8f",
1004 darkslateblue :"#483d8b",
1005 darkslategray :"#2f4f4f",
1006 darkturquoise :"#00ced1",
1007 darkviolet :"#9400d3",
1008 deeppink :"#ff1493",
1009 deepskyblue :"#00bfff",
1011 dodgerblue :"#1e90ff",
1012 firebrick :"#b22222",
1013 floralwhite :"#fffaf0",
1014 forestgreen :"#228b22",
1016 gainsboro :"#dcdcdc",
1017 ghostwhite :"#f8f8ff",
1019 goldenrod :"#daa520",
1022 greenyellow :"#adff2f",
1023 honeydew :"#f0fff0",
1025 indianred :"#cd5c5c",
1029 lavender :"#e6e6fa",
1030 lavenderblush :"#fff0f5",
1031 lawngreen :"#7cfc00",
1032 lemonchiffon :"#fffacd",
1033 lightblue :"#add8e6",
1034 lightcoral :"#f08080",
1035 lightcyan :"#e0ffff",
1036 lightgoldenrodyellow :"#fafad2",
1037 lightgray :"#d3d3d3",
1038 lightgreen :"#90ee90",
1039 lightpink :"#ffb6c1",
1040 lightsalmon :"#ffa07a",
1041 lightseagreen :"#20b2aa",
1042 lightskyblue :"#87cefa",
1043 lightslategray :"#778899",
1044 lightsteelblue :"#b0c4de",
1045 lightyellow :"#ffffe0",
1047 limegreen :"#32cd32",
1051 mediumaquamarine:"#66cdaa",
1052 mediumblue :"#0000cd",
1053 mediumorchid :"#ba55d3",
1054 mediumpurple :"#9370d8",
1055 mediumseagreen :"#3cb371",
1056 mediumslateblue :"#7b68ee",
1057 mediumspringgreen :"#00fa9a",
1058 mediumturquoise :"#48d1cc",
1059 mediumvioletred :"#c71585",
1060 midnightblue :"#191970",
1061 mintcream :"#f5fffa",
1062 mistyrose :"#ffe4e1",
1063 moccasin :"#ffe4b5",
1064 navajowhite :"#ffdead",
1068 olivedrab :"#6b8e23",
1070 orangered :"#ff4500",
1072 palegoldenrod :"#eee8aa",
1073 palegreen :"#98fb98",
1074 paleturquoise :"#afeeee",
1075 palevioletred :"#d87093",
1076 papayawhip :"#ffefd5",
1077 peachpuff :"#ffdab9",
1081 powderblue :"#b0e0e6",
1084 rosybrown :"#bc8f8f",
1085 royalblue :"#4169e1",
1086 saddlebrown :"#8b4513",
1088 sandybrown :"#f4a460",
1089 seagreen :"#2e8b57",
1090 seashell :"#fff5ee",
1094 slateblue :"#6a5acd",
1095 slategray :"#708090",
1097 springgreen :"#00ff7f",
1098 steelblue :"#4682b4",
1103 turquoise :"#40e0d0",
1107 whitesmoke :"#f5f5f5",
1109 yellowgreen :"#9acd32"
1111 /*global SyntaxUnit, Parser*/
1113 * Represents a selector combinator (whitespace, +, >).
1114 * @namespace parserlib.css
1116 * @extends parserlib.util.SyntaxUnit
1118 * @param {String} text The text representation of the unit.
1119 * @param {int} line The line of text on which the unit resides.
1120 * @param {int} col The column of text on which the unit resides.
1122 function Combinator(text, line, col){
1124 SyntaxUnit.call(this, text, line, col, Parser.COMBINATOR_TYPE);
1127 * The type of modifier.
1131 this.type = "unknown";
1134 if (/^\s+$/.test(text)){
1135 this.type = "descendant";
1136 } else if (text == ">"){
1137 this.type = "child";
1138 } else if (text == "+"){
1139 this.type = "adjacent-sibling";
1140 } else if (text == "~"){
1141 this.type = "sibling";
1146 Combinator.prototype = new SyntaxUnit();
1147 Combinator.prototype.constructor = Combinator;
1150 /*global SyntaxUnit, Parser*/
1152 * Represents a media feature, such as max-width:500.
1153 * @namespace parserlib.css
1154 * @class MediaFeature
1155 * @extends parserlib.util.SyntaxUnit
1157 * @param {SyntaxUnit} name The name of the feature.
1158 * @param {SyntaxUnit} value The value of the feature or null if none.
1160 function MediaFeature(name, value){
1162 SyntaxUnit.call(this, "(" + name + (value !== null ? ":" + value : "") + ")", name.startLine, name.startCol, Parser.MEDIA_FEATURE_TYPE);
1165 * The name of the media feature
1172 * The value for the feature or null if there is none.
1179 MediaFeature.prototype = new SyntaxUnit();
1180 MediaFeature.prototype.constructor = MediaFeature;
1183 /*global SyntaxUnit, Parser*/
1185 * Represents an individual media query.
1186 * @namespace parserlib.css
1188 * @extends parserlib.util.SyntaxUnit
1190 * @param {String} modifier The modifier "not" or "only" (or null).
1191 * @param {String} mediaType The type of media (i.e., "print").
1192 * @param {Array} parts Array of selectors parts making up this selector.
1193 * @param {int} line The line of text on which the unit resides.
1194 * @param {int} col The column of text on which the unit resides.
1196 function MediaQuery(modifier, mediaType, features, line, col){
1198 SyntaxUnit.call(this, (modifier ? modifier + " ": "") + (mediaType ? mediaType + " " : "") + features.join(" and "), line, col, Parser.MEDIA_QUERY_TYPE);
1201 * The media modifier ("not" or "only")
1203 * @property modifier
1205 this.modifier = modifier;
1208 * The mediaType (i.e., "print")
1210 * @property mediaType
1212 this.mediaType = mediaType;
1215 * The parts that make up the selector.
1217 * @property features
1219 this.features = features;
1223 MediaQuery.prototype = new SyntaxUnit();
1224 MediaQuery.prototype.constructor = MediaQuery;
1227 /*global Tokens, TokenStream, SyntaxError, Properties, Validation, ValidationError, SyntaxUnit,
1228 PropertyValue, PropertyValuePart, SelectorPart, SelectorSubPart, Selector,
1229 PropertyName, Combinator, MediaFeature, MediaQuery, EventTarget */
1233 * @namespace parserlib.css
1236 * @param {Object} options (Optional) Various options for the parser:
1237 * starHack (true|false) to allow IE6 star hack as valid,
1238 * underscoreHack (true|false) to interpret leading underscores
1239 * as IE6-7 targeting for known properties, ieFilters (true|false)
1240 * to indicate that IE < 8 filters should be accepted and not throw
1243 function Parser(options){
1245 //inherit event functionality
1246 EventTarget.call(this);
1249 this.options = options || {};
1251 this._tokenStream = null;
1255 Parser.DEFAULT_TYPE = 0;
1256 Parser.COMBINATOR_TYPE = 1;
1257 Parser.MEDIA_FEATURE_TYPE = 2;
1258 Parser.MEDIA_QUERY_TYPE = 3;
1259 Parser.PROPERTY_NAME_TYPE = 4;
1260 Parser.PROPERTY_VALUE_TYPE = 5;
1261 Parser.PROPERTY_VALUE_PART_TYPE = 6;
1262 Parser.SELECTOR_TYPE = 7;
1263 Parser.SELECTOR_PART_TYPE = 8;
1264 Parser.SELECTOR_SUB_PART_TYPE = 9;
1266 Parser.prototype = function(){
1268 var proto = new EventTarget(), //new prototype
1272 //restore constructor
1273 constructor: Parser,
1275 //instance constants - yuck
1277 COMBINATOR_TYPE : 1,
1278 MEDIA_FEATURE_TYPE : 2,
1279 MEDIA_QUERY_TYPE : 3,
1280 PROPERTY_NAME_TYPE : 4,
1281 PROPERTY_VALUE_TYPE : 5,
1282 PROPERTY_VALUE_PART_TYPE : 6,
1284 SELECTOR_PART_TYPE : 8,
1285 SELECTOR_SUB_PART_TYPE : 9,
1287 //-----------------------------------------------------------------
1289 //-----------------------------------------------------------------
1291 _stylesheet: function(){
1295 * : [ CHARSET_SYM S* STRING S* ';' ]?
1296 * [S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
1297 * [ namespace [S|CDO|CDC]* ]*
1298 * [ [ ruleset | media | page | font_face | keyframes ] [S|CDO|CDC]* ]*
1302 var tokenStream = this._tokenStream,
1308 this.fire("startstylesheet");
1310 //try to read character set
1315 //try to read imports - may be more than one
1316 while (tokenStream.peek() == Tokens.IMPORT_SYM){
1321 //try to read namespaces - may be more than one
1322 while (tokenStream.peek() == Tokens.NAMESPACE_SYM){
1327 //get the next token
1328 tt = tokenStream.peek();
1330 //try to read the rest
1331 while(tt > Tokens.EOF){
1336 case Tokens.MEDIA_SYM:
1340 case Tokens.PAGE_SYM:
1344 case Tokens.FONT_FACE_SYM:
1348 case Tokens.KEYFRAMES_SYM:
1352 case Tokens.UNKNOWN_SYM: //unknown @ rule
1354 if (!this.options.strict){
1360 message: "Unknown @ rule: " + tokenStream.LT(0).value + ".",
1361 line: tokenStream.LT(0).startLine,
1362 col: tokenStream.LT(0).startCol
1367 while (tokenStream.advance([Tokens.LBRACE, Tokens.RBRACE]) == Tokens.LBRACE){
1368 count++; //keep track of nesting depth
1372 tokenStream.advance([Tokens.RBRACE]);
1377 //not a syntax error, rethrow it
1378 throw new SyntaxError("Unknown @ rule.", tokenStream.LT(0).startLine, tokenStream.LT(0).startCol);
1382 this._readWhitespace();
1385 if(!this._ruleset()){
1387 //error handling for known issues
1389 case Tokens.CHARSET_SYM:
1390 token = tokenStream.LT(1);
1391 this._charset(false);
1392 throw new SyntaxError("@charset not allowed here.", token.startLine, token.startCol);
1393 case Tokens.IMPORT_SYM:
1394 token = tokenStream.LT(1);
1395 this._import(false);
1396 throw new SyntaxError("@import not allowed here.", token.startLine, token.startCol);
1397 case Tokens.NAMESPACE_SYM:
1398 token = tokenStream.LT(1);
1399 this._namespace(false);
1400 throw new SyntaxError("@namespace not allowed here.", token.startLine, token.startCol);
1402 tokenStream.get(); //get the last token
1403 this._unexpectedToken(tokenStream.token());
1409 if (ex instanceof SyntaxError && !this.options.strict){
1413 message: ex.message,
1422 tt = tokenStream.peek();
1425 if (tt != Tokens.EOF){
1426 this._unexpectedToken(tokenStream.token());
1429 this.fire("endstylesheet");
1432 _charset: function(emit){
1433 var tokenStream = this._tokenStream,
1439 if (tokenStream.match(Tokens.CHARSET_SYM)){
1440 line = tokenStream.token().startLine;
1441 col = tokenStream.token().startCol;
1443 this._readWhitespace();
1444 tokenStream.mustMatch(Tokens.STRING);
1446 token = tokenStream.token();
1447 charset = token.value;
1449 this._readWhitespace();
1450 tokenStream.mustMatch(Tokens.SEMICOLON);
1452 if (emit !== false){
1463 _import: function(emit){
1467 * [STRING|URI] S* media_query_list? ';' S*
1470 var tokenStream = this._tokenStream,
1476 //read import symbol
1477 tokenStream.mustMatch(Tokens.IMPORT_SYM);
1478 importToken = tokenStream.token();
1479 this._readWhitespace();
1481 tokenStream.mustMatch([Tokens.STRING, Tokens.URI]);
1483 //grab the URI value
1484 uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1");
1486 this._readWhitespace();
1488 mediaList = this._media_query_list();
1490 //must end with a semicolon
1491 tokenStream.mustMatch(Tokens.SEMICOLON);
1492 this._readWhitespace();
1494 if (emit !== false){
1499 line: importToken.startLine,
1500 col: importToken.startCol
1506 _namespace: function(emit){
1509 * : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
1512 var tokenStream = this._tokenStream,
1518 //read import symbol
1519 tokenStream.mustMatch(Tokens.NAMESPACE_SYM);
1520 line = tokenStream.token().startLine;
1521 col = tokenStream.token().startCol;
1522 this._readWhitespace();
1524 //it's a namespace prefix - no _namespace_prefix() method because it's just an IDENT
1525 if (tokenStream.match(Tokens.IDENT)){
1526 prefix = tokenStream.token().value;
1527 this._readWhitespace();
1530 tokenStream.mustMatch([Tokens.STRING, Tokens.URI]);
1531 /*if (!tokenStream.match(Tokens.STRING)){
1532 tokenStream.mustMatch(Tokens.URI);
1535 //grab the URI value
1536 uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1");
1538 this._readWhitespace();
1540 //must end with a semicolon
1541 tokenStream.mustMatch(Tokens.SEMICOLON);
1542 this._readWhitespace();
1544 if (emit !== false){
1559 * : MEDIA_SYM S* media_query_list S* '{' S* ruleset* '}' S*
1562 var tokenStream = this._tokenStream,
1568 tokenStream.mustMatch(Tokens.MEDIA_SYM);
1569 line = tokenStream.token().startLine;
1570 col = tokenStream.token().startCol;
1572 this._readWhitespace();
1574 mediaList = this._media_query_list();
1576 tokenStream.mustMatch(Tokens.LBRACE);
1577 this._readWhitespace();
1587 if (tokenStream.peek() == Tokens.PAGE_SYM){
1589 } else if (!this._ruleset()){
1594 tokenStream.mustMatch(Tokens.RBRACE);
1595 this._readWhitespace();
1606 //CSS3 Media Queries
1607 _media_query_list: function(){
1610 * : S* [media_query [ ',' S* media_query ]* ]?
1613 var tokenStream = this._tokenStream,
1617 this._readWhitespace();
1619 if (tokenStream.peek() == Tokens.IDENT || tokenStream.peek() == Tokens.LPAREN){
1620 mediaList.push(this._media_query());
1623 while(tokenStream.match(Tokens.COMMA)){
1624 this._readWhitespace();
1625 mediaList.push(this._media_query());
1632 * Note: "expression" in the grammar maps to the _media_expression
1636 _media_query: function(){
1639 * : [ONLY | NOT]? S* media_type S* [ AND S* expression ]*
1640 * | expression [ AND S* expression ]*
1643 var tokenStream = this._tokenStream,
1649 if (tokenStream.match(Tokens.IDENT)){
1650 ident = tokenStream.token().value.toLowerCase();
1652 //since there's no custom tokens for these, need to manually check
1653 if (ident != "only" && ident != "not"){
1654 tokenStream.unget();
1657 token = tokenStream.token();
1661 this._readWhitespace();
1663 if (tokenStream.peek() == Tokens.IDENT){
1664 type = this._media_type();
1665 if (token === null){
1666 token = tokenStream.token();
1668 } else if (tokenStream.peek() == Tokens.LPAREN){
1669 if (token === null){
1670 token = tokenStream.LT(1);
1672 expressions.push(this._media_expression());
1675 if (type === null && expressions.length === 0){
1678 this._readWhitespace();
1679 while (tokenStream.match(Tokens.IDENT)){
1680 if (tokenStream.token().value.toLowerCase() != "and"){
1681 this._unexpectedToken(tokenStream.token());
1684 this._readWhitespace();
1685 expressions.push(this._media_expression());
1689 return new MediaQuery(ident, type, expressions, token.startLine, token.startCol);
1692 //CSS3 Media Queries
1693 _media_type: function(){
1699 return this._media_feature();
1703 * Note: in CSS3 Media Queries, this is called "expression".
1704 * Renamed here to avoid conflict with CSS3 Selectors
1705 * definition of "expression". Also note that "expr" in the
1706 * grammar now maps to "expression" from CSS3 selectors.
1707 * @method _media_expression
1710 _media_expression: function(){
1713 * : '(' S* media_feature S* [ ':' S* expr ]? ')' S*
1716 var tokenStream = this._tokenStream,
1721 tokenStream.mustMatch(Tokens.LPAREN);
1723 feature = this._media_feature();
1724 this._readWhitespace();
1726 if (tokenStream.match(Tokens.COLON)){
1727 this._readWhitespace();
1728 token = tokenStream.LT(1);
1729 expression = this._expression();
1732 tokenStream.mustMatch(Tokens.RPAREN);
1733 this._readWhitespace();
1735 return new MediaFeature(feature, (expression ? new SyntaxUnit(expression, token.startLine, token.startCol) : null));
1738 //CSS3 Media Queries
1739 _media_feature: function(){
1745 var tokenStream = this._tokenStream;
1747 tokenStream.mustMatch(Tokens.IDENT);
1749 return SyntaxUnit.fromToken(tokenStream.token());
1756 * PAGE_SYM S* IDENT? pseudo_page? S*
1757 * '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S*
1760 var tokenStream = this._tokenStream,
1767 tokenStream.mustMatch(Tokens.PAGE_SYM);
1768 line = tokenStream.token().startLine;
1769 col = tokenStream.token().startCol;
1771 this._readWhitespace();
1773 if (tokenStream.match(Tokens.IDENT)){
1774 identifier = tokenStream.token().value;
1776 //The value 'auto' may not be used as a page name and MUST be treated as a syntax error.
1777 if (identifier.toLowerCase() === "auto"){
1778 this._unexpectedToken(tokenStream.token());
1782 //see if there's a colon upcoming
1783 if (tokenStream.peek() == Tokens.COLON){
1784 pseudoPage = this._pseudo_page();
1787 this._readWhitespace();
1797 this._readDeclarations(true, true);
1810 _margin: function(){
1813 * margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
1816 var tokenStream = this._tokenStream,
1819 marginSym = this._margin_sym();
1822 line = tokenStream.token().startLine;
1823 col = tokenStream.token().startCol;
1826 type: "startpagemargin",
1832 this._readDeclarations(true);
1835 type: "endpagemargin",
1847 _margin_sym: function(){
1851 * TOPLEFTCORNER_SYM |
1855 * TOPRIGHTCORNER_SYM |
1856 * BOTTOMLEFTCORNER_SYM |
1858 * BOTTOMCENTER_SYM |
1860 * BOTTOMRIGHTCORNER_SYM |
1870 var tokenStream = this._tokenStream;
1872 if(tokenStream.match([Tokens.TOPLEFTCORNER_SYM, Tokens.TOPLEFT_SYM,
1873 Tokens.TOPCENTER_SYM, Tokens.TOPRIGHT_SYM, Tokens.TOPRIGHTCORNER_SYM,
1874 Tokens.BOTTOMLEFTCORNER_SYM, Tokens.BOTTOMLEFT_SYM,
1875 Tokens.BOTTOMCENTER_SYM, Tokens.BOTTOMRIGHT_SYM,
1876 Tokens.BOTTOMRIGHTCORNER_SYM, Tokens.LEFTTOP_SYM,
1877 Tokens.LEFTMIDDLE_SYM, Tokens.LEFTBOTTOM_SYM, Tokens.RIGHTTOP_SYM,
1878 Tokens.RIGHTMIDDLE_SYM, Tokens.RIGHTBOTTOM_SYM]))
1880 return SyntaxUnit.fromToken(tokenStream.token());
1887 _pseudo_page: function(){
1894 var tokenStream = this._tokenStream;
1896 tokenStream.mustMatch(Tokens.COLON);
1897 tokenStream.mustMatch(Tokens.IDENT);
1899 //TODO: CSS3 Paged Media says only "left", "center", and "right" are allowed
1901 return tokenStream.token().value;
1904 _font_face: function(){
1907 * : FONT_FACE_SYM S*
1908 * '{' S* declaration [ ';' S* declaration ]* '}' S*
1911 var tokenStream = this._tokenStream,
1916 tokenStream.mustMatch(Tokens.FONT_FACE_SYM);
1917 line = tokenStream.token().startLine;
1918 col = tokenStream.token().startCol;
1920 this._readWhitespace();
1923 type: "startfontface",
1928 this._readDeclarations(true);
1931 type: "endfontface",
1937 _operator: function(){
1941 * : '/' S* | ',' S* | /( empty )/
1945 var tokenStream = this._tokenStream,
1948 if (tokenStream.match([Tokens.SLASH, Tokens.COMMA])){
1949 token = tokenStream.token();
1950 this._readWhitespace();
1952 return token ? PropertyValuePart.fromToken(token) : null;
1956 _combinator: function(){
1960 * : PLUS S* | GREATER S* | TILDE S* | S+
1964 var tokenStream = this._tokenStream,
1968 if(tokenStream.match([Tokens.PLUS, Tokens.GREATER, Tokens.TILDE])){
1969 token = tokenStream.token();
1970 value = new Combinator(token.value, token.startLine, token.startCol);
1971 this._readWhitespace();
1977 _unary_operator: function(){
1985 var tokenStream = this._tokenStream;
1987 if (tokenStream.match([Tokens.MINUS, Tokens.PLUS])){
1988 return tokenStream.token().value;
1994 _property: function(){
2002 var tokenStream = this._tokenStream,
2010 //check for star hack - throws error if not allowed
2011 if (tokenStream.peek() == Tokens.STAR && this.options.starHack){
2013 token = tokenStream.token();
2015 line = token.startLine;
2016 col = token.startCol;
2019 if(tokenStream.match(Tokens.IDENT)){
2020 token = tokenStream.token();
2021 tokenValue = token.value;
2023 //check for underscore hack - no error if not allowed because it's valid CSS syntax
2024 if (tokenValue.charAt(0) == "_" && this.options.underscoreHack){
2026 tokenValue = tokenValue.substring(1);
2029 value = new PropertyName(tokenValue, hack, (line||token.startLine), (col||token.startCol));
2030 this._readWhitespace();
2036 //Augmented with CSS3 Selectors
2037 _ruleset: function(){
2041 * '{' S* declaration? [ ';' S* declaration? ]* '}' S*
2045 var tokenStream = this._tokenStream,
2051 * Error Recovery: If even a single selector fails to parse,
2052 * then the entire ruleset should be thrown away.
2055 selectors = this._selectors_group();
2057 if (ex instanceof SyntaxError && !this.options.strict){
2063 message: ex.message,
2068 //skip over everything until closing brace
2069 tt = tokenStream.advance([Tokens.RBRACE]);
2070 if (tt == Tokens.RBRACE){
2071 //if there's a right brace, the rule is finished so don't do anything
2073 //otherwise, rethrow the error because it wasn't handled properly
2078 //not a syntax error, rethrow it
2082 //trigger parser to continue
2086 //if it got here, all selectors parsed
2091 selectors: selectors,
2092 line: selectors[0].line,
2093 col: selectors[0].col
2096 this._readDeclarations(true);
2100 selectors: selectors,
2101 line: selectors[0].line,
2102 col: selectors[0].col
2112 _selectors_group: function(){
2116 * : selector [ COMMA S* selector ]*
2119 var tokenStream = this._tokenStream,
2123 selector = this._selector();
2124 if (selector !== null){
2126 selectors.push(selector);
2127 while(tokenStream.match(Tokens.COMMA)){
2128 this._readWhitespace();
2129 selector = this._selector();
2130 if (selector !== null){
2131 selectors.push(selector);
2133 this._unexpectedToken(tokenStream.LT(1));
2138 return selectors.length ? selectors : null;
2142 _selector: function(){
2145 * : simple_selector_sequence [ combinator simple_selector_sequence ]*
2149 var tokenStream = this._tokenStream,
2151 nextSelector = null,
2155 //if there's no simple selector, then there's no selector
2156 nextSelector = this._simple_selector_sequence();
2157 if (nextSelector === null){
2161 selector.push(nextSelector);
2165 //look for a combinator
2166 combinator = this._combinator();
2168 if (combinator !== null){
2169 selector.push(combinator);
2170 nextSelector = this._simple_selector_sequence();
2172 //there must be a next selector
2173 if (nextSelector === null){
2174 this._unexpectedToken(this.LT(1));
2177 //nextSelector is an instance of SelectorPart
2178 selector.push(nextSelector);
2182 //if there's not whitespace, we're done
2183 if (this._readWhitespace()){
2185 //add whitespace separator
2186 ws = new Combinator(tokenStream.token().value, tokenStream.token().startLine, tokenStream.token().startCol);
2188 //combinator is not required
2189 combinator = this._combinator();
2191 //selector is required if there's a combinator
2192 nextSelector = this._simple_selector_sequence();
2193 if (nextSelector === null){
2194 if (combinator !== null){
2195 this._unexpectedToken(tokenStream.LT(1));
2199 if (combinator !== null){
2200 selector.push(combinator);
2205 selector.push(nextSelector);
2214 return new Selector(selector, selector[0].line, selector[0].col);
2218 _simple_selector_sequence: function(){
2220 * simple_selector_sequence
2221 * : [ type_selector | universal ]
2222 * [ HASH | class | attrib | pseudo | negation ]*
2223 * | [ HASH | class | attrib | pseudo | negation ]+
2227 var tokenStream = this._tokenStream,
2229 //parts of a simple selector
2233 //complete selector text
2236 //the different parts after the element name to search for
2240 return tokenStream.match(Tokens.HASH) ?
2241 new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) :
2250 len = components.length,
2257 //get starting line and column for the selector
2258 line = tokenStream.LT(1).startLine;
2259 col = tokenStream.LT(1).startCol;
2261 elementName = this._type_selector();
2263 elementName = this._universal();
2266 if (elementName !== null){
2267 selectorText += elementName;
2272 //whitespace means we're done
2273 if (tokenStream.peek() === Tokens.S){
2277 //check for each component
2278 while(i < len && component === null){
2279 component = components[i++].call(this);
2282 if (component === null){
2284 //we don't have a selector
2285 if (selectorText === ""){
2292 modifiers.push(component);
2293 selectorText += component.toString();
2299 return selectorText !== "" ?
2300 new SelectorPart(elementName, modifiers, selectorText, line, col) :
2305 _type_selector: function(){
2308 * : [ namespace_prefix ]? element_name
2312 var tokenStream = this._tokenStream,
2313 ns = this._namespace_prefix(),
2314 elementName = this._element_name();
2318 * Need to back out the namespace that was read due to both
2319 * type_selector and universal reading namespace_prefix
2320 * first. Kind of hacky, but only way I can figure out
2321 * right now how to not change the grammar.
2324 tokenStream.unget();
2326 tokenStream.unget();
2333 elementName.text = ns + elementName.text;
2334 elementName.col -= ns.length;
2348 var tokenStream = this._tokenStream,
2351 if (tokenStream.match(Tokens.DOT)){
2352 tokenStream.mustMatch(Tokens.IDENT);
2353 token = tokenStream.token();
2354 return new SelectorSubPart("." + token.value, "class", token.startLine, token.startCol - 1);
2362 _element_name: function(){
2369 var tokenStream = this._tokenStream,
2372 if (tokenStream.match(Tokens.IDENT)){
2373 token = tokenStream.token();
2374 return new SelectorSubPart(token.value, "elementName", token.startLine, token.startCol);
2382 _namespace_prefix: function(){
2385 * : [ IDENT | '*' ]? '|'
2388 var tokenStream = this._tokenStream,
2391 //verify that this is a namespace prefix
2392 if (tokenStream.LA(1) === Tokens.PIPE || tokenStream.LA(2) === Tokens.PIPE){
2394 if(tokenStream.match([Tokens.IDENT, Tokens.STAR])){
2395 value += tokenStream.token().value;
2398 tokenStream.mustMatch(Tokens.PIPE);
2403 return value.length ? value : null;
2407 _universal: function(){
2410 * : [ namespace_prefix ]? '*'
2413 var tokenStream = this._tokenStream,
2417 ns = this._namespace_prefix();
2422 if(tokenStream.match(Tokens.STAR)){
2426 return value.length ? value : null;
2431 _attrib: function(){
2434 * : '[' S* [ namespace_prefix ]? IDENT S*
2440 * DASHMATCH ] S* [ IDENT | STRING ] S*
2445 var tokenStream = this._tokenStream,
2450 if (tokenStream.match(Tokens.LBRACKET)){
2451 token = tokenStream.token();
2452 value = token.value;
2453 value += this._readWhitespace();
2455 ns = this._namespace_prefix();
2461 tokenStream.mustMatch(Tokens.IDENT);
2462 value += tokenStream.token().value;
2463 value += this._readWhitespace();
2465 if(tokenStream.match([Tokens.PREFIXMATCH, Tokens.SUFFIXMATCH, Tokens.SUBSTRINGMATCH,
2466 Tokens.EQUALS, Tokens.INCLUDES, Tokens.DASHMATCH])){
2468 value += tokenStream.token().value;
2469 value += this._readWhitespace();
2471 tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]);
2472 value += tokenStream.token().value;
2473 value += this._readWhitespace();
2476 tokenStream.mustMatch(Tokens.RBRACKET);
2478 return new SelectorSubPart(value + "]", "attribute", token.startLine, token.startCol);
2485 _pseudo: function(){
2489 * : ':' ':'? [ IDENT | functional_pseudo ]
2493 var tokenStream = this._tokenStream,
2499 if (tokenStream.match(Tokens.COLON)){
2501 if (tokenStream.match(Tokens.COLON)){
2505 if (tokenStream.match(Tokens.IDENT)){
2506 pseudo = tokenStream.token().value;
2507 line = tokenStream.token().startLine;
2508 col = tokenStream.token().startCol - colons.length;
2509 } else if (tokenStream.peek() == Tokens.FUNCTION){
2510 line = tokenStream.LT(1).startLine;
2511 col = tokenStream.LT(1).startCol - colons.length;
2512 pseudo = this._functional_pseudo();
2516 pseudo = new SelectorSubPart(colons + pseudo, "pseudo", line, col);
2524 _functional_pseudo: function(){
2527 * : FUNCTION S* expression ')'
2531 var tokenStream = this._tokenStream,
2534 if(tokenStream.match(Tokens.FUNCTION)){
2535 value = tokenStream.token().value;
2536 value += this._readWhitespace();
2537 value += this._expression();
2538 tokenStream.mustMatch(Tokens.RPAREN);
2546 _expression: function(){
2549 * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
2553 var tokenStream = this._tokenStream,
2556 while(tokenStream.match([Tokens.PLUS, Tokens.MINUS, Tokens.DIMENSION,
2557 Tokens.NUMBER, Tokens.STRING, Tokens.IDENT, Tokens.LENGTH,
2558 Tokens.FREQ, Tokens.ANGLE, Tokens.TIME,
2559 Tokens.RESOLUTION])){
2561 value += tokenStream.token().value;
2562 value += this._readWhitespace();
2565 return value.length ? value : null;
2570 _negation: function(){
2573 * : NOT S* negation_arg S* ')'
2577 var tokenStream = this._tokenStream,
2584 if (tokenStream.match(Tokens.NOT)){
2585 value = tokenStream.token().value;
2586 line = tokenStream.token().startLine;
2587 col = tokenStream.token().startCol;
2588 value += this._readWhitespace();
2589 arg = this._negation_arg();
2591 value += this._readWhitespace();
2592 tokenStream.match(Tokens.RPAREN);
2593 value += tokenStream.token().value;
2595 subpart = new SelectorSubPart(value, "not", line, col);
2596 subpart.args.push(arg);
2603 _negation_arg: function(){
2606 * : type_selector | universal | HASH | class | attrib | pseudo
2610 var tokenStream = this._tokenStream,
2612 this._type_selector,
2615 return tokenStream.match(Tokens.HASH) ?
2616 new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) :
2631 line = tokenStream.LT(1).startLine;
2632 col = tokenStream.LT(1).startCol;
2634 while(i < len && arg === null){
2636 arg = args[i].call(this);
2640 //must be a negation arg
2642 this._unexpectedToken(tokenStream.LT(1));
2645 //it's an element name
2646 if (arg.type == "elementName"){
2647 part = new SelectorPart(arg, [], arg.toString(), line, col);
2649 part = new SelectorPart(null, [arg], arg.toString(), line, col);
2655 _declaration: function(){
2659 * : property ':' S* expr prio?
2664 var tokenStream = this._tokenStream,
2672 property = this._property();
2673 if (property !== null){
2675 tokenStream.mustMatch(Tokens.COLON);
2676 this._readWhitespace();
2678 expr = this._expr();
2680 //if there's no parts for the value, it's an error
2681 if (!expr || expr.length === 0){
2682 this._unexpectedToken(tokenStream.LT(1));
2685 prio = this._prio();
2688 * If hacks should be allowed, then only check the root
2689 * property. If hacks should not be allowed, treat
2690 * _property or *property as invalid properties.
2692 propertyName = property.toString();
2693 if (this.options.starHack && property.hack == "*" ||
2694 this.options.underscoreHack && property.hack == "_") {
2696 propertyName = property.text;
2700 this._validateProperty(propertyName, expr);
2710 line: property.line,
2724 * : IMPORTANT_SYM S*
2728 var tokenStream = this._tokenStream,
2729 result = tokenStream.match(Tokens.IMPORTANT_SYM);
2731 this._readWhitespace();
2738 * : term [ operator term ]*
2742 var tokenStream = this._tokenStream,
2748 value = this._term();
2749 if (value !== null){
2754 operator = this._operator();
2756 //if there's an operator, keep building up the value parts
2758 values.push(operator);
2760 //if there's not an operator, you have a full value
2761 values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col));
2765 value = this._term();
2767 if (value === null){
2776 /*if (valueParts.length){
2777 values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col));
2780 return values.length > 0 ? new PropertyValue(values, values[0].line, values[0].col) : null;
2788 * [ NUMBER S* | PERCENTAGE S* | LENGTH S* | ANGLE S* |
2789 * TIME S* | FREQ S* | function | ie_function ]
2790 * | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor
2794 var tokenStream = this._tokenStream,
2801 //returns the operator or null
2802 unary = this._unary_operator();
2803 if (unary !== null){
2804 line = tokenStream.token().startLine;
2805 col = tokenStream.token().startCol;
2808 //exception for IE filters
2809 if (tokenStream.peek() == Tokens.IE_FUNCTION && this.options.ieFilters){
2811 value = this._ie_function();
2812 if (unary === null){
2813 line = tokenStream.token().startLine;
2814 col = tokenStream.token().startCol;
2817 //see if there's a simple match
2818 } else if (tokenStream.match([Tokens.NUMBER, Tokens.PERCENTAGE, Tokens.LENGTH,
2819 Tokens.ANGLE, Tokens.TIME,
2820 Tokens.FREQ, Tokens.STRING, Tokens.IDENT, Tokens.URI, Tokens.UNICODE_RANGE])){
2822 value = tokenStream.token().value;
2823 if (unary === null){
2824 line = tokenStream.token().startLine;
2825 col = tokenStream.token().startCol;
2827 this._readWhitespace();
2830 //see if it's a color
2831 token = this._hexcolor();
2832 if (token === null){
2834 //if there's no unary, get the start of the next token for line/col info
2835 if (unary === null){
2836 line = tokenStream.LT(1).startLine;
2837 col = tokenStream.LT(1).startCol;
2840 //has to be a function
2841 if (value === null){
2844 * This checks for alpha(opacity=0) style of IE
2845 * functions. IE_FUNCTION only presents progid: style.
2847 if (tokenStream.LA(3) == Tokens.EQUALS && this.options.ieFilters){
2848 value = this._ie_function();
2850 value = this._function();
2854 /*if (value === null){
2856 //throw new Error("Expected identifier at line " + tokenStream.token().startLine + ", character " + tokenStream.token().startCol + ".");
2860 value = token.value;
2861 if (unary === null){
2862 line = token.startLine;
2863 col = token.startCol;
2869 return value !== null ?
2870 new PropertyValuePart(unary !== null ? unary + value : value, line, col) :
2875 _function: function(){
2879 * : FUNCTION S* expr ')' S*
2883 var tokenStream = this._tokenStream,
2884 functionText = null,
2888 if (tokenStream.match(Tokens.FUNCTION)){
2889 functionText = tokenStream.token().value;
2890 this._readWhitespace();
2891 expr = this._expr();
2892 functionText += expr;
2894 //START: Horrible hack in case it's an IE filter
2895 if (this.options.ieFilters && tokenStream.peek() == Tokens.EQUALS){
2898 if (this._readWhitespace()){
2899 functionText += tokenStream.token().value;
2902 //might be second time in the loop
2903 if (tokenStream.LA(0) == Tokens.COMMA){
2904 functionText += tokenStream.token().value;
2907 tokenStream.match(Tokens.IDENT);
2908 functionText += tokenStream.token().value;
2910 tokenStream.match(Tokens.EQUALS);
2911 functionText += tokenStream.token().value;
2913 //functionText += this._term();
2914 lt = tokenStream.peek();
2915 while(lt != Tokens.COMMA && lt != Tokens.S && lt != Tokens.RPAREN){
2917 functionText += tokenStream.token().value;
2918 lt = tokenStream.peek();
2920 } while(tokenStream.match([Tokens.COMMA, Tokens.S]));
2923 //END: Horrible Hack
2925 tokenStream.match(Tokens.RPAREN);
2926 functionText += ")";
2927 this._readWhitespace();
2930 return functionText;
2933 _ie_function: function(){
2935 /* (My own extension)
2937 * : IE_FUNCTION S* IDENT '=' term [S* ','? IDENT '=' term]+ ')' S*
2941 var tokenStream = this._tokenStream,
2942 functionText = null,
2946 //IE function can begin like a regular function, too
2947 if (tokenStream.match([Tokens.IE_FUNCTION, Tokens.FUNCTION])){
2948 functionText = tokenStream.token().value;
2952 if (this._readWhitespace()){
2953 functionText += tokenStream.token().value;
2956 //might be second time in the loop
2957 if (tokenStream.LA(0) == Tokens.COMMA){
2958 functionText += tokenStream.token().value;
2961 tokenStream.match(Tokens.IDENT);
2962 functionText += tokenStream.token().value;
2964 tokenStream.match(Tokens.EQUALS);
2965 functionText += tokenStream.token().value;
2967 //functionText += this._term();
2968 lt = tokenStream.peek();
2969 while(lt != Tokens.COMMA && lt != Tokens.S && lt != Tokens.RPAREN){
2971 functionText += tokenStream.token().value;
2972 lt = tokenStream.peek();
2974 } while(tokenStream.match([Tokens.COMMA, Tokens.S]));
2976 tokenStream.match(Tokens.RPAREN);
2977 functionText += ")";
2978 this._readWhitespace();
2981 return functionText;
2984 _hexcolor: function(){
2986 * There is a constraint on the color that it must
2987 * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
2988 * after the "#"; e.g., "#000" is OK, but "#abcd" is not.
2995 var tokenStream = this._tokenStream,
2999 if(tokenStream.match(Tokens.HASH)){
3001 //need to do some validation here
3003 token = tokenStream.token();
3004 color = token.value;
3005 if (!/#[a-f0-9]{3,6}/i.test(color)){
3006 throw new SyntaxError("Expected a hex color but found '" + color + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol);
3008 this._readWhitespace();
3014 //-----------------------------------------------------------------
3015 // Animations methods
3016 //-----------------------------------------------------------------
3018 _keyframes: function(){
3022 * : KEYFRAMES_SYM S* keyframe_name S* '{' S* keyframe_rule* '}' {
3025 var tokenStream = this._tokenStream,
3030 tokenStream.mustMatch(Tokens.KEYFRAMES_SYM);
3031 this._readWhitespace();
3032 name = this._keyframe_name();
3034 this._readWhitespace();
3035 tokenStream.mustMatch(Tokens.LBRACE);
3038 type: "startkeyframes",
3044 this._readWhitespace();
3045 tt = tokenStream.peek();
3048 while(tt == Tokens.IDENT || tt == Tokens.PERCENTAGE) {
3049 this._keyframe_rule();
3050 this._readWhitespace();
3051 tt = tokenStream.peek();
3055 type: "endkeyframes",
3061 this._readWhitespace();
3062 tokenStream.mustMatch(Tokens.RBRACE);
3066 _keyframe_name: function(){
3074 var tokenStream = this._tokenStream,
3077 tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]);
3078 return SyntaxUnit.fromToken(tokenStream.token());
3081 _keyframe_rule: function(){
3086 * '{' S* declaration [ ';' S* declaration ]* '}' S*
3089 var tokenStream = this._tokenStream,
3091 keyList = this._key_list();
3094 type: "startkeyframerule",
3096 line: keyList[0].line,
3100 this._readDeclarations(true);
3103 type: "endkeyframerule",
3105 line: keyList[0].line,
3111 _key_list: function(){
3115 * : key [ S* ',' S* key]*
3118 var tokenStream = this._tokenStream,
3123 //must be least one key
3124 keyList.push(this._key());
3126 this._readWhitespace();
3128 while(tokenStream.match(Tokens.COMMA)){
3129 this._readWhitespace();
3130 keyList.push(this._key());
3131 this._readWhitespace();
3139 * There is a restriction that IDENT can be only "from" or "to".
3147 var tokenStream = this._tokenStream,
3150 if (tokenStream.match(Tokens.PERCENTAGE)){
3151 return SyntaxUnit.fromToken(tokenStream.token());
3152 } else if (tokenStream.match(Tokens.IDENT)){
3153 token = tokenStream.token();
3155 if (/from|to/i.test(token.value)){
3156 return SyntaxUnit.fromToken(token);
3159 tokenStream.unget();
3162 //if it gets here, there wasn't a valid token, so time to explode
3163 this._unexpectedToken(tokenStream.LT(1));
3166 //-----------------------------------------------------------------
3168 //-----------------------------------------------------------------
3171 * Not part of CSS grammar, but useful for skipping over
3172 * combination of white space and HTML-style comments.
3174 * @method _skipCruft
3177 _skipCruft: function(){
3178 while(this._tokenStream.match([Tokens.S, Tokens.CDO, Tokens.CDC])){
3184 * Not part of CSS grammar, but this pattern occurs frequently
3185 * in the official CSS grammar. Split out here to eliminate
3187 * @param {Boolean} checkStart Indicates if the rule should check
3188 * for the left brace at the beginning.
3189 * @param {Boolean} readMargins Indicates if the rule should check
3190 * for margin patterns.
3192 * @method _readDeclarations
3195 _readDeclarations: function(checkStart, readMargins){
3198 * S* '{' S* declaration [ ';' S* declaration ]* '}' S*
3200 * S* '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S*
3201 * Note that this is how it is described in CSS3 Paged Media, but is actually incorrect.
3202 * A semicolon is only necessary following a delcaration is there's another declaration
3203 * or margin afterwards.
3205 var tokenStream = this._tokenStream,
3209 this._readWhitespace();
3212 tokenStream.mustMatch(Tokens.LBRACE);
3215 this._readWhitespace();
3221 if (tokenStream.match(Tokens.SEMICOLON) || (readMargins && this._margin())){
3223 } else if (this._declaration()){
3224 if (!tokenStream.match(Tokens.SEMICOLON)){
3231 //if ((!this._margin() && !this._declaration()) || !tokenStream.match(Tokens.SEMICOLON)){
3234 this._readWhitespace();
3237 tokenStream.mustMatch(Tokens.RBRACE);
3238 this._readWhitespace();
3241 if (ex instanceof SyntaxError && !this.options.strict){
3247 message: ex.message,
3252 //see if there's another declaration
3253 tt = tokenStream.advance([Tokens.SEMICOLON, Tokens.RBRACE]);
3254 if (tt == Tokens.SEMICOLON){
3255 //if there's a semicolon, then there might be another declaration
3256 this._readDeclarations(false, readMargins);
3257 } else if (tt != Tokens.RBRACE){
3258 //if there's a right brace, the rule is finished so don't do anything
3259 //otherwise, rethrow the error because it wasn't handled properly
3264 //not a syntax error, rethrow it
3272 * In some cases, you can end up with two white space tokens in a
3273 * row. Instead of making a change in every function that looks for
3274 * white space, this function is used to match as much white space
3276 * @method _readWhitespace
3277 * @return {String} The white space if found, empty string if not.
3280 _readWhitespace: function(){
3282 var tokenStream = this._tokenStream,
3285 while(tokenStream.match(Tokens.S)){
3286 ws += tokenStream.token().value;
3294 * Throws an error when an unexpected token is found.
3295 * @param {Object} token The token that was found.
3296 * @method _unexpectedToken
3300 _unexpectedToken: function(token){
3301 throw new SyntaxError("Unexpected token '" + token.value + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol);
3305 * Helper method used for parsing subparts of a style sheet.
3307 * @method _verifyEnd
3310 _verifyEnd: function(){
3311 if (this._tokenStream.LA(1) != Tokens.EOF){
3312 this._unexpectedToken(this._tokenStream.LT(1));
3316 //-----------------------------------------------------------------
3317 // Validation methods
3318 //-----------------------------------------------------------------
3319 _validateProperty: function(property, value){
3320 Validation.validate(property, value);
3323 //-----------------------------------------------------------------
3325 //-----------------------------------------------------------------
3327 parse: function(input){
3328 this._tokenStream = new TokenStream(input, Tokens);
3332 parseStyleSheet: function(input){
3334 return this.parse(input);
3337 parseMediaQuery: function(input){
3338 this._tokenStream = new TokenStream(input, Tokens);
3339 var result = this._media_query();
3341 //if there's anything more, then it's an invalid selector
3344 //otherwise return result
3349 * Parses a property value (everything after the semicolon).
3350 * @return {parserlib.css.PropertyValue} The property value.
3351 * @throws parserlib.util.SyntaxError If an unexpected token is found.
3352 * @method parserPropertyValue
3354 parsePropertyValue: function(input){
3356 this._tokenStream = new TokenStream(input, Tokens);
3357 this._readWhitespace();
3359 var result = this._expr();
3361 //okay to have a trailing white space
3362 this._readWhitespace();
3364 //if there's anything more, then it's an invalid selector
3367 //otherwise return result
3372 * Parses a complete CSS rule, including selectors and
3374 * @param {String} input The text to parser.
3375 * @return {Boolean} True if the parse completed successfully, false if not.
3378 parseRule: function(input){
3379 this._tokenStream = new TokenStream(input, Tokens);
3381 //skip any leading white space
3382 this._readWhitespace();
3384 var result = this._ruleset();
3386 //skip any trailing white space
3387 this._readWhitespace();
3389 //if there's anything more, then it's an invalid selector
3392 //otherwise return result
3397 * Parses a single CSS selector (no comma)
3398 * @param {String} input The text to parse as a CSS selector.
3399 * @return {Selector} An object representing the selector.
3400 * @throws parserlib.util.SyntaxError If an unexpected token is found.
3401 * @method parseSelector
3403 parseSelector: function(input){
3405 this._tokenStream = new TokenStream(input, Tokens);
3407 //skip any leading white space
3408 this._readWhitespace();
3410 var result = this._selector();
3412 //skip any trailing white space
3413 this._readWhitespace();
3415 //if there's anything more, then it's an invalid selector
3418 //otherwise return result
3423 * Parses an HTML style attribute: a set of CSS declarations
3424 * separated by semicolons.
3425 * @param {String} input The text to parse as a style attribute
3427 * @method parseStyleAttribute
3429 parseStyleAttribute: function(input){
3430 input += "}"; // for error recovery in _readDeclarations()
3431 this._tokenStream = new TokenStream(input, Tokens);
3432 this._readDeclarations();
3436 //copy over onto prototype
3437 for (prop in additions){
3438 if (additions.hasOwnProperty(prop)){
3439 proto[prop] = additions[prop];
3449 : S* [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]? |
3450 ['-'|'+']? INTEGER | {O}{D}{D} | {E}{V}{E}{N} ] S*
3453 /*global Validation, ValidationTypes, ValidationError*/
3457 "alignment-adjust" : "auto | baseline | before-edge | text-before-edge | middle | central | after-edge | text-after-edge | ideographic | alphabetic | hanging | mathematical | <percentage> | <length>",
3458 "alignment-baseline" : "baseline | use-script | before-edge | text-before-edge | after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | mathematical",
3460 "animation-delay" : { multi: "<time>", comma: true },
3461 "animation-direction" : { multi: "normal | alternate", comma: true },
3462 "animation-duration" : { multi: "<time>", comma: true },
3463 "animation-iteration-count" : { multi: "<number> | infinite", comma: true },
3464 "animation-name" : { multi: "none | <ident>", comma: true },
3465 "animation-play-state" : { multi: "running | paused", comma: true },
3466 "animation-timing-function" : 1,
3469 "-moz-animation-delay" : { multi: "<time>", comma: true },
3470 "-moz-animation-direction" : { multi: "normal | alternate", comma: true },
3471 "-moz-animation-duration" : { multi: "<time>", comma: true },
3472 "-moz-animation-iteration-count" : { multi: "<number> | infinite", comma: true },
3473 "-moz-animation-name" : { multi: "none | <ident>", comma: true },
3474 "-moz-animation-play-state" : { multi: "running | paused", comma: true },
3476 "-ms-animation-delay" : { multi: "<time>", comma: true },
3477 "-ms-animation-direction" : { multi: "normal | alternate", comma: true },
3478 "-ms-animation-duration" : { multi: "<time>", comma: true },
3479 "-ms-animation-iteration-count" : { multi: "<number> | infinite", comma: true },
3480 "-ms-animation-name" : { multi: "none | <ident>", comma: true },
3481 "-ms-animation-play-state" : { multi: "running | paused", comma: true },
3483 "-webkit-animation-delay" : { multi: "<time>", comma: true },
3484 "-webkit-animation-direction" : { multi: "normal | alternate", comma: true },
3485 "-webkit-animation-duration" : { multi: "<time>", comma: true },
3486 "-webkit-animation-iteration-count" : { multi: "<number> | infinite", comma: true },
3487 "-webkit-animation-name" : { multi: "none | <ident>", comma: true },
3488 "-webkit-animation-play-state" : { multi: "running | paused", comma: true },
3490 "-o-animation-delay" : { multi: "<time>", comma: true },
3491 "-o-animation-direction" : { multi: "normal | alternate", comma: true },
3492 "-o-animation-duration" : { multi: "<time>", comma: true },
3493 "-o-animation-iteration-count" : { multi: "<number> | infinite", comma: true },
3494 "-o-animation-name" : { multi: "none | <ident>", comma: true },
3495 "-o-animation-play-state" : { multi: "running | paused", comma: true },
3497 "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",
3498 "azimuth" : function (expression) {
3499 var simple = "<angle> | leftwards | rightwards | inherit",
3500 direction = "left-side | far-left | left | center-left | center | center-right | right | far-right | right-side",
3505 if (!ValidationTypes.isAny(expression, simple)) {
3506 if (ValidationTypes.isAny(expression, "behind")) {
3511 if (ValidationTypes.isAny(expression, direction)) {
3514 ValidationTypes.isAny(expression, "behind");
3519 if (expression.hasNext()) {
3520 part = expression.next();
3522 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
3524 throw new ValidationError("Expected (<'azimuth'>) but found '" + part + "'.", part.line, part.col);
3530 "backface-visibility" : "visible | hidden",
3532 "background-attachment" : { multi: "<attachment>", comma: true },
3533 "background-clip" : { multi: "<box>", comma: true },
3534 "background-color" : "<color> | inherit",
3535 "background-image" : { multi: "<bg-image>", comma: true },
3536 "background-origin" : { multi: "<box>", comma: true },
3537 "background-position" : { multi: "<bg-position>", comma: true },
3538 "background-repeat" : { multi: "<repeat-style>" },
3539 "background-size" : { multi: "<bg-size>", comma: true },
3540 "baseline-shift" : "baseline | sub | super | <percentage> | <length>",
3543 "bleed" : "<length>",
3544 "bookmark-label" : "<content> | <attr> | <string>",
3545 "bookmark-level" : "none | <integer>",
3546 "bookmark-state" : "open | closed",
3547 "bookmark-target" : "none | <uri> | <attr>",
3548 "border" : "<border-width> || <border-style> || <color>",
3549 "border-bottom" : "<border-width> || <border-style> || <color>",
3550 "border-bottom-color" : "<color>",
3551 "border-bottom-left-radius" : "<x-one-radius>",
3552 "border-bottom-right-radius" : "<x-one-radius>",
3553 "border-bottom-style" : "<border-style>",
3554 "border-bottom-width" : "<border-width>",
3555 "border-collapse" : "collapse | separate | inherit",
3556 "border-color" : { multi: "<color> | inherit", max: 4 },
3558 "border-image-outset" : { multi: "<length> | <number>", max: 4 },
3559 "border-image-repeat" : { multi: "stretch | repeat | round", max: 2 },
3560 "border-image-slice" : function(expression) {
3563 numeric = "<number> | <percentage>",
3569 if (ValidationTypes.isAny(expression, "fill")) {
3574 while (expression.hasNext() && count < max) {
3575 valid = ValidationTypes.isAny(expression, numeric);
3584 ValidationTypes.isAny(expression, "fill");
3589 if (expression.hasNext()) {
3590 part = expression.next();
3592 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
3594 throw new ValidationError("Expected ([<number> | <percentage>]{1,4} && fill?) but found '" + part + "'.", part.line, part.col);
3598 "border-image-source" : "<image> | none",
3599 "border-image-width" : { multi: "<length> | <percentage> | <number> | auto", max: 4 },
3600 "border-left" : "<border-width> || <border-style> || <color>",
3601 "border-left-color" : "<color> | inherit",
3602 "border-left-style" : "<border-style>",
3603 "border-left-width" : "<border-width>",
3604 "border-radius" : function(expression) {
3607 numeric = "<length> | <percentage>",
3614 while (expression.hasNext() && count < max) {
3615 valid = ValidationTypes.isAny(expression, numeric);
3618 if (expression.peek() == "/" && count > 1 && !slash) {
3629 if (expression.hasNext()) {
3630 part = expression.next();
3632 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
3634 throw new ValidationError("Expected (<'border-radius'>) but found '" + part + "'.", part.line, part.col);
3638 "border-right" : "<border-width> || <border-style> || <color>",
3639 "border-right-color" : "<color> | inherit",
3640 "border-right-style" : "<border-style>",
3641 "border-right-width" : "<border-width>",
3642 "border-spacing" : { multi: "<length> | inherit", max: 2 },
3643 "border-style" : { multi: "<border-style>", max: 4 },
3644 "border-top" : "<border-width> || <border-style> || <color>",
3645 "border-top-color" : "<color> | inherit",
3646 "border-top-left-radius" : "<x-one-radius>",
3647 "border-top-right-radius" : "<x-one-radius>",
3648 "border-top-style" : "<border-style>",
3649 "border-top-width" : "<border-width>",
3650 "border-width" : { multi: "<border-width>", max: 4 },
3651 "bottom" : "<margin-width> | inherit",
3652 "box-align" : "start | end | center | baseline | stretch", //http://www.w3.org/TR/2009/WD-css3-flexbox-20090723/
3653 "box-decoration-break" : "slice |clone",
3654 "box-direction" : "normal | reverse | inherit",
3655 "box-flex" : "<number>",
3656 "box-flex-group" : "<integer>",
3657 "box-lines" : "single | multiple",
3658 "box-ordinal-group" : "<integer>",
3659 "box-orient" : "horizontal | vertical | inline-axis | block-axis | inherit",
3660 "box-pack" : "start | end | center | justify",
3661 "box-shadow" : function (expression) {
3665 if (!ValidationTypes.isAny(expression, "none")) {
3666 Validation.multiProperty("<shadow>", expression, true, Infinity);
3668 if (expression.hasNext()) {
3669 part = expression.next();
3670 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
3674 "box-sizing" : "content-box | border-box | inherit",
3675 "break-after" : "auto | always | avoid | left | right | page | column | avoid-page | avoid-column",
3676 "break-before" : "auto | always | avoid | left | right | page | column | avoid-page | avoid-column",
3677 "break-inside" : "auto | avoid | avoid-page | avoid-column",
3680 "caption-side" : "top | bottom | inherit",
3681 "clear" : "none | right | left | both | inherit",
3683 "color" : "<color> | inherit",
3684 "color-profile" : 1,
3685 "column-count" : "<integer> | auto", //http://www.w3.org/TR/css3-multicol/
3686 "column-fill" : "auto | balance",
3687 "column-gap" : "<length> | normal",
3688 "column-rule" : "<border-width> || <border-style> || <color>",
3689 "column-rule-color" : "<color>",
3690 "column-rule-style" : "<border-style>",
3691 "column-rule-width" : "<border-width>",
3692 "column-span" : "none | all",
3693 "column-width" : "<length> | auto",
3696 "counter-increment" : 1,
3697 "counter-reset" : 1,
3698 "crop" : "<shape> | auto",
3699 "cue" : "cue-after | cue-before | inherit",
3705 "direction" : "ltr | rtl | inherit",
3706 "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",
3707 "dominant-baseline" : 1,
3708 "drop-initial-after-adjust" : "central | middle | after-edge | text-after-edge | ideographic | alphabetic | mathematical | <percentage> | <length>",
3709 "drop-initial-after-align" : "baseline | use-script | before-edge | text-before-edge | after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | mathematical",
3710 "drop-initial-before-adjust" : "before-edge | text-before-edge | central | middle | hanging | mathematical | <percentage> | <length>",
3711 "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",
3712 "drop-initial-size" : "auto | line | <length> | <percentage>",
3713 "drop-initial-value" : "initial | <integer>",
3716 "elevation" : "<angle> | below | level | above | higher | lower | inherit",
3717 "empty-cells" : "show | hide | inherit",
3721 "fit" : "fill | hidden | meet | slice",
3723 "float" : "left | right | none | inherit",
3727 "font-size" : "<absolute-size> | <relative-size> | <length> | <percentage> | inherit",
3728 "font-size-adjust" : "<number> | none | inherit",
3729 "font-stretch" : "normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded | inherit",
3730 "font-style" : "normal | italic | oblique | inherit",
3731 "font-variant" : "normal | small-caps | inherit",
3732 "font-weight" : "normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | inherit",
3735 "grid-cell-stacking" : "columns | rows | layer",
3738 "grid-column-align" : "start | end | center | stretch",
3739 "grid-column-sizing" : 1,
3740 "grid-column-span" : "<integer>",
3741 "grid-flow" : "none | rows | columns",
3742 "grid-layer" : "<integer>",
3745 "grid-row-align" : "start | end | center | stretch",
3746 "grid-row-span" : "<integer>",
3747 "grid-row-sizing" : 1,
3750 "hanging-punctuation" : 1,
3751 "height" : "<margin-width> | inherit",
3752 "hyphenate-after" : "<integer> | auto",
3753 "hyphenate-before" : "<integer> | auto",
3754 "hyphenate-character" : "<string> | auto",
3755 "hyphenate-lines" : "no-limit | <integer>",
3756 "hyphenate-resource" : 1,
3757 "hyphens" : "none | manual | auto",
3761 "image-orientation" : "angle | auto",
3762 "image-rendering" : 1,
3763 "image-resolution" : 1,
3764 "inline-box-align" : "initial | last | <integer>",
3767 "left" : "<margin-width> | inherit",
3768 "letter-spacing" : "<length> | normal | inherit",
3769 "line-height" : "<number> | <length> | <percentage> | normal | inherit",
3770 "line-break" : "auto | loose | normal | strict",
3771 "line-stacking" : 1,
3772 "line-stacking-ruby" : "exclude-ruby | include-ruby",
3773 "line-stacking-shift" : "consider-shifts | disregard-shifts",
3774 "line-stacking-strategy" : "inline-line-height | block-line-height | max-height | grid-height",
3776 "list-style-image" : "<uri> | none | inherit",
3777 "list-style-position" : "inside | outside | inherit",
3778 "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",
3781 "margin" : { multi: "<margin-width> | inherit", max: 4 },
3782 "margin-bottom" : "<margin-width> | inherit",
3783 "margin-left" : "<margin-width> | inherit",
3784 "margin-right" : "<margin-width> | inherit",
3785 "margin-top" : "<margin-width> | inherit",
3790 "marquee-direction" : 1,
3791 "marquee-play-count" : 1,
3792 "marquee-speed" : 1,
3793 "marquee-style" : 1,
3794 "max-height" : "<length> | <percentage> | none | inherit",
3795 "max-width" : "<length> | <percentage> | none | inherit",
3796 "min-height" : "<length> | <percentage> | inherit",
3797 "min-width" : "<length> | <percentage> | inherit",
3808 "opacity" : "<number> | inherit",
3809 "orphans" : "<integer> | inherit",
3811 "outline-color" : "<color> | invert | inherit",
3812 "outline-offset" : 1,
3813 "outline-style" : "<border-style> | inherit",
3814 "outline-width" : "<border-width> | inherit",
3815 "overflow" : "visible | hidden | scroll | auto | inherit",
3816 "overflow-style" : 1,
3821 "padding" : { multi: "<padding-width> | inherit", max: 4 },
3822 "padding-bottom" : "<padding-width> | inherit",
3823 "padding-left" : "<padding-width> | inherit",
3824 "padding-right" : "<padding-width> | inherit",
3825 "padding-top" : "<padding-width> | inherit",
3827 "page-break-after" : "auto | always | avoid | left | right | inherit",
3828 "page-break-before" : "auto | always | avoid | left | right | inherit",
3829 "page-break-inside" : "auto | avoid | inherit",
3835 "perspective-origin" : 1,
3840 "position" : "static | relative | absolute | fixed | inherit",
3841 "presentation-level" : 1,
3842 "punctuation-trim" : 1,
3848 "rendering-intent" : 1,
3854 "right" : "<margin-width> | inherit",
3856 "rotation-point" : 1,
3858 "ruby-overhang" : 1,
3859 "ruby-position" : 1,
3864 "speak" : "normal | none | spell-out | inherit",
3865 "speak-header" : "once | always | inherit",
3866 "speak-numeral" : "digits | continuous | inherit",
3867 "speak-punctuation" : "code | none | inherit",
3873 "table-layout" : "auto | fixed | inherit",
3874 "tab-size" : "<integer> | <length>",
3878 "target-position" : 1,
3879 "text-align" : "left | right | center | justify | inherit" ,
3880 "text-align-last" : 1,
3881 "text-decoration" : 1,
3882 "text-emphasis" : 1,
3884 "text-indent" : "<length> | <percentage> | inherit",
3885 "text-justify" : "auto | none | inter-word | inter-ideograph | inter-cluster | distribute | kashida",
3887 "text-overflow" : 1,
3888 "text-rendering" : "auto | optimizeSpeed | optimizeLegibility | geometricPrecision | inherit",
3890 "text-transform" : "capitalize | uppercase | lowercase | none | inherit",
3891 "text-wrap" : "normal | none | avoid",
3892 "top" : "<margin-width> | inherit",
3894 "transform-origin" : 1,
3895 "transform-style" : 1,
3897 "transition-delay" : 1,
3898 "transition-duration" : 1,
3899 "transition-property" : 1,
3900 "transition-timing-function" : 1,
3903 "unicode-bidi" : "normal | embed | bidi-override | inherit",
3904 "user-modify" : "read-only | read-write | write-only | inherit",
3905 "user-select" : "none | text | toggle | element | elements | all | inherit",
3908 "vertical-align" : "<percentage> | <length> | baseline | sub | super | top | text-top | middle | bottom | text-bottom | inherit",
3909 "visibility" : "visible | hidden | collapse | inherit",
3910 "voice-balance" : 1,
3911 "voice-duration" : 1,
3914 "voice-pitch-range" : 1,
3921 "white-space" : "normal | pre | nowrap | pre-wrap | pre-line | inherit",
3922 "white-space-collapse" : 1,
3923 "widows" : "<integer> | inherit",
3924 "width" : "<length> | <percentage> | auto | inherit" ,
3925 "word-break" : "normal | keep-all | break-all",
3926 "word-spacing" : "<length> | normal | inherit",
3930 "z-index" : "<integer> | auto | inherit",
3931 "zoom" : "<number> | <percentage> | normal"
3933 /*global SyntaxUnit, Parser*/
3935 * Represents a selector combinator (whitespace, +, >).
3936 * @namespace parserlib.css
3937 * @class PropertyName
3938 * @extends parserlib.util.SyntaxUnit
3940 * @param {String} text The text representation of the unit.
3941 * @param {String} hack The type of IE hack applied ("*", "_", or null).
3942 * @param {int} line The line of text on which the unit resides.
3943 * @param {int} col The column of text on which the unit resides.
3945 function PropertyName(text, hack, line, col){
3947 SyntaxUnit.call(this, text, line, col, Parser.PROPERTY_NAME_TYPE);
3950 * The type of IE hack applied ("*", "_", or null).
3958 PropertyName.prototype = new SyntaxUnit();
3959 PropertyName.prototype.constructor = PropertyName;
3960 PropertyName.prototype.toString = function(){
3961 return (this.hack ? this.hack : "") + this.text;
3964 /*global SyntaxUnit, Parser*/
3966 * Represents a single part of a CSS property value, meaning that it represents
3967 * just everything single part between ":" and ";". If there are multiple values
3968 * separated by commas, this type represents just one of the values.
3969 * @param {String[]} parts An array of value parts making up this value.
3970 * @param {int} line The line of text on which the unit resides.
3971 * @param {int} col The column of text on which the unit resides.
3972 * @namespace parserlib.css
3973 * @class PropertyValue
3974 * @extends parserlib.util.SyntaxUnit
3977 function PropertyValue(parts, line, col){
3979 SyntaxUnit.call(this, parts.join(" "), line, col, Parser.PROPERTY_VALUE_TYPE);
3982 * The parts that make up the selector.
3990 PropertyValue.prototype = new SyntaxUnit();
3991 PropertyValue.prototype.constructor = PropertyValue;
3994 /*global SyntaxUnit, Parser*/
3996 * A utility class that allows for easy iteration over the various parts of a
3998 * @param {parserlib.css.PropertyValue} value The property value to iterate over.
3999 * @namespace parserlib.css
4000 * @class PropertyValueIterator
4003 function PropertyValueIterator(value){
4014 * The parts that make up the value.
4019 this._parts = value.parts;
4022 * Keeps track of bookmarks along the way.
4030 * Holds the original property value.
4031 * @type parserlib.css.PropertyValue
4039 * Returns the total number of parts in the value.
4040 * @return {int} The total number of parts in the value.
4043 PropertyValueIterator.prototype.count = function(){
4044 return this._parts.length;
4048 * Indicates if the iterator is positioned at the first item.
4049 * @return {Boolean} True if positioned at first item, false if not.
4052 PropertyValueIterator.prototype.isFirst = function(){
4053 return this._i === 0;
4057 * Indicates if there are more parts of the property value.
4058 * @return {Boolean} True if there are more parts, false if not.
4061 PropertyValueIterator.prototype.hasNext = function(){
4062 return (this._i < this._parts.length);
4066 * Marks the current spot in the iteration so it can be restored to
4071 PropertyValueIterator.prototype.mark = function(){
4072 this._marks.push(this._i);
4076 * Returns the next part of the property value or null if there is no next
4077 * part. Does not move the internal counter forward.
4078 * @return {parserlib.css.PropertyValuePart} The next part of the property value or null if there is no next
4082 PropertyValueIterator.prototype.peek = function(count){
4083 return this.hasNext() ? this._parts[this._i + (count || 0)] : null;
4087 * Returns the next part of the property value or null if there is no next
4089 * @return {parserlib.css.PropertyValuePart} The next part of the property value or null if there is no next
4093 PropertyValueIterator.prototype.next = function(){
4094 return this.hasNext() ? this._parts[this._i++] : null;
4098 * Returns the previous part of the property value or null if there is no
4100 * @return {parserlib.css.PropertyValuePart} The previous part of the
4101 * property value or null if there is no next part.
4104 PropertyValueIterator.prototype.previous = function(){
4105 return this._i > 0 ? this._parts[--this._i] : null;
4109 * Restores the last saved bookmark.
4113 PropertyValueIterator.prototype.restore = function(){
4114 if (this._marks.length){
4115 this._i = this._marks.pop();
4120 /*global SyntaxUnit, Parser, Colors*/
4122 * Represents a single part of a CSS property value, meaning that it represents
4123 * just one part of the data between ":" and ";".
4124 * @param {String} text The text representation of the unit.
4125 * @param {int} line The line of text on which the unit resides.
4126 * @param {int} col The column of text on which the unit resides.
4127 * @namespace parserlib.css
4128 * @class PropertyValuePart
4129 * @extends parserlib.util.SyntaxUnit
4132 function PropertyValuePart(text, line, col){
4134 SyntaxUnit.call(this, text, line, col, Parser.PROPERTY_VALUE_PART_TYPE);
4137 * Indicates the type of value unit.
4141 this.type = "unknown";
4143 //figure out what type of data it is
4147 //it is a measurement?
4148 if (/^([+\-]?[\d\.]+)([a-z]+)$/i.test(text)){ //dimension
4149 this.type = "dimension";
4150 this.value = +RegExp.$1;
4151 this.units = RegExp.$2;
4153 //try to narrow down
4154 switch(this.units.toLowerCase()){
4166 this.type = "length";
4172 this.type = "angle";
4182 this.type = "frequency";
4187 this.type = "resolution";
4194 } else if (/^([+\-]?[\d\.]+)%$/i.test(text)){ //percentage
4195 this.type = "percentage";
4196 this.value = +RegExp.$1;
4197 } else if (/^([+\-]?[\d\.]+)%$/i.test(text)){ //percentage
4198 this.type = "percentage";
4199 this.value = +RegExp.$1;
4200 } else if (/^([+\-]?\d+)$/i.test(text)){ //integer
4201 this.type = "integer";
4202 this.value = +RegExp.$1;
4203 } else if (/^([+\-]?[\d\.]+)$/i.test(text)){ //number
4204 this.type = "number";
4205 this.value = +RegExp.$1;
4207 } else if (/^#([a-f0-9]{3,6})/i.test(text)){ //hexcolor
4208 this.type = "color";
4210 if (temp.length == 3){
4211 this.red = parseInt(temp.charAt(0)+temp.charAt(0),16);
4212 this.green = parseInt(temp.charAt(1)+temp.charAt(1),16);
4213 this.blue = parseInt(temp.charAt(2)+temp.charAt(2),16);
4215 this.red = parseInt(temp.substring(0,2),16);
4216 this.green = parseInt(temp.substring(2,4),16);
4217 this.blue = parseInt(temp.substring(4,6),16);
4219 } else if (/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i.test(text)){ //rgb() color with absolute numbers
4220 this.type = "color";
4221 this.red = +RegExp.$1;
4222 this.green = +RegExp.$2;
4223 this.blue = +RegExp.$3;
4224 } else if (/^rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i.test(text)){ //rgb() color with percentages
4225 this.type = "color";
4226 this.red = +RegExp.$1 * 255 / 100;
4227 this.green = +RegExp.$2 * 255 / 100;
4228 this.blue = +RegExp.$3 * 255 / 100;
4229 } else if (/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d\.]+)\s*\)/i.test(text)){ //rgba() color with absolute numbers
4230 this.type = "color";
4231 this.red = +RegExp.$1;
4232 this.green = +RegExp.$2;
4233 this.blue = +RegExp.$3;
4234 this.alpha = +RegExp.$4;
4235 } else if (/^rgba\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([\d\.]+)\s*\)/i.test(text)){ //rgba() color with percentages
4236 this.type = "color";
4237 this.red = +RegExp.$1 * 255 / 100;
4238 this.green = +RegExp.$2 * 255 / 100;
4239 this.blue = +RegExp.$3 * 255 / 100;
4240 this.alpha = +RegExp.$4;
4241 } else if (/^hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i.test(text)){ //hsl()
4242 this.type = "color";
4243 this.hue = +RegExp.$1;
4244 this.saturation = +RegExp.$2 / 100;
4245 this.lightness = +RegExp.$3 / 100;
4246 } else if (/^hsla\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([\d\.]+)\s*\)/i.test(text)){ //hsla() color with percentages
4247 this.type = "color";
4248 this.hue = +RegExp.$1;
4249 this.saturation = +RegExp.$2 / 100;
4250 this.lightness = +RegExp.$3 / 100;
4251 this.alpha = +RegExp.$4;
4252 } else if (/^url\(["']?([^\)"']+)["']?\)/i.test(text)){ //URI
4254 this.uri = RegExp.$1;
4255 } else if (/^([^\(]+)\(/i.test(text)){
4256 this.type = "function";
4257 this.name = RegExp.$1;
4259 } else if (/^["'][^"']*["']/.test(text)){ //string
4260 this.type = "string";
4261 this.value = eval(text);
4262 } else if (Colors[text.toLowerCase()]){ //named color
4263 this.type = "color";
4264 temp = Colors[text.toLowerCase()].substring(1);
4265 this.red = parseInt(temp.substring(0,2),16);
4266 this.green = parseInt(temp.substring(2,4),16);
4267 this.blue = parseInt(temp.substring(4,6),16);
4268 } else if (/^[\,\/]$/.test(text)){
4269 this.type = "operator";
4271 } else if (/^[a-z\-\u0080-\uFFFF][a-z0-9\-\u0080-\uFFFF]*$/i.test(text)){
4272 this.type = "identifier";
4278 PropertyValuePart.prototype = new SyntaxUnit();
4279 PropertyValuePart.prototype.constructor = PropertyValuePart;
4282 * Create a new syntax unit based solely on the given token.
4283 * Convenience method for creating a new syntax unit when
4284 * it represents a single token instead of multiple.
4285 * @param {Object} token The token object to represent.
4286 * @return {parserlib.css.PropertyValuePart} The object representing the token.
4290 PropertyValuePart.fromToken = function(token){
4291 return new PropertyValuePart(token.value, token.startLine, token.startCol);
4300 Pseudos.ELEMENT = 1;
4303 Pseudos.isElement = function(pseudo){
4304 return pseudo.indexOf("::") === 0 || Pseudos[pseudo.toLowerCase()] == Pseudos.ELEMENT;
4306 /*global SyntaxUnit, Parser, Specificity*/
4308 * Represents an entire single selector, including all parts but not
4309 * including multiple selectors (those separated by commas).
4310 * @namespace parserlib.css
4312 * @extends parserlib.util.SyntaxUnit
4314 * @param {Array} parts Array of selectors parts making up this selector.
4315 * @param {int} line The line of text on which the unit resides.
4316 * @param {int} col The column of text on which the unit resides.
4318 function Selector(parts, line, col){
4320 SyntaxUnit.call(this, parts.join(" "), line, col, Parser.SELECTOR_TYPE);
4323 * The parts that make up the selector.
4330 * The specificity of the selector.
4331 * @type parserlib.css.Specificity
4332 * @property specificity
4334 this.specificity = Specificity.calculate(this);
4338 Selector.prototype = new SyntaxUnit();
4339 Selector.prototype.constructor = Selector;
4342 /*global SyntaxUnit, Parser*/
4344 * Represents a single part of a selector string, meaning a single set of
4345 * element name and modifiers. This does not include combinators such as
4346 * spaces, +, >, etc.
4347 * @namespace parserlib.css
4348 * @class SelectorPart
4349 * @extends parserlib.util.SyntaxUnit
4351 * @param {String} elementName The element name in the selector or null
4352 * if there is no element name.
4353 * @param {Array} modifiers Array of individual modifiers for the element.
4354 * May be empty if there are none.
4355 * @param {String} text The text representation of the unit.
4356 * @param {int} line The line of text on which the unit resides.
4357 * @param {int} col The column of text on which the unit resides.
4359 function SelectorPart(elementName, modifiers, text, line, col){
4361 SyntaxUnit.call(this, text, line, col, Parser.SELECTOR_PART_TYPE);
4364 * The tag name of the element to which this part
4365 * of the selector affects.
4367 * @property elementName
4369 this.elementName = elementName;
4372 * The parts that come after the element name, such as class names, IDs,
4373 * pseudo classes/elements, etc.
4375 * @property modifiers
4377 this.modifiers = modifiers;
4381 SelectorPart.prototype = new SyntaxUnit();
4382 SelectorPart.prototype.constructor = SelectorPart;
4385 /*global SyntaxUnit, Parser*/
4387 * Represents a selector modifier string, meaning a class name, element name,
4388 * element ID, pseudo rule, etc.
4389 * @namespace parserlib.css
4390 * @class SelectorSubPart
4391 * @extends parserlib.util.SyntaxUnit
4393 * @param {String} text The text representation of the unit.
4394 * @param {String} type The type of selector modifier.
4395 * @param {int} line The line of text on which the unit resides.
4396 * @param {int} col The column of text on which the unit resides.
4398 function SelectorSubPart(text, type, line, col){
4400 SyntaxUnit.call(this, text, line, col, Parser.SELECTOR_SUB_PART_TYPE);
4403 * The type of modifier.
4410 * Some subparts have arguments, this represents them.
4418 SelectorSubPart.prototype = new SyntaxUnit();
4419 SelectorSubPart.prototype.constructor = SelectorSubPart;
4422 /*global Pseudos, SelectorPart*/
4424 * Represents a selector's specificity.
4425 * @namespace parserlib.css
4426 * @class Specificity
4428 * @param {int} a Should be 1 for inline styles, zero for stylesheet styles
4429 * @param {int} b Number of ID selectors
4430 * @param {int} c Number of classes and pseudo classes
4431 * @param {int} d Number of element names and pseudo elements
4433 function Specificity(a, b, c, d){
4440 Specificity.prototype = {
4441 constructor: Specificity,
4444 * Compare this specificity to another.
4445 * @param {Specificity} other The other specificity to compare to.
4446 * @return {int} -1 if the other specificity is larger, 1 if smaller, 0 if equal.
4449 compare: function(other){
4450 var comps = ["a", "b", "c", "d"],
4453 for (i=0, len=comps.length; i < len; i++){
4454 if (this[comps[i]] < other[comps[i]]){
4456 } else if (this[comps[i]] > other[comps[i]]){
4465 * Creates a numeric value for the specificity.
4466 * @return {int} The numeric value for the specificity.
4469 valueOf: function(){
4470 return (this.a * 1000) + (this.b * 100) + (this.c * 10) + this.d;
4474 * Returns a string representation for specificity.
4475 * @return {String} The string representation of specificity.
4478 toString: function(){
4479 return this.a + "," + this.b + "," + this.c + "," + this.d;
4485 * Calculates the specificity of the given selector.
4486 * @param {parserlib.css.Selector} The selector to calculate specificity for.
4487 * @return {parserlib.css.Specificity} The specificity of the selector.
4491 Specificity.calculate = function(selector){
4497 function updateValues(part){
4500 elementName = part.elementName ? part.elementName.text : "",
4503 if (elementName && elementName.charAt(elementName.length-1) != "*") {
4507 for (i=0, len=part.modifiers.length; i < len; i++){
4508 modifier = part.modifiers[i];
4509 switch(modifier.type){
4520 if (Pseudos.isElement(modifier.text)){
4528 for (j=0, num=modifier.args.length; j < num; j++){
4529 updateValues(modifier.args[j]);
4535 for (i=0, len=selector.parts.length; i < len; i++){
4536 part = selector.parts[i];
4538 if (part instanceof SelectorPart){
4543 return new Specificity(0, b, c, d);
4546 /*global Tokens, TokenStreamBase*/
4548 var h = /^[0-9a-fA-F]$/,
4549 nonascii = /^[\u0080-\uFFFF]$/,
4550 nl = /\n|\r\n|\r|\f/;
4552 //-----------------------------------------------------------------------------
4554 //-----------------------------------------------------------------------------
4557 function isHexDigit(c){
4558 return c !== null && h.test(c);
4561 function isDigit(c){
4562 return c !== null && /\d/.test(c);
4565 function isWhitespace(c){
4566 return c !== null && /\s/.test(c);
4569 function isNewLine(c){
4570 return c !== null && nl.test(c);
4573 function isNameStart(c){
4574 return c !== null && (/[a-z_\u0080-\uFFFF\\]/i.test(c));
4577 function isNameChar(c){
4578 return c !== null && (isNameStart(c) || /[0-9\-\\]/.test(c));
4581 function isIdentStart(c){
4582 return c !== null && (isNameStart(c) || /\-\\/.test(c));
4585 function mix(receiver, supplier){
4586 for (var prop in supplier){
4587 if (supplier.hasOwnProperty(prop)){
4588 receiver[prop] = supplier[prop];
4594 //-----------------------------------------------------------------------------
4596 //-----------------------------------------------------------------------------
4600 * A token stream that produces CSS tokens.
4601 * @param {String|Reader} input The source of text to tokenize.
4603 * @class TokenStream
4604 * @namespace parserlib.css
4606 function TokenStream(input){
4607 TokenStreamBase.call(this, input, Tokens);
4610 TokenStream.prototype = mix(new TokenStreamBase(), {
4613 * Overrides the TokenStreamBase method of the same name
4614 * to produce CSS tokens.
4615 * @param {variant} channel The name of the channel to use
4616 * for the next token.
4617 * @return {Object} A token object representing the next token.
4621 _getToken: function(channel){
4624 reader = this._reader,
4626 startLine = reader.getLine(),
4627 startCol = reader.getCol();
4643 if(reader.peek() == "*"){
4644 token = this.commentToken(c, startLine, startCol);
4646 token = this.charToken(c, startLine, startCol);
4664 if(reader.peek() == "="){
4665 token = this.comparisonToken(c, startLine, startCol);
4667 token = this.charToken(c, startLine, startCol);
4678 token = this.stringToken(c, startLine, startCol);
4687 if (isNameChar(reader.peek())){
4688 token = this.hashToken(c, startLine, startCol);
4690 token = this.charToken(c, startLine, startCol);
4702 if (isDigit(reader.peek())){
4703 token = this.numberToken(c, startLine, startCol);
4705 token = this.charToken(c, startLine, startCol);
4718 if (reader.peek() == "-"){ //could be closing HTML-style comment
4719 token = this.htmlCommentEndToken(c, startLine, startCol);
4720 } else if (isNameStart(reader.peek())){
4721 token = this.identOrFunctionToken(c, startLine, startCol);
4723 token = this.charToken(c, startLine, startCol);
4733 token = this.importantToken(c, startLine, startCol);
4737 * Any at-keyword or CHAR
4740 token = this.atRuleToken(c, startLine, startCol);
4749 token = this.notToken(c, startLine, startCol);
4758 token = this.htmlCommentStartToken(c, startLine, startCol);
4769 if (reader.peek() == "+"){
4770 token = this.unicodeRangeToken(c, startLine, startCol);
4788 token = this.numberToken(c, startLine, startCol);
4795 if (isWhitespace(c)){
4796 token = this.whitespaceToken(c, startLine, startCol);
4803 if (isIdentStart(c)){
4804 token = this.identOrFunctionToken(c, startLine, startCol);
4813 token = this.charToken(c, startLine, startCol);
4823 //make sure this token is wanted
4824 //TODO: check channel
4828 if (!token && c === null){
4829 token = this.createToken(Tokens.EOF,null,startLine,startCol);
4835 //-------------------------------------------------------------------------
4836 // Methods to create tokens
4837 //-------------------------------------------------------------------------
4840 * Produces a token based on available data and the current
4841 * reader position information. This method is called by other
4842 * private methods to create tokens and is never called directly.
4843 * @param {int} tt The token type.
4844 * @param {String} value The text value of the token.
4845 * @param {int} startLine The beginning line for the character.
4846 * @param {int} startCol The beginning column for the character.
4847 * @param {Object} options (Optional) Specifies a channel property
4848 * to indicate that a different channel should be scanned
4849 * and/or a hide property indicating that the token should
4851 * @return {Object} A token object.
4852 * @method createToken
4854 createToken: function(tt, value, startLine, startCol, options){
4855 var reader = this._reader;
4856 options = options || {};
4861 channel: options.channel,
4862 hide: options.hide || false,
4863 startLine: startLine,
4865 endLine: reader.getLine(),
4866 endCol: reader.getCol()
4870 //-------------------------------------------------------------------------
4871 // Methods to create specific tokens
4872 //-------------------------------------------------------------------------
4875 * Produces a token for any at-rule. If the at-rule is unknown, then
4876 * the token is for a single "@" character.
4877 * @param {String} first The first character for the token.
4878 * @param {int} startLine The beginning line for the character.
4879 * @param {int} startCol The beginning column for the character.
4880 * @return {Object} A token object.
4881 * @method atRuleToken
4883 atRuleToken: function(first, startLine, startCol){
4885 reader = this._reader,
4892 * First, mark where we are. There are only four @ rules,
4893 * so anything else is really just an invalid token.
4894 * Basically, if this doesn't match one of the known @
4895 * rules, just return '@' as an unknown token and allow
4896 * parsing to continue after that point.
4900 //try to find the at-keyword
4901 ident = this.readName();
4902 rule = first + ident;
4903 tt = Tokens.type(rule.toLowerCase());
4905 //if it's not valid, use the first character only and reset the reader
4906 if (tt == Tokens.CHAR || tt == Tokens.UNKNOWN){
4907 if (rule.length > 1){
4908 tt = Tokens.UNKNOWN_SYM;
4916 return this.createToken(tt, rule, startLine, startCol);
4920 * Produces a character token based on the given character
4921 * and location in the stream. If there's a special (non-standard)
4922 * token name, this is used; otherwise CHAR is used.
4923 * @param {String} c The character for the token.
4924 * @param {int} startLine The beginning line for the character.
4925 * @param {int} startCol The beginning column for the character.
4926 * @return {Object} A token object.
4929 charToken: function(c, startLine, startCol){
4930 var tt = Tokens.type(c);
4936 return this.createToken(tt, c, startLine, startCol);
4940 * Produces a character token based on the given character
4941 * and location in the stream. If there's a special (non-standard)
4942 * token name, this is used; otherwise CHAR is used.
4943 * @param {String} first The first character for the token.
4944 * @param {int} startLine The beginning line for the character.
4945 * @param {int} startCol The beginning column for the character.
4946 * @return {Object} A token object.
4947 * @method commentToken
4949 commentToken: function(first, startLine, startCol){
4950 var reader = this._reader,
4951 comment = this.readComment(first);
4953 return this.createToken(Tokens.COMMENT, comment, startLine, startCol);
4957 * Produces a comparison token based on the given character
4958 * and location in the stream. The next character must be
4959 * read and is already known to be an equals sign.
4960 * @param {String} c The character for the token.
4961 * @param {int} startLine The beginning line for the character.
4962 * @param {int} startCol The beginning column for the character.
4963 * @return {Object} A token object.
4964 * @method comparisonToken
4966 comparisonToken: function(c, startLine, startCol){
4967 var reader = this._reader,
4968 comparison = c + reader.read(),
4969 tt = Tokens.type(comparison) || Tokens.CHAR;
4971 return this.createToken(tt, comparison, startLine, startCol);
4975 * Produces a hash token based on the specified information. The
4976 * first character provided is the pound sign (#) and then this
4977 * method reads a name afterward.
4978 * @param {String} first The first character (#) in the hash name.
4979 * @param {int} startLine The beginning line for the character.
4980 * @param {int} startCol The beginning column for the character.
4981 * @return {Object} A token object.
4984 hashToken: function(first, startLine, startCol){
4985 var reader = this._reader,
4986 name = this.readName(first);
4988 return this.createToken(Tokens.HASH, name, startLine, startCol);
4992 * Produces a CDO or CHAR token based on the specified information. The
4993 * first character is provided and the rest is read by the function to determine
4994 * the correct token to create.
4995 * @param {String} first The first character in the token.
4996 * @param {int} startLine The beginning line for the character.
4997 * @param {int} startCol The beginning column for the character.
4998 * @return {Object} A token object.
4999 * @method htmlCommentStartToken
5001 htmlCommentStartToken: function(first, startLine, startCol){
5002 var reader = this._reader,
5006 text += reader.readCount(3);
5008 if (text == "<!--"){
5009 return this.createToken(Tokens.CDO, text, startLine, startCol);
5012 return this.charToken(first, startLine, startCol);
5017 * Produces a CDC or CHAR token based on the specified information. The
5018 * first character is provided and the rest is read by the function to determine
5019 * the correct token to create.
5020 * @param {String} first The first character in the token.
5021 * @param {int} startLine The beginning line for the character.
5022 * @param {int} startCol The beginning column for the character.
5023 * @return {Object} A token object.
5024 * @method htmlCommentEndToken
5026 htmlCommentEndToken: function(first, startLine, startCol){
5027 var reader = this._reader,
5031 text += reader.readCount(2);
5034 return this.createToken(Tokens.CDC, text, startLine, startCol);
5037 return this.charToken(first, startLine, startCol);
5042 * Produces an IDENT or FUNCTION token based on the specified information. The
5043 * first character is provided and the rest is read by the function to determine
5044 * the correct token to create.
5045 * @param {String} first The first character in the identifier.
5046 * @param {int} startLine The beginning line for the character.
5047 * @param {int} startCol The beginning column for the character.
5048 * @return {Object} A token object.
5049 * @method identOrFunctionToken
5051 identOrFunctionToken: function(first, startLine, startCol){
5052 var reader = this._reader,
5053 ident = this.readName(first),
5056 //if there's a left paren immediately after, it's a URI or function
5057 if (reader.peek() == "("){
5058 ident += reader.read();
5059 if (ident.toLowerCase() == "url("){
5061 ident = this.readURI(ident);
5063 //didn't find a valid URL or there's no closing paren
5064 if (ident.toLowerCase() == "url("){
5065 tt = Tokens.FUNCTION;
5068 tt = Tokens.FUNCTION;
5070 } else if (reader.peek() == ":"){ //might be an IE function
5072 //IE-specific functions always being with progid:
5073 if (ident.toLowerCase() == "progid"){
5074 ident += reader.readTo("(");
5075 tt = Tokens.IE_FUNCTION;
5079 return this.createToken(tt, ident, startLine, startCol);
5083 * Produces an IMPORTANT_SYM or CHAR token based on the specified information. The
5084 * first character is provided and the rest is read by the function to determine
5085 * the correct token to create.
5086 * @param {String} first The first character in the token.
5087 * @param {int} startLine The beginning line for the character.
5088 * @param {int} startCol The beginning column for the character.
5089 * @return {Object} A token object.
5090 * @method importantToken
5092 importantToken: function(first, startLine, startCol){
5093 var reader = this._reader,
5104 //there can be a comment in here
5107 //if the next character isn't a star, then this isn't a valid !important token
5108 if (reader.peek() != "*"){
5111 temp = this.readComment(c);
5112 if (temp === ""){ //broken!
5116 } else if (isWhitespace(c)){
5117 important += c + this.readWhitespace();
5118 } else if (/i/i.test(c)){
5119 temp = reader.readCount(8);
5120 if (/mportant/i.test(temp)){
5121 important += c + temp;
5122 tt = Tokens.IMPORTANT_SYM;
5133 if (tt == Tokens.CHAR){
5135 return this.charToken(first, startLine, startCol);
5137 return this.createToken(tt, important, startLine, startCol);
5144 * Produces a NOT or CHAR token based on the specified information. The
5145 * first character is provided and the rest is read by the function to determine
5146 * the correct token to create.
5147 * @param {String} first The first character in the token.
5148 * @param {int} startLine The beginning line for the character.
5149 * @param {int} startCol The beginning column for the character.
5150 * @return {Object} A token object.
5153 notToken: function(first, startLine, startCol){
5154 var reader = this._reader,
5158 text += reader.readCount(4);
5160 if (text.toLowerCase() == ":not("){
5161 return this.createToken(Tokens.NOT, text, startLine, startCol);
5164 return this.charToken(first, startLine, startCol);
5169 * Produces a number token based on the given character
5170 * and location in the stream. This may return a token of
5171 * NUMBER, EMS, EXS, LENGTH, ANGLE, TIME, FREQ, DIMENSION,
5173 * @param {String} first The first character for the token.
5174 * @param {int} startLine The beginning line for the character.
5175 * @param {int} startCol The beginning column for the character.
5176 * @return {Object} A token object.
5177 * @method numberToken
5179 numberToken: function(first, startLine, startCol){
5180 var reader = this._reader,
5181 value = this.readNumber(first),
5186 if (isIdentStart(c)){
5187 ident = this.readName(reader.read());
5190 if (/^em$|^ex$|^px$|^gd$|^rem$|^vw$|^vh$|^vm$|^ch$|^cm$|^mm$|^in$|^pt$|^pc$/i.test(ident)){
5192 } else if (/^deg|^rad$|^grad$/i.test(ident)){
5194 } else if (/^ms$|^s$/i.test(ident)){
5196 } else if (/^hz$|^khz$/i.test(ident)){
5198 } else if (/^dpi$|^dpcm$/i.test(ident)){
5199 tt = Tokens.RESOLUTION;
5201 tt = Tokens.DIMENSION;
5204 } else if (c == "%"){
5205 value += reader.read();
5206 tt = Tokens.PERCENTAGE;
5209 return this.createToken(tt, value, startLine, startCol);
5213 * Produces a string token based on the given character
5214 * and location in the stream. Since strings may be indicated
5215 * by single or double quotes, a failure to match starting
5216 * and ending quotes results in an INVALID token being generated.
5217 * The first character in the string is passed in and then
5218 * the rest are read up to and including the final quotation mark.
5219 * @param {String} first The first character in the string.
5220 * @param {int} startLine The beginning line for the character.
5221 * @param {int} startCol The beginning column for the character.
5222 * @return {Object} A token object.
5223 * @method stringToken
5225 stringToken: function(first, startLine, startCol){
5228 reader = this._reader,
5236 //if the delimiter is found with an escapement, we're done.
5237 if (c == delim && prev != "\\"){
5241 //if there's a newline without an escapement, it's an invalid string
5242 if (isNewLine(reader.peek()) && c != "\\"){
5243 tt = Tokens.INVALID;
5247 //save previous and get next
5252 //if c is null, that means we're out of input and the string was never closed
5254 tt = Tokens.INVALID;
5257 return this.createToken(tt, string, startLine, startCol);
5260 unicodeRangeToken: function(first, startLine, startCol){
5261 var reader = this._reader,
5266 //then it should be a unicode range
5267 if (reader.peek() == "+"){
5269 value += reader.read();
5270 value += this.readUnicodeRangePart(true);
5272 //ensure there's an actual unicode range here
5273 if (value.length == 2){
5277 tt = Tokens.UNICODE_RANGE;
5279 //if there's a ? in the first part, there can't be a second part
5280 if (value.indexOf("?") == -1){
5282 if (reader.peek() == "-"){
5284 temp = reader.read();
5285 temp += this.readUnicodeRangePart(false);
5287 //if there's not another value, back up and just take the first
5288 if (temp.length == 1){
5299 return this.createToken(tt, value, startLine, startCol);
5303 * Produces a S token based on the specified information. Since whitespace
5304 * may have multiple characters, this consumes all whitespace characters
5305 * into a single token.
5306 * @param {String} first The first character in the token.
5307 * @param {int} startLine The beginning line for the character.
5308 * @param {int} startCol The beginning column for the character.
5309 * @return {Object} A token object.
5310 * @method whitespaceToken
5312 whitespaceToken: function(first, startLine, startCol){
5313 var reader = this._reader,
5314 value = first + this.readWhitespace();
5315 return this.createToken(Tokens.S, value, startLine, startCol);
5321 //-------------------------------------------------------------------------
5322 // Methods to read values from the string stream
5323 //-------------------------------------------------------------------------
5325 readUnicodeRangePart: function(allowQuestionMark){
5326 var reader = this._reader,
5330 //first read hex digits
5331 while(isHexDigit(c) && part.length < 6){
5337 //then read question marks if allowed
5338 if (allowQuestionMark){
5339 while(c == "?" && part.length < 6){
5346 //there can't be any other characters after this point
5351 readWhitespace: function(){
5352 var reader = this._reader,
5356 while(isWhitespace(c)){
5364 readNumber: function(first){
5365 var reader = this._reader,
5367 hasDot = (first == "."),
5373 number += reader.read();
5374 } else if (c == "."){
5379 number += reader.read();
5390 readString: function(){
5391 var reader = this._reader,
5392 delim = reader.read(),
5401 //if the delimiter is found with an escapement, we're done.
5402 if (c == delim && prev != "\\"){
5406 //if there's a newline without an escapement, it's an invalid string
5407 if (isNewLine(reader.peek()) && c != "\\"){
5412 //save previous and get next
5417 //if c is null, that means we're out of input and the string was never closed
5424 readURI: function(first){
5425 var reader = this._reader,
5432 //skip whitespace before
5433 while(c && isWhitespace(c)){
5439 if (c == "'" || c == "\""){
5440 inner = this.readString();
5442 inner = this.readURL();
5447 //skip whitespace after
5448 while(c && isWhitespace(c)){
5453 //if there was no inner value or the next character isn't closing paren, it's not a URI
5454 if (inner === "" || c != ")"){
5458 uri += inner + reader.read();
5463 readURL: function(){
5464 var reader = this._reader,
5468 //TODO: Check for escape and nonascii
5469 while (/^[!#$%&\\*-~]$/.test(c)){
5470 url += reader.read();
5477 readName: function(first){
5478 var reader = this._reader,
5479 ident = first || "",
5484 ident += this.readEscape(reader.read());
5486 } else if(c && isNameChar(c)){
5487 ident += reader.read();
5497 readEscape: function(first){
5498 var reader = this._reader,
5499 cssEscape = first || "",
5505 cssEscape += reader.read();
5507 } while(c && isHexDigit(c) && ++i < 6);
5510 if (cssEscape.length == 3 && /\s/.test(c) ||
5511 cssEscape.length == 7 || cssEscape.length == 1){
5517 return cssEscape + c;
5520 readComment: function(first){
5521 var reader = this._reader,
5522 comment = first || "",
5529 //look for end of comment
5530 if (comment.length > 2 && c == "*" && reader.peek() == "/"){
5531 comment += reader.read();
5550 * The following token names are defined in CSS3 Grammar: http://www.w3.org/TR/css3-syntax/#lexical
5553 //HTML-style comments
5558 { name: "S", whitespace: true/*, channel: "ws"*/},
5559 { name: "COMMENT", comment: true, hide: true, channel: "comment" },
5561 //attribute equality
5562 { name: "INCLUDES", text: "~="},
5563 { name: "DASHMATCH", text: "|="},
5564 { name: "PREFIXMATCH", text: "^="},
5565 { name: "SUFFIXMATCH", text: "$="},
5566 { name: "SUBSTRINGMATCH", text: "*="},
5574 { name: "IMPORT_SYM", text: "@import"},
5575 { name: "PAGE_SYM", text: "@page"},
5576 { name: "MEDIA_SYM", text: "@media"},
5577 { name: "FONT_FACE_SYM", text: "@font-face"},
5578 { name: "CHARSET_SYM", text: "@charset"},
5579 { name: "NAMESPACE_SYM", text: "@namespace"},
5580 { name: "UNKNOWN_SYM" },
5581 //{ name: "ATKEYWORD"},
5584 { name: "KEYFRAMES_SYM", text: [ "@keyframes", "@-webkit-keyframes", "@-moz-keyframes", "@-ms-keyframes" ] },
5587 { name: "IMPORTANT_SYM"},
5594 { name: "DIMENSION"},
5595 { name: "PERCENTAGE"},
5600 { name: "FUNCTION"},
5603 { name: "UNICODE_RANGE"},
5606 * The following token names are defined in CSS3 Selectors: http://www.w3.org/TR/css3-selectors/#selector-syntax
5613 { name: "PLUS", text: "+" },
5614 { name: "GREATER", text: ">"},
5615 { name: "COMMA", text: ","},
5616 { name: "TILDE", text: "~"},
5622 * Defined in CSS3 Paged Media
5624 { name: "TOPLEFTCORNER_SYM", text: "@top-left-corner"},
5625 { name: "TOPLEFT_SYM", text: "@top-left"},
5626 { name: "TOPCENTER_SYM", text: "@top-center"},
5627 { name: "TOPRIGHT_SYM", text: "@top-right"},
5628 { name: "TOPRIGHTCORNER_SYM", text: "@top-right-corner"},
5629 { name: "BOTTOMLEFTCORNER_SYM", text: "@bottom-left-corner"},
5630 { name: "BOTTOMLEFT_SYM", text: "@bottom-left"},
5631 { name: "BOTTOMCENTER_SYM", text: "@bottom-center"},
5632 { name: "BOTTOMRIGHT_SYM", text: "@bottom-right"},
5633 { name: "BOTTOMRIGHTCORNER_SYM", text: "@bottom-right-corner"},
5634 { name: "LEFTTOP_SYM", text: "@left-top"},
5635 { name: "LEFTMIDDLE_SYM", text: "@left-middle"},
5636 { name: "LEFTBOTTOM_SYM", text: "@left-bottom"},
5637 { name: "RIGHTTOP_SYM", text: "@right-top"},
5638 { name: "RIGHTMIDDLE_SYM", text: "@right-middle"},
5639 { name: "RIGHTBOTTOM_SYM", text: "@right-bottom"},
5642 * The following token names are defined in CSS3 Media Queries: http://www.w3.org/TR/css3-mediaqueries/#syntax
5644 /*{ name: "MEDIA_ONLY", state: "media"},
5645 { name: "MEDIA_NOT", state: "media"},
5646 { name: "MEDIA_AND", state: "media"},*/
5647 { name: "RESOLUTION", state: "media"},
5650 * The following token names are not defined in any CSS specification but are used by the lexer.
5653 //not a real token, but useful for stupid IE filters
5654 { name: "IE_FUNCTION" },
5656 //part of CSS3 grammar but not the Flex code
5660 //Not defined as tokens, but might as well be
5726 Tokens.UNKNOWN = -1;
5727 Tokens.unshift({name:"EOF"});
5728 for (var i=0, len = Tokens.length; i < len; i++){
5729 nameMap.push(Tokens[i].name);
5730 Tokens[Tokens[i].name] = i;
5731 if (Tokens[i].text){
5732 if (Tokens[i].text instanceof Array){
5733 for (var j=0; j < Tokens[i].text.length; j++){
5734 typeMap[Tokens[i].text[j]] = i;
5737 typeMap[Tokens[i].text] = i;
5742 Tokens.name = function(tt){
5746 Tokens.type = function(c){
5747 return typeMap[c] || -1;
5755 //This file will likely change a lot! Very experimental!
5756 /*global Properties, ValidationTypes, ValidationError, PropertyValueIterator */
5759 validate: function(property, value){
5762 var name = property.toString().toLowerCase(),
5763 parts = value.parts,
5764 expression = new PropertyValueIterator(value),
5765 spec = Properties[name],
5776 if (name.indexOf("-") !== 0){ //vendor prefixed are ok
5777 throw new ValidationError("Unknown property '" + property + "'.", property.line, property.col);
5779 } else if (typeof spec != "number"){
5782 if (typeof spec == "string"){
5783 if (spec.indexOf("||") > -1) {
5784 this.groupProperty(spec, expression);
5786 this.singleProperty(spec, expression, 1);
5789 } else if (spec.multi) {
5790 this.multiProperty(spec.multi, expression, spec.comma, spec.max || Infinity);
5791 } else if (typeof spec == "function") {
5799 singleProperty: function(types, expression, max, partial) {
5802 value = expression.value,
5806 while (expression.hasNext() && count < max) {
5807 result = ValidationTypes.isAny(expression, types);
5815 if (expression.hasNext() && !expression.isFirst()) {
5816 part = expression.peek();
5817 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
5819 throw new ValidationError("Expected (" + types + ") but found '" + value + "'.", value.line, value.col);
5821 } else if (expression.hasNext()) {
5822 part = expression.next();
5823 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
5828 multiProperty: function (types, expression, comma, max) {
5831 value = expression.value,
5836 while(expression.hasNext() && !result && count < max) {
5837 if (ValidationTypes.isAny(expression, types)) {
5839 if (!expression.hasNext()) {
5843 if (expression.peek() == ",") {
5844 part = expression.next();
5856 if (expression.hasNext() && !expression.isFirst()) {
5857 part = expression.peek();
5858 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
5860 part = expression.previous();
5861 if (comma && part == ",") {
5862 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
5864 throw new ValidationError("Expected (" + types + ") but found '" + value + "'.", value.line, value.col);
5868 } else if (expression.hasNext()) {
5869 part = expression.next();
5870 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
5875 groupProperty: function (types, expression, comma) {
5878 value = expression.value,
5879 typeCount = types.split("||").length,
5880 groups = { count: 0 },
5885 while(expression.hasNext() && !result) {
5886 name = ValidationTypes.isAnyOfGroup(expression, types);
5897 if (groups.count == typeCount || !expression.hasNext()) {
5907 if (partial && expression.hasNext()) {
5908 part = expression.peek();
5909 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
5911 throw new ValidationError("Expected (" + types + ") but found '" + value + "'.", value.line, value.col);
5913 } else if (expression.hasNext()) {
5914 part = expression.next();
5915 throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
5923 * Type to use when a validation error occurs.
5924 * @class ValidationError
5925 * @namespace parserlib.util
5927 * @param {String} message The error message.
5928 * @param {int} line The line at which the error occurred.
5929 * @param {int} col The column at which the error occurred.
5931 function ValidationError(message, line, col){
5934 * The column at which the error occurred.
5941 * The line at which the error occurred.
5948 * The text representation of the unit.
5952 this.message = message;
5956 //inherit from Error
5957 ValidationError.prototype = new Error();
5958 //This file will likely change a lot! Very experimental!
5959 /*global Properties, Validation, ValidationError, PropertyValueIterator, console*/
5960 var ValidationTypes = {
5962 isLiteral: function (part, literals) {
5963 var text = part.text.toString().toLowerCase(),
5964 args = literals.split(" | "),
5965 i, len, found = false;
5967 for (i=0,len=args.length; i < len && !found; i++){
5968 if (text == args[i].toLowerCase()){
5976 isSimple: function(type) {
5977 return !!this.simple[type];
5980 isComplex: function(type) {
5981 return !!this.complex[type];
5985 * Determines if the next part(s) of the given expression
5986 * are any of the given types.
5988 isAny: function (expression, types) {
5989 var args = types.split(" | "),
5990 i, len, found = false;
5992 for (i=0,len=args.length; i < len && !found && expression.hasNext(); i++){
5993 found = this.isType(expression, args[i]);
6000 * Determines if the next part(s) of the given expresion
6001 * are one of a group.
6003 isAnyOfGroup: function(expression, types) {
6004 var args = types.split(" || "),
6005 i, len, found = false;
6007 for (i=0,len=args.length; i < len && !found; i++){
6008 found = this.isType(expression, args[i]);
6011 return found ? args[i-1] : false;
6015 * Determines if the next part(s) of the given expression
6016 * are of a given type.
6018 isType: function (expression, type) {
6019 var part = expression.peek(),
6022 if (type.charAt(0) != "<") {
6023 result = this.isLiteral(part, type);
6027 } else if (this.simple[type]) {
6028 result = this.simple[type](part);
6033 result = this.complex[type](expression);
6043 "<absolute-size>": function(part){
6044 return ValidationTypes.isLiteral(part, "xx-small | x-small | small | medium | large | x-large | xx-large");
6047 "<attachment>": function(part){
6048 return ValidationTypes.isLiteral(part, "scroll | fixed | local");
6051 "<attr>": function(part){
6052 return part.type == "function" && part.name == "attr";
6055 "<bg-image>": function(part){
6056 return this["<image>"](part) || this["<gradient>"](part) || part == "none";
6059 "<gradient>": function(part) {
6060 return part.type == "function" && /^(?:\-(?:ms|moz|o|webkit)\-)?(?:repeating\-)?(?:radial\-|linear\-)?gradient/i.test(part);
6063 "<box>": function(part){
6064 return ValidationTypes.isLiteral(part, "padding-box | border-box | content-box");
6067 "<content>": function(part){
6068 return part.type == "function" && part.name == "content";
6071 "<relative-size>": function(part){
6072 return ValidationTypes.isLiteral(part, "smaller | larger");
6076 "<ident>": function(part){
6077 return part.type == "identifier";
6080 "<length>": function(part){
6081 return part.type == "length" || part.type == "number" || part.type == "integer" || part == "0";
6084 "<color>": function(part){
6085 return part.type == "color" || part == "transparent";
6088 "<number>": function(part){
6089 return part.type == "number" || this["<integer>"](part);
6092 "<integer>": function(part){
6093 return part.type == "integer";
6096 "<line>": function(part){
6097 return part.type == "integer";
6100 "<angle>": function(part){
6101 return part.type == "angle";
6104 "<uri>": function(part){
6105 return part.type == "uri";
6108 "<image>": function(part){
6109 return this["<uri>"](part);
6112 "<percentage>": function(part){
6113 return part.type == "percentage" || part == "0";
6116 "<border-width>": function(part){
6117 return this["<length>"](part) || ValidationTypes.isLiteral(part, "thin | medium | thick");
6120 "<border-style>": function(part){
6121 return ValidationTypes.isLiteral(part, "none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset");
6124 "<margin-width>": function(part){
6125 return this["<length>"](part) || this["<percentage>"](part) || ValidationTypes.isLiteral(part, "auto");
6128 "<padding-width>": function(part){
6129 return this["<length>"](part) || this["<percentage>"](part);
6132 "<shape>": function(part){
6133 return part.type == "function" && (part.name == "rect" || part.name == "inset-rect");
6136 "<time>": function(part) {
6137 return part.type == "time";
6143 "<bg-position>": function(expression){
6146 numeric = "<percentage> | <length>",
6147 xDir = "left | center | right",
6148 yDir = "top | center | bottom",
6154 [ left | center | right | top | bottom | <percentage> | <length> ]
6156 [ left | center | right | <percentage> | <length> ]
6157 [ top | center | bottom | <percentage> | <length> ]
6159 [ center | [ left | right ] [ <percentage> | <length> ]? ] &&
6160 [ center | [ top | bottom ] [ <percentage> | <length> ]? ]
6165 if (ValidationTypes.isAny(expression, "top | bottom")) {
6170 if (ValidationTypes.isAny(expression, numeric)){
6171 if (expression.hasNext()){
6172 result = ValidationTypes.isAny(expression, numeric + " | " + yDir);
6174 } else if (ValidationTypes.isAny(expression, xDir)){
6175 if (expression.hasNext()){
6177 //two- or three-part
6178 if (ValidationTypes.isAny(expression, yDir)){
6181 ValidationTypes.isAny(expression, numeric);
6183 } else if (ValidationTypes.isAny(expression, numeric)){
6185 //could also be two-part, so check the next part
6186 if (ValidationTypes.isAny(expression, yDir)){
6187 ValidationTypes.isAny(expression, numeric);
6200 "<bg-size>": function(expression){
6201 //<bg-size> = [ <length> | <percentage> | auto ]{1,2} | cover | contain
6204 numeric = "<percentage> | <length> | auto",
6208 if (ValidationTypes.isAny(expression, "cover | contain")) {
6210 } else if (ValidationTypes.isAny(expression, numeric)) {
6212 ValidationTypes.isAny(expression, numeric);
6218 "<repeat-style>": function(expression){
6219 //repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2}
6221 values = "repeat | space | round | no-repeat",
6224 if (expression.hasNext()){
6225 part = expression.next();
6227 if (ValidationTypes.isLiteral(part, "repeat-x | repeat-y")) {
6229 } else if (ValidationTypes.isLiteral(part, values)) {
6232 if (expression.hasNext() && ValidationTypes.isLiteral(expression.peek(), values)) {
6242 "<shadow>": function(expression) {
6243 //inset? && [ <length>{2,4} && <color>? ]
6250 if (expression.hasNext()) {
6252 if (ValidationTypes.isAny(expression, "inset")){
6256 if (ValidationTypes.isAny(expression, "<color>")) {
6260 while (ValidationTypes.isAny(expression, "<length>") && count < 4) {
6265 if (expression.hasNext()) {
6267 ValidationTypes.isAny(expression, "<color>");
6271 ValidationTypes.isAny(expression, "inset");
6276 result = (count >= 2 && count <= 4);
6283 "<x-one-radius>": function(expression) {
6284 //[ <length> | <percentage> ] [ <length> | <percentage> ]?
6287 numeric = "<length> | <percentage>",
6290 if (ValidationTypes.isAny(expression, numeric)){
6293 ValidationTypes.isAny(expression, numeric);
6304 Combinator :Combinator,
6306 PropertyName :PropertyName,
6307 PropertyValue :PropertyValue,
6308 PropertyValuePart :PropertyValuePart,
6309 MediaFeature :MediaFeature,
6310 MediaQuery :MediaQuery,
6312 SelectorPart :SelectorPart,
6313 SelectorSubPart :SelectorSubPart,
6314 Specificity :Specificity,
6315 TokenStream :TokenStream,
6317 ValidationError :ValidationError
6324 * Main CSSLint object.
6327 * @extends parserlib.util.EventTarget
6329 /*global parserlib, Reporter*/
6330 var CSSLint = (function(){
6334 api = new parserlib.util.EventTarget();
6336 api.version = "0.9.8";
6338 //-------------------------------------------------------------------------
6340 //-------------------------------------------------------------------------
6343 * Adds a new rule to the engine.
6344 * @param {Object} rule The rule to add.
6347 api.addRule = function(rule){
6349 rules[rule.id] = rule;
6353 * Clears all rule from the engine.
6354 * @method clearRules
6356 api.clearRules = function(){
6361 * Returns the rule objects.
6362 * @return An array of rule objects.
6365 api.getRules = function(){
6366 return [].concat(rules).sort(function(a,b){
6367 return a.id > b.id ? 1 : 0;
6371 //-------------------------------------------------------------------------
6373 //-------------------------------------------------------------------------
6376 * Adds a new formatter to the engine.
6377 * @param {Object} formatter The formatter to add.
6378 * @method addFormatter
6380 api.addFormatter = function(formatter) {
6381 // formatters.push(formatter);
6382 formatters[formatter.id] = formatter;
6386 * Retrieves a formatter for use.
6387 * @param {String} formatId The name of the format to retrieve.
6388 * @return {Object} The formatter or undefined.
6389 * @method getFormatter
6391 api.getFormatter = function(formatId){
6392 return formatters[formatId];
6396 * Formats the results in a particular format for a single file.
6397 * @param {Object} result The results returned from CSSLint.verify().
6398 * @param {String} filename The filename for which the results apply.
6399 * @param {String} formatId The name of the formatter to use.
6400 * @param {Object} options (Optional) for special output handling.
6401 * @return {String} A formatted string for the results.
6404 api.format = function(results, filename, formatId, options) {
6405 var formatter = this.getFormatter(formatId),
6409 result = formatter.startFormat();
6410 result += formatter.formatResults(results, filename, options || {});
6411 result += formatter.endFormat();
6418 * Indicates if the given format is supported.
6419 * @param {String} formatId The ID of the format to check.
6420 * @return {Boolean} True if the format exists, false if not.
6423 api.hasFormat = function(formatId){
6424 return formatters.hasOwnProperty(formatId);
6427 //-------------------------------------------------------------------------
6429 //-------------------------------------------------------------------------
6432 * Starts the verification process for the given CSS text.
6433 * @param {String} text The CSS text to verify.
6434 * @param {Object} ruleset (Optional) List of rules to apply. If null, then
6435 * all rules are used. If a rule has a value of 1 then it's a warning,
6436 * a value of 2 means it's an error.
6437 * @return {Object} Results of the verification.
6440 api.verify = function(text, ruleset){
6447 parser = new parserlib.css.Parser({ starHack: true, ieFilters: true,
6448 underscoreHack: true, strict: false });
6450 lines = text.replace(/\n\r?/g, "$split$").split('$split$');
6455 ruleset[rules[i++].id] = 1; //by default, everything is a warning
6459 reporter = new Reporter(lines, ruleset);
6461 ruleset.errors = 2; //always report parsing errors as errors
6463 if(ruleset.hasOwnProperty(i)){
6465 rules[i].init(parser, reporter);
6471 //capture most horrible error type
6475 reporter.error("Fatal error, cannot continue: " + ex.message, ex.line, ex.col, {});
6479 messages : reporter.messages,
6480 stats : reporter.stats
6483 //sort by line numbers, rollups at the bottom
6484 report.messages.sort(function (a, b){
6485 if (a.rollup && !b.rollup){
6487 } else if (!a.rollup && b.rollup){
6490 return a.line - b.line;
6497 //-------------------------------------------------------------------------
6499 //-------------------------------------------------------------------------
6507 * An instance of Report is used to report results of the
6508 * verification back to the main API.
6511 * @param {String[]} lines The text lines of the source.
6512 * @param {Object} ruleset The set of rules to work with, including if
6513 * they are errors or warnings.
6515 function Reporter(lines, ruleset){
6518 * List of messages being reported.
6519 * @property messages
6525 * List of statistics being reported.
6532 * Lines of code being reported on. Used to provide contextual information
6540 * Information about the rules. Used to determine whether an issue is an
6545 this.ruleset = ruleset;
6548 Reporter.prototype = {
6550 //restore constructor
6551 constructor: Reporter,
6555 * @param {String} message The message to store.
6556 * @param {int} line The line number.
6557 * @param {int} col The column number.
6558 * @param {Object} rule The rule this message relates to.
6561 error: function(message, line, col, rule){
6562 this.messages.push({
6567 evidence: this.lines[line-1],
6573 * Report an warning.
6574 * @param {String} message The message to store.
6575 * @param {int} line The line number.
6576 * @param {int} col The column number.
6577 * @param {Object} rule The rule this message relates to.
6579 * @deprecated Use report instead.
6581 warn: function(message, line, col, rule){
6582 this.report(message, line, col, rule);
6587 * @param {String} message The message to store.
6588 * @param {int} line The line number.
6589 * @param {int} col The column number.
6590 * @param {Object} rule The rule this message relates to.
6593 report: function(message, line, col, rule){
6594 this.messages.push({
6595 type : this.ruleset[rule.id] == 2 ? "error" : "warning",
6599 evidence: this.lines[line-1],
6605 * Report some informational text.
6606 * @param {String} message The message to store.
6607 * @param {int} line The line number.
6608 * @param {int} col The column number.
6609 * @param {Object} rule The rule this message relates to.
6612 info: function(message, line, col, rule){
6613 this.messages.push({
6618 evidence: this.lines[line-1],
6624 * Report some rollup error information.
6625 * @param {String} message The message to store.
6626 * @param {Object} rule The rule this message relates to.
6627 * @method rollupError
6629 rollupError: function(message, rule){
6630 this.messages.push({
6639 * Report some rollup warning information.
6640 * @param {String} message The message to store.
6641 * @param {Object} rule The rule this message relates to.
6642 * @method rollupWarn
6644 rollupWarn: function(message, rule){
6645 this.messages.push({
6654 * Report a statistic.
6655 * @param {String} name The name of the stat to store.
6656 * @param {Variant} value The value of the stat.
6659 stat: function(name, value){
6660 this.stats[name] = value;
6664 //expose for testing purposes
6665 CSSLint._Reporter = Reporter;
6670 * Utility functions that make life easier.
6674 * Adds all properties from supplier onto receiver,
6675 * overwriting if the same name already exists on
6677 * @param {Object} The object to receive the properties.
6678 * @param {Object} The object to provide the properties.
6679 * @return {Object} The receiver
6681 mix: function(receiver, supplier){
6684 for (prop in supplier){
6685 if (supplier.hasOwnProperty(prop)){
6686 receiver[prop] = supplier[prop];
6694 * Polyfill for array indexOf() method.
6695 * @param {Array} values The array to search.
6696 * @param {Variant} value The value to search for.
6697 * @return {int} The index of the value if found, -1 if not.
6699 indexOf: function(values, value){
6700 if (values.indexOf){
6701 return values.indexOf(value);
6703 for (var i=0, len=values.length; i < len; i++){
6704 if (values[i] === value){
6713 * Polyfill for array forEach() method.
6714 * @param {Array} values The array to operate on.
6715 * @param {Function} func The function to call on each item.
6718 forEach: function(values, func) {
6719 if (values.forEach){
6720 return values.forEach(func);
6722 for (var i=0, len=values.length; i < len; i++){
6723 func(values[i], i, values);
6730 * Rule: Don't use adjoining classes (.foo.bar).
6735 id: "adjoining-classes",
6736 name: "Disallow adjoining classes",
6737 desc: "Don't use adjoining classes.",
6741 init: function(parser, reporter){
6743 parser.addListener("startrule", function(event){
6744 var selectors = event.selectors,
6751 for (i=0; i < selectors.length; i++){
6752 selector = selectors[i];
6753 for (j=0; j < selector.parts.length; j++){
6754 part = selector.parts[j];
6755 if (part.type == parser.SELECTOR_PART_TYPE){
6757 for (k=0; k < part.modifiers.length; k++){
6758 modifier = part.modifiers[k];
6759 if (modifier.type == "class"){
6762 if (classCount > 1){
6763 reporter.report("Don't use adjoining classes.", part.line, part.col, rule);
6776 * Rule: Don't use width or height when using padding or border.
6782 name: "Beware of broken box size",
6783 desc: "Don't use width or height when using padding or border.",
6787 init: function(parser, reporter){
6797 heightProperties = {
6802 "padding-bottom": 1,
6807 function startRule(){
6813 if (properties.height){
6814 for (prop in heightProperties){
6815 if (heightProperties.hasOwnProperty(prop) && properties[prop]){
6817 //special case for padding
6818 if (!(prop == "padding" && properties[prop].value.parts.length === 2 && properties[prop].value.parts[0].value === 0)){
6819 reporter.report("Using height with " + prop + " can sometimes make elements larger than you expect.", properties[prop].line, properties[prop].col, rule);
6825 if (properties.width){
6826 for (prop in widthProperties){
6827 if (widthProperties.hasOwnProperty(prop) && properties[prop]){
6829 if (!(prop == "padding" && properties[prop].value.parts.length === 2 && properties[prop].value.parts[1].value === 0)){
6830 reporter.report("Using width with " + prop + " can sometimes make elements larger than you expect.", properties[prop].line, properties[prop].col, rule);
6837 parser.addListener("startrule", startRule);
6838 parser.addListener("startfontface", startRule);
6839 parser.addListener("startpage", startRule);
6840 parser.addListener("startpagemargin", startRule);
6841 parser.addListener("startkeyframerule", startRule);
6843 parser.addListener("property", function(event){
6844 var name = event.property.text.toLowerCase();
6846 if (heightProperties[name] || widthProperties[name]){
6847 if (!/^0\S*$/.test(event.value) && !(name == "border" && event.value == "none")){
6848 properties[name] = { line: event.property.line, col: event.property.col, value: event.value };
6851 if (name == "width" || name == "height"){
6852 properties[name] = 1;
6858 parser.addListener("endrule", endRule);
6859 parser.addListener("endfontface", endRule);
6860 parser.addListener("endpage", endRule);
6861 parser.addListener("endpagemargin", endRule);
6862 parser.addListener("endkeyframerule", endRule);
6869 * Rule: box-sizing doesn't work in IE6 and IE7.
6875 name: "Disallow use of box-sizing",
6876 desc: "The box-sizing properties isn't supported in IE6 and IE7.",
6877 browsers: "IE6, IE7",
6878 tags: ["Compatibility"],
6881 init: function(parser, reporter){
6884 parser.addListener("property", function(event){
6885 var name = event.property.text.toLowerCase();
6887 if (name == "box-sizing"){
6888 reporter.report("The box-sizing property isn't supported in IE6 and IE7.", event.line, event.col, rule);
6895 * Rule: Include all compatible vendor prefixes to reach a wider
6902 id: "compatible-vendor-prefixes",
6903 name: "Require compatible vendor prefixes",
6904 desc: "Include all compatible vendor prefixes to reach a wider range of users.",
6908 init: function (parser, reporter) {
6917 arrayPush = Array.prototype.push,
6920 // See http://peter.sh/experiments/vendor-prefixed-css-property-overview/ for details
6921 compatiblePrefixes = {
6922 "animation" : "webkit moz ms",
6923 "animation-delay" : "webkit moz ms",
6924 "animation-direction" : "webkit moz ms",
6925 "animation-duration" : "webkit moz ms",
6926 "animation-fill-mode" : "webkit moz ms",
6927 "animation-iteration-count" : "webkit moz ms",
6928 "animation-name" : "webkit moz ms",
6929 "animation-play-state" : "webkit moz ms",
6930 "animation-timing-function" : "webkit moz ms",
6931 "appearance" : "webkit moz",
6932 "border-end" : "webkit moz",
6933 "border-end-color" : "webkit moz",
6934 "border-end-style" : "webkit moz",
6935 "border-end-width" : "webkit moz",
6936 "border-image" : "webkit moz o",
6937 "border-radius" : "webkit moz",
6938 "border-start" : "webkit moz",
6939 "border-start-color" : "webkit moz",
6940 "border-start-style" : "webkit moz",
6941 "border-start-width" : "webkit moz",
6942 "box-align" : "webkit moz ms",
6943 "box-direction" : "webkit moz ms",
6944 "box-flex" : "webkit moz ms",
6945 "box-lines" : "webkit ms",
6946 "box-ordinal-group" : "webkit moz ms",
6947 "box-orient" : "webkit moz ms",
6948 "box-pack" : "webkit moz ms",
6949 "box-sizing" : "webkit moz",
6950 "box-shadow" : "webkit moz",
6951 "column-count" : "webkit moz ms",
6952 "column-gap" : "webkit moz ms",
6953 "column-rule" : "webkit moz ms",
6954 "column-rule-color" : "webkit moz ms",
6955 "column-rule-style" : "webkit moz ms",
6956 "column-rule-width" : "webkit moz ms",
6957 "column-width" : "webkit moz ms",
6958 "hyphens" : "epub moz",
6959 "line-break" : "webkit ms",
6960 "margin-end" : "webkit moz",
6961 "margin-start" : "webkit moz",
6962 "marquee-speed" : "webkit wap",
6963 "marquee-style" : "webkit wap",
6964 "padding-end" : "webkit moz",
6965 "padding-start" : "webkit moz",
6966 "tab-size" : "moz o",
6967 "text-size-adjust" : "webkit ms",
6968 "transform" : "webkit moz ms o",
6969 "transform-origin" : "webkit moz ms o",
6970 "transition" : "webkit moz o ms",
6971 "transition-delay" : "webkit moz o ms",
6972 "transition-duration" : "webkit moz o ms",
6973 "transition-property" : "webkit moz o ms",
6974 "transition-timing-function" : "webkit moz o ms",
6975 "user-modify" : "webkit moz",
6976 "user-select" : "webkit moz ms",
6977 "word-break" : "epub ms",
6978 "writing-mode" : "epub ms"
6982 for (prop in compatiblePrefixes) {
6983 if (compatiblePrefixes.hasOwnProperty(prop)) {
6985 prefixed = compatiblePrefixes[prop].split(' ');
6986 for (i = 0, len = prefixed.length; i < len; i++) {
6987 variations.push('-' + prefixed[i] + '-' + prop);
6989 compatiblePrefixes[prop] = variations;
6990 arrayPush.apply(applyTo, variations);
6993 parser.addListener("startrule", function () {
6997 parser.addListener("property", function (event) {
6998 var name = event.property;
6999 if (CSSLint.Util.indexOf(applyTo, name.text) > -1) {
7000 properties.push(name);
7004 parser.addListener("endrule", function (event) {
7005 if (!properties.length) {
7009 var propertyGroups = {},
7019 propertiesSpecified;
7021 for (i = 0, len = properties.length; i < len; i++) {
7022 name = properties[i];
7024 for (prop in compatiblePrefixes) {
7025 if (compatiblePrefixes.hasOwnProperty(prop)) {
7026 variations = compatiblePrefixes[prop];
7027 if (CSSLint.Util.indexOf(variations, name.text) > -1) {
7028 if (!propertyGroups[prop]) {
7029 propertyGroups[prop] = {
7030 full : variations.slice(0),
7035 if (CSSLint.Util.indexOf(propertyGroups[prop].actual, name.text) === -1) {
7036 propertyGroups[prop].actual.push(name.text);
7037 propertyGroups[prop].actualNodes.push(name);
7044 for (prop in propertyGroups) {
7045 if (propertyGroups.hasOwnProperty(prop)) {
7046 value = propertyGroups[prop];
7048 actual = value.actual;
7050 if (full.length > actual.length) {
7051 for (i = 0, len = full.length; i < len; i++) {
7053 if (CSSLint.Util.indexOf(actual, item) === -1) {
7054 propertiesSpecified = (actual.length === 1) ? actual[0] : (actual.length == 2) ? actual.join(" and ") : actual.join(", ");
7055 reporter.report("The property " + item + " is compatible with " + propertiesSpecified + " and should be included as well.", value.actualNodes[0].line, value.actualNodes[0].col, rule);
7066 * Rule: Certain properties don't play well with certain display values.
7067 * - float should not be used with inline-block
7068 * - height, width, margin-top, margin-bottom, float should not be used with inline
7069 * - vertical-align should not be used with block
7070 * - margin, float should not be used with table-*
7076 id: "display-property-grouping",
7077 name: "Require properties appropriate for display",
7078 desc: "Certain properties shouldn't be used with certain display property values.",
7082 init: function(parser, reporter){
7085 var propertiesToCheck = {
7098 "padding-bottom": 1,
7104 function reportProperty(name, display, msg){
7105 if (properties[name]){
7106 if (typeof propertiesToCheck[name] != "string" || properties[name].value.toLowerCase() != propertiesToCheck[name]){
7107 reporter.report(msg || name + " can't be used with display: " + display + ".", properties[name].line, properties[name].col, rule);
7112 function startRule(){
7118 var display = properties.display ? properties.display.value : null;
7123 //height, width, margin-top, margin-bottom, float should not be used with inline
7124 reportProperty("height", display);
7125 reportProperty("width", display);
7126 reportProperty("margin", display);
7127 reportProperty("margin-top", display);
7128 reportProperty("margin-bottom", display);
7129 reportProperty("float", display, "display:inline has no effect on floated elements (but may be used to fix the IE6 double-margin bug).");
7133 //vertical-align should not be used with block
7134 reportProperty("vertical-align", display);
7137 case "inline-block":
7138 //float should not be used with inline-block
7139 reportProperty("float", display);
7143 //margin, float should not be used with table
7144 if (display.indexOf("table-") === 0){
7145 reportProperty("margin", display);
7146 reportProperty("margin-left", display);
7147 reportProperty("margin-right", display);
7148 reportProperty("margin-top", display);
7149 reportProperty("margin-bottom", display);
7150 reportProperty("float", display);
7153 //otherwise do nothing
7159 parser.addListener("startrule", startRule);
7160 parser.addListener("startfontface", startRule);
7161 parser.addListener("startkeyframerule", startRule);
7162 parser.addListener("startpagemargin", startRule);
7163 parser.addListener("startpage", startRule);
7165 parser.addListener("property", function(event){
7166 var name = event.property.text.toLowerCase();
7168 if (propertiesToCheck[name]){
7169 properties[name] = { value: event.value.text, line: event.property.line, col: event.property.col };
7173 parser.addListener("endrule", endRule);
7174 parser.addListener("endfontface", endRule);
7175 parser.addListener("endkeyframerule", endRule);
7176 parser.addListener("endpagemargin", endRule);
7177 parser.addListener("endpage", endRule);
7183 * Rule: Disallow duplicate background-images (using url).
7189 id: "duplicate-background-images",
7190 name: "Disallow duplicate background images",
7191 desc: "Every background-image should be unique. Use a common class for e.g. sprites.",
7195 init: function(parser, reporter){
7199 parser.addListener("property", function(event){
7200 var name = event.property.text,
7201 value = event.value,
7204 if (name.match(/background/i)) {
7205 for (i=0, len=value.parts.length; i < len; i++) {
7206 if (value.parts[i].type == 'uri') {
7207 if (typeof stack[value.parts[i].uri] === 'undefined') {
7208 stack[value.parts[i].uri] = event;
7211 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);
7220 * Rule: Duplicate properties must appear one after the other. If an already-defined
7221 * property appears somewhere else in the rule, then it's likely an error.
7227 id: "duplicate-properties",
7228 name: "Disallow duplicate properties",
7229 desc: "Duplicate properties must appear one after the other.",
7233 init: function(parser, reporter){
7238 function startRule(event){
7242 parser.addListener("startrule", startRule);
7243 parser.addListener("startfontface", startRule);
7244 parser.addListener("startpage", startRule);
7245 parser.addListener("startpagemargin", startRule);
7246 parser.addListener("startkeyframerule", startRule);
7248 parser.addListener("property", function(event){
7249 var property = event.property,
7250 name = property.text.toLowerCase();
7252 if (properties[name] && (lastProperty != name || properties[name] == event.value.text)){
7253 reporter.report("Duplicate property '" + event.property + "' found.", event.line, event.col, rule);
7256 properties[name] = event.value.text;
7257 lastProperty = name;
7266 * Rule: Style rules without any properties defined should be removed.
7273 name: "Disallow empty rules",
7274 desc: "Rules without any properties specified should be removed.",
7278 init: function(parser, reporter){
7282 parser.addListener("startrule", function(){
7286 parser.addListener("property", function(){
7290 parser.addListener("endrule", function(event){
7291 var selectors = event.selectors;
7293 reporter.report("Rule is empty.", selectors[0].line, selectors[0].col, rule);
7300 * Rule: There should be no syntax errors. (Duh.)
7307 name: "Parsing Errors",
7308 desc: "This rule looks for recoverable syntax errors.",
7312 init: function(parser, reporter){
7315 parser.addListener("error", function(event){
7316 reporter.error(event.message, event.line, event.col, rule);
7327 id: "fallback-colors",
7328 name: "Require fallback colors",
7329 desc: "For older browsers that don't support RGBA, HSL, or HSLA, provide a fallback color.",
7330 browsers: "IE6,IE7,IE8",
7333 init: function(parser, reporter){
7336 propertiesToCheck = {
7339 "background-color": 1
7343 function startRule(event){
7345 lastProperty = null;
7348 parser.addListener("startrule", startRule);
7349 parser.addListener("startfontface", startRule);
7350 parser.addListener("startpage", startRule);
7351 parser.addListener("startpagemargin", startRule);
7352 parser.addListener("startkeyframerule", startRule);
7354 parser.addListener("property", function(event){
7355 var property = event.property,
7356 name = property.text.toLowerCase(),
7357 parts = event.value.parts,
7362 if(propertiesToCheck[name]){
7364 if (parts[i].type == "color"){
7365 if ("alpha" in parts[i] || "hue" in parts[i]){
7367 if (/([^\)]+)\(/.test(parts[i])){
7368 colorType = RegExp.$1.toUpperCase();
7371 if (!lastProperty || (lastProperty.property.text.toLowerCase() != name || lastProperty.colorType != "compat")){
7372 reporter.report("Fallback " + name + " (hex or RGB) should precede " + colorType + " " + name + ".", event.line, event.col, rule);
7375 event.colorType = "compat";
7383 lastProperty = event;
7390 * Rule: You shouldn't use more than 10 floats. If you do, there's probably
7391 * room for some abstraction.
7398 name: "Disallow too many floats",
7399 desc: "This rule tests if the float property is used too many times",
7403 init: function(parser, reporter){
7407 //count how many times "float" is used
7408 parser.addListener("property", function(event){
7409 if (event.property.text.toLowerCase() == "float" &&
7410 event.value.text.toLowerCase() != "none"){
7415 //report the results
7416 parser.addListener("endstylesheet", function(){
7417 reporter.stat("floats", count);
7419 reporter.rollupWarn("Too many floats (" + count + "), you're probably using them for layout. Consider using a grid system instead.", rule);
7426 * Rule: Avoid too many @font-face declarations in the same stylesheet.
7433 name: "Don't use too many web fonts",
7434 desc: "Too many different web fonts in the same stylesheet.",
7438 init: function(parser, reporter){
7443 parser.addListener("startfontface", function(){
7447 parser.addListener("endstylesheet", function(){
7449 reporter.rollupWarn("Too many @font-face declarations (" + count + ").", rule);
7456 * Rule: You shouldn't need more than 9 font-size declarations.
7464 name: "Disallow too many font sizes",
7465 desc: "Checks the number of font-size declarations.",
7469 init: function(parser, reporter){
7473 //check for use of "font-size"
7474 parser.addListener("property", function(event){
7475 if (event.property == "font-size"){
7480 //report the results
7481 parser.addListener("endstylesheet", function(){
7482 reporter.stat("font-sizes", count);
7484 reporter.rollupWarn("Too many font-size declarations (" + count + "), abstraction needed.", rule);
7491 * Rule: When using a vendor-prefixed gradient, make sure to use them all.
7498 name: "Require all gradient definitions",
7499 desc: "When using a vendor-prefixed gradient, make sure to use them all.",
7503 init: function(parser, reporter){
7507 parser.addListener("startrule", function(){
7517 parser.addListener("property", function(event){
7519 if (/\-(moz|ms|o|webkit)(?:\-(?:linear|radial))\-gradient/i.test(event.value)){
7520 gradients[RegExp.$1] = 1;
7521 } else if (/\-webkit\-gradient/i.test(event.value)){
7522 gradients.oldWebkit = 1;
7527 parser.addListener("endrule", function(event){
7530 if (!gradients.moz){
7531 missing.push("Firefox 3.6+");
7534 if (!gradients.webkit){
7535 missing.push("Webkit (Safari 5+, Chrome)");
7538 if (!gradients.oldWebkit){
7539 missing.push("Old Webkit (Safari 4+, Chrome)");
7543 missing.push("Internet Explorer 10+");
7547 missing.push("Opera 11.1+");
7550 if (missing.length && missing.length < 5){
7551 reporter.report("Missing vendor-prefixed CSS gradients for " + missing.join(", ") + ".", event.selectors[0].line, event.selectors[0].col, rule);
7560 * Rule: Don't use IDs for selectors.
7567 name: "Disallow IDs in selectors",
7568 desc: "Selectors should not contain IDs.",
7572 init: function(parser, reporter){
7574 parser.addListener("startrule", function(event){
7575 var selectors = event.selectors,
7582 for (i=0; i < selectors.length; i++){
7583 selector = selectors[i];
7586 for (j=0; j < selector.parts.length; j++){
7587 part = selector.parts[j];
7588 if (part.type == parser.SELECTOR_PART_TYPE){
7589 for (k=0; k < part.modifiers.length; k++){
7590 modifier = part.modifiers[k];
7591 if (modifier.type == "id"){
7599 reporter.report("Don't use IDs in selectors.", selector.line, selector.col, rule);
7600 } else if (idCount > 1){
7601 reporter.report(idCount + " IDs in the selector, really?", selector.line, selector.col, rule);
7610 * Rule: Don't use @import, use <link> instead.
7617 name: "Disallow @import",
7618 desc: "Don't use @import, use <link> instead.",
7622 init: function(parser, reporter){
7625 parser.addListener("import", function(event){
7626 reporter.report("@import prevents parallel downloads, use <link> instead.", event.line, event.col, rule);
7633 * Rule: Make sure !important is not overused, this could lead to specificity
7634 * war. Display a warning on !important declarations, an error if it's
7635 * used more at least 10 times.
7642 name: "Disallow !important",
7643 desc: "Be careful when using !important declaration",
7647 init: function(parser, reporter){
7651 //warn that important is used and increment the declaration counter
7652 parser.addListener("property", function(event){
7653 if (event.important === true){
7655 reporter.report("Use of !important", event.line, event.col, rule);
7659 //if there are more than 10, show an error
7660 parser.addListener("endstylesheet", function(){
7661 reporter.stat("important", count);
7663 reporter.rollupWarn("Too many !important declarations (" + count + "), try to use less than 10 to avoid specificity issues.", rule);
7670 * Rule: Properties should be known (listed in CSS3 specification) or
7671 * be a vendor-prefixed property.
7677 id: "known-properties",
7678 name: "Require use of known properties",
7679 desc: "Properties should be known (listed in CSS specification) or be a vendor-prefixed property.",
7683 init: function(parser, reporter){
7687 "alignment-adjust": 1,
7688 "alignment-baseline": 1,
7690 "animation-delay": 1,
7691 "animation-direction": 1,
7692 "animation-duration": 1,
7693 "animation-fill-mode": 1,
7694 "animation-iteration-count": 1,
7695 "animation-name": 1,
7696 "animation-play-state": 1,
7697 "animation-timing-function": 1,
7700 "backface-visibility": 1,
7702 "background-attachment": 1,
7703 "background-break": 1,
7704 "background-clip": 1,
7705 "background-color": 1,
7706 "background-image": 1,
7707 "background-origin": 1,
7708 "background-position": 1,
7709 "background-repeat": 1,
7710 "background-size": 1,
7711 "baseline-shift": 1,
7714 "bookmark-label": 1,
7715 "bookmark-level": 1,
7716 "bookmark-state": 1,
7717 "bookmark-target": 1,
7720 "border-bottom-color": 1,
7721 "border-bottom-left-radius": 1,
7722 "border-bottom-right-radius": 1,
7723 "border-bottom-style": 1,
7724 "border-bottom-width": 1,
7725 "border-collapse": 1,
7728 "border-image-outset": 1,
7729 "border-image-repeat": 1,
7730 "border-image-slice": 1,
7731 "border-image-source": 1,
7732 "border-image-width": 1,
7734 "border-left-color": 1,
7735 "border-left-style": 1,
7736 "border-left-width": 1,
7739 "border-right-color": 1,
7740 "border-right-style": 1,
7741 "border-right-width": 1,
7742 "border-spacing": 1,
7745 "border-top-color": 1,
7746 "border-top-left-radius": 1,
7747 "border-top-right-radius": 1,
7748 "border-top-style": 1,
7749 "border-top-width": 1,
7753 "box-decoration-break": 1,
7756 "box-flex-group": 1,
7758 "box-ordinal-group": 1,
7775 "column-rule-color": 1,
7776 "column-rule-style": 1,
7777 "column-rule-width": 1,
7782 "counter-increment": 1,
7791 "dominant-baseline": 1,
7792 "drop-initial-after-adjust": 1,
7793 "drop-initial-after-align": 1,
7794 "drop-initial-before-adjust": 1,
7795 "drop-initial-before-align": 1,
7796 "drop-initial-size": 1,
7797 "drop-initial-value": 1,
7807 "font-size-adjust": 1,
7814 "hanging-punctuation": 1,
7816 "hyphenate-after": 1,
7817 "hyphenate-before": 1,
7818 "hyphenate-character": 1,
7819 "hyphenate-lines": 1,
7820 "hyphenate-resource": 1,
7823 "image-orientation": 1,
7824 "image-rendering": 1,
7825 "image-resolution": 1,
7826 "inline-box-align": 1,
7828 "letter-spacing": 1,
7831 "line-stacking-ruby": 1,
7832 "line-stacking-shift": 1,
7833 "line-stacking-strategy": 1,
7835 "list-style-image": 1,
7836 "list-style-position": 1,
7837 "list-style-type": 1,
7847 "marquee-direction": 1,
7848 "marquee-play-count": 1,
7865 "outline-offset": 1,
7869 "overflow-style": 1,
7873 "padding-bottom": 1,
7878 "page-break-after": 1,
7879 "page-break-before": 1,
7880 "page-break-inside": 1,
7886 "perspective-origin": 1,
7892 "presentation-level": 1,
7893 "punctuation-trim": 1,
7895 "rendering-intent": 1,
7903 "rotation-point": 1,
7912 "speak-punctuation": 1,
7920 "target-position": 1,
7922 "text-align-last": 1,
7923 "text-decoration": 1,
7930 "text-transform": 1,
7934 "transform-origin": 1,
7935 "transform-style": 1,
7937 "transition-delay": 1,
7938 "transition-duration": 1,
7939 "transition-property": 1,
7940 "transition-timing-function": 1,
7944 "vertical-align": 1,
7947 "voice-duration": 1,
7950 "voice-pitch-range": 1,
7956 "white-space-collapse": 1,
7972 parser.addListener("property", function(event){
7973 var name = event.property.text.toLowerCase();
7975 if (event.invalid) {
7976 reporter.report(event.invalid.message, event.line, event.col, rule);
7978 //if (!properties[name] && name.charAt(0) != "-"){
7979 // reporter.error("Unknown property '" + event.property + "'.", event.line, event.col, rule);
7987 * Rule: outline: none or outline: 0 should only be used in a :focus rule
7988 * and only if there are other properties in the same rule.
7995 name: "Disallow outline: none",
7996 desc: "Use of outline: none or outline: 0 should be limited to :focus rules.",
7998 tags: ["Accessibility"],
8001 init: function(parser, reporter){
8005 function startRule(event){
8006 if (event.selectors){
8010 selectors: event.selectors,
8019 function endRule(event){
8021 if (lastRule.outline){
8022 if (lastRule.selectors.toString().toLowerCase().indexOf(":focus") == -1){
8023 reporter.report("Outlines should only be modified using :focus.", lastRule.line, lastRule.col, rule);
8024 } else if (lastRule.propCount == 1) {
8025 reporter.report("Outlines shouldn't be hidden unless other visual changes are made.", lastRule.line, lastRule.col, rule);
8031 parser.addListener("startrule", startRule);
8032 parser.addListener("startfontface", startRule);
8033 parser.addListener("startpage", startRule);
8034 parser.addListener("startpagemargin", startRule);
8035 parser.addListener("startkeyframerule", startRule);
8037 parser.addListener("property", function(event){
8038 var name = event.property.text.toLowerCase(),
8039 value = event.value;
8042 lastRule.propCount++;
8043 if (name == "outline" && (value == "none" || value == "0")){
8044 lastRule.outline = true;
8050 parser.addListener("endrule", endRule);
8051 parser.addListener("endfontface", endRule);
8052 parser.addListener("endpage", endRule);
8053 parser.addListener("endpagemargin", endRule);
8054 parser.addListener("endkeyframerule", endRule);
8060 * Rule: Don't use classes or IDs with elements (a.foo or a#foo).
8066 id: "overqualified-elements",
8067 name: "Disallow overqualified elements",
8068 desc: "Don't use classes or IDs with elements (a.foo or a#foo).",
8072 init: function(parser, reporter){
8076 parser.addListener("startrule", function(event){
8077 var selectors = event.selectors,
8083 for (i=0; i < selectors.length; i++){
8084 selector = selectors[i];
8086 for (j=0; j < selector.parts.length; j++){
8087 part = selector.parts[j];
8088 if (part.type == parser.SELECTOR_PART_TYPE){
8089 for (k=0; k < part.modifiers.length; k++){
8090 modifier = part.modifiers[k];
8091 if (part.elementName && modifier.type == "id"){
8092 reporter.report("Element (" + part + ") is overqualified, just use " + modifier + " without element name.", part.line, part.col, rule);
8093 } else if (modifier.type == "class"){
8095 if (!classes[modifier]){
8096 classes[modifier] = [];
8098 classes[modifier].push({ modifier: modifier, part: part });
8106 parser.addListener("endstylesheet", function(){
8109 for (prop in classes){
8110 if (classes.hasOwnProperty(prop)){
8112 //one use means that this is overqualified
8113 if (classes[prop].length == 1 && classes[prop][0].part.elementName){
8114 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);
8123 * Rule: Headings (h1-h6) should not be qualified (namespaced).
8129 id: "qualified-headings",
8130 name: "Disallow qualified headings",
8131 desc: "Headings should not be qualified (namespaced).",
8135 init: function(parser, reporter){
8138 parser.addListener("startrule", function(event){
8139 var selectors = event.selectors,
8144 for (i=0; i < selectors.length; i++){
8145 selector = selectors[i];
8147 for (j=0; j < selector.parts.length; j++){
8148 part = selector.parts[j];
8149 if (part.type == parser.SELECTOR_PART_TYPE){
8150 if (part.elementName && /h[1-6]/.test(part.elementName.toString()) && j > 0){
8151 reporter.report("Heading (" + part.elementName + ") should not be qualified.", part.line, part.col, rule);
8161 * Rule: Selectors that look like regular expressions are slow and should be avoided.
8167 id: "regex-selectors",
8168 name: "Disallow selectors that look like regexs",
8169 desc: "Selectors that look like regular expressions are slow and should be avoided.",
8173 init: function(parser, reporter){
8176 parser.addListener("startrule", function(event){
8177 var selectors = event.selectors,
8183 for (i=0; i < selectors.length; i++){
8184 selector = selectors[i];
8185 for (j=0; j < selector.parts.length; j++){
8186 part = selector.parts[j];
8187 if (part.type == parser.SELECTOR_PART_TYPE){
8188 for (k=0; k < part.modifiers.length; k++){
8189 modifier = part.modifiers[k];
8190 if (modifier.type == "attribute"){
8191 if (/([\~\|\^\$\*]=)/.test(modifier)){
8192 reporter.report("Attribute selectors with " + RegExp.$1 + " are slow!", modifier.line, modifier.col, rule);
8205 * Rule: Total number of rules should not exceed x.
8212 name: "Rules Count",
8213 desc: "Track how many rules there are.",
8217 init: function(parser, reporter){
8222 parser.addListener("startrule", function(){
8226 parser.addListener("endstylesheet", function(){
8227 reporter.stat("rule-count", count);
8233 * Rule: Use shorthand properties where possible.
8241 name: "Require shorthand properties",
8242 desc: "Use shorthand properties where possible.",
8246 init: function(parser, reporter){
8249 propertiesToCheck = {},
8266 //initialize propertiesToCheck
8267 for (prop in mapping){
8268 if (mapping.hasOwnProperty(prop)){
8269 for (i=0, len=mapping[prop].length; i < len; i++){
8270 propertiesToCheck[mapping[prop][i]] = prop;
8275 function startRule(event){
8279 //event handler for end of rules
8280 function endRule(event){
8282 var prop, i, len, total;
8284 //check which properties this rule has
8285 for (prop in mapping){
8286 if (mapping.hasOwnProperty(prop)){
8289 for (i=0, len=mapping[prop].length; i < len; i++){
8290 total += properties[mapping[prop][i]] ? 1 : 0;
8293 if (total == mapping[prop].length){
8294 reporter.report("The properties " + mapping[prop].join(", ") + " can be replaced by " + prop + ".", event.line, event.col, rule);
8300 parser.addListener("startrule", startRule);
8301 parser.addListener("startfontface", startRule);
8303 //check for use of "font-size"
8304 parser.addListener("property", function(event){
8305 var name = event.property.toString().toLowerCase(),
8306 value = event.value.parts[0].value;
8308 if (propertiesToCheck[name]){
8309 properties[name] = 1;
8313 parser.addListener("endrule", endRule);
8314 parser.addListener("endfontface", endRule);
8320 * Rule: Don't use properties with a star prefix.
8327 id: "star-property-hack",
8328 name: "Disallow properties with a star prefix",
8329 desc: "Checks for the star property hack (targets IE6/7)",
8333 init: function(parser, reporter){
8336 //check if property name starts with "*"
8337 parser.addListener("property", function(event){
8338 var property = event.property;
8340 if (property.hack == "*") {
8341 reporter.report("Property with star prefix found.", event.property.line, event.property.col, rule);
8347 * Rule: Don't use text-indent for image replacement if you need to support rtl.
8355 name: "Disallow negative text-indent",
8356 desc: "Checks for text indent less than -99px",
8360 init: function(parser, reporter){
8366 function startRule(event){
8368 direction = "inherit";
8371 //event handler for end of rules
8372 function endRule(event){
8373 if (textIndent && direction != "ltr"){
8374 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);
8378 parser.addListener("startrule", startRule);
8379 parser.addListener("startfontface", startRule);
8381 //check for use of "font-size"
8382 parser.addListener("property", function(event){
8383 var name = event.property.toString().toLowerCase(),
8384 value = event.value;
8386 if (name == "text-indent" && value.parts[0].value < -99){
8387 textIndent = event.property;
8388 } else if (name == "direction" && value == "ltr"){
8393 parser.addListener("endrule", endRule);
8394 parser.addListener("endfontface", endRule);
8400 * Rule: Don't use properties with a underscore prefix.
8407 id: "underscore-property-hack",
8408 name: "Disallow properties with an underscore prefix",
8409 desc: "Checks for the underscore property hack (targets IE6)",
8413 init: function(parser, reporter){
8416 //check if property name starts with "_"
8417 parser.addListener("property", function(event){
8418 var property = event.property;
8420 if (property.hack == "_") {
8421 reporter.report("Property with underscore prefix found.", event.property.line, event.property.col, rule);
8427 * Rule: Headings (h1-h6) should be defined only once.
8433 id: "unique-headings",
8434 name: "Headings should only be defined once",
8435 desc: "Headings should be defined only once.",
8439 init: function(parser, reporter){
8451 parser.addListener("startrule", function(event){
8452 var selectors = event.selectors,
8458 for (i=0; i < selectors.length; i++){
8459 selector = selectors[i];
8460 part = selector.parts[selector.parts.length-1];
8462 if (part.elementName && /(h[1-6])/i.test(part.elementName.toString())){
8464 for (j=0; j < part.modifiers.length; j++){
8465 if (part.modifiers[j].type == "pseudo"){
8472 headings[RegExp.$1]++;
8473 if (headings[RegExp.$1] > 1) {
8474 reporter.report("Heading (" + part.elementName + ") has already been defined.", part.line, part.col, rule);
8481 parser.addListener("endstylesheet", function(event){
8485 for (prop in headings){
8486 if (headings.hasOwnProperty(prop)){
8487 if (headings[prop] > 1){
8488 messages.push(headings[prop] + " " + prop + "s");
8493 if (messages.length){
8494 reporter.rollupWarn("You have " + messages.join(", ") + " defined in this stylesheet.", rule);
8501 * Rule: Don't use universal selector because it's slow.
8507 id: "universal-selector",
8508 name: "Disallow universal selector",
8509 desc: "The universal selector (*) is known to be slow.",
8513 init: function(parser, reporter){
8516 parser.addListener("startrule", function(event){
8517 var selectors = event.selectors,
8523 for (i=0; i < selectors.length; i++){
8524 selector = selectors[i];
8526 part = selector.parts[selector.parts.length-1];
8527 if (part.elementName == "*"){
8528 reporter.report(rule.desc, part.line, part.col, rule);
8536 * Rule: Don't use unqualified attribute selectors because they're just like universal selectors.
8542 id: "unqualified-attributes",
8543 name: "Disallow unqualified attribute selectors",
8544 desc: "Unqualified attribute selectors are known to be slow.",
8548 init: function(parser, reporter){
8551 parser.addListener("startrule", function(event){
8553 var selectors = event.selectors,
8559 for (i=0; i < selectors.length; i++){
8560 selector = selectors[i];
8562 part = selector.parts[selector.parts.length-1];
8563 if (part.type == parser.SELECTOR_PART_TYPE){
8564 for (k=0; k < part.modifiers.length; k++){
8565 modifier = part.modifiers[k];
8566 if (modifier.type == "attribute" && (!part.elementName || part.elementName == "*")){
8567 reporter.report(rule.desc, part.line, part.col, rule);
8578 * Rule: When using a vendor-prefixed property, make sure to
8579 * include the standard one.
8585 id: "vendor-prefix",
8586 name: "Require standard property with vendor prefix",
8587 desc: "When using a vendor-prefixed property, make sure to include the standard one.",
8591 init: function(parser, reporter){
8595 propertiesToCheck = {
8596 "-webkit-border-radius": "border-radius",
8597 "-webkit-border-top-left-radius": "border-top-left-radius",
8598 "-webkit-border-top-right-radius": "border-top-right-radius",
8599 "-webkit-border-bottom-left-radius": "border-bottom-left-radius",
8600 "-webkit-border-bottom-right-radius": "border-bottom-right-radius",
8602 "-o-border-radius": "border-radius",
8603 "-o-border-top-left-radius": "border-top-left-radius",
8604 "-o-border-top-right-radius": "border-top-right-radius",
8605 "-o-border-bottom-left-radius": "border-bottom-left-radius",
8606 "-o-border-bottom-right-radius": "border-bottom-right-radius",
8608 "-moz-border-radius": "border-radius",
8609 "-moz-border-radius-topleft": "border-top-left-radius",
8610 "-moz-border-radius-topright": "border-top-right-radius",
8611 "-moz-border-radius-bottomleft": "border-bottom-left-radius",
8612 "-moz-border-radius-bottomright": "border-bottom-right-radius",
8614 "-moz-column-count": "column-count",
8615 "-webkit-column-count": "column-count",
8617 "-moz-column-gap": "column-gap",
8618 "-webkit-column-gap": "column-gap",
8620 "-moz-column-rule": "column-rule",
8621 "-webkit-column-rule": "column-rule",
8623 "-moz-column-rule-style": "column-rule-style",
8624 "-webkit-column-rule-style": "column-rule-style",
8626 "-moz-column-rule-color": "column-rule-color",
8627 "-webkit-column-rule-color": "column-rule-color",
8629 "-moz-column-rule-width": "column-rule-width",
8630 "-webkit-column-rule-width": "column-rule-width",
8632 "-moz-column-width": "column-width",
8633 "-webkit-column-width": "column-width",
8635 "-webkit-column-span": "column-span",
8636 "-webkit-columns": "columns",
8638 "-moz-box-shadow": "box-shadow",
8639 "-webkit-box-shadow": "box-shadow",
8641 "-moz-transform" : "transform",
8642 "-webkit-transform" : "transform",
8643 "-o-transform" : "transform",
8644 "-ms-transform" : "transform",
8646 "-moz-transform-origin" : "transform-origin",
8647 "-webkit-transform-origin" : "transform-origin",
8648 "-o-transform-origin" : "transform-origin",
8649 "-ms-transform-origin" : "transform-origin",
8651 "-moz-box-sizing" : "box-sizing",
8652 "-webkit-box-sizing" : "box-sizing",
8654 "-moz-user-select" : "user-select",
8655 "-khtml-user-select" : "user-select",
8656 "-webkit-user-select" : "user-select"
8659 //event handler for beginning of rules
8660 function startRule(){
8665 //event handler for end of rules
8666 function endRule(event){
8674 for (prop in properties){
8675 if (propertiesToCheck[prop]){
8676 needsStandard.push({ actual: prop, needed: propertiesToCheck[prop]});
8680 for (i=0, len=needsStandard.length; i < len; i++){
8681 needed = needsStandard[i].needed;
8682 actual = needsStandard[i].actual;
8684 if (!properties[needed]){
8685 reporter.report("Missing standard property '" + needed + "' to go along with '" + actual + "'.", properties[actual][0].name.line, properties[actual][0].name.col, rule);
8687 //make sure standard property is last
8688 if (properties[needed][0].pos < properties[actual][0].pos){
8689 reporter.report("Standard property '" + needed + "' should come after vendor-prefixed property '" + actual + "'.", properties[actual][0].name.line, properties[actual][0].name.col, rule);
8696 parser.addListener("startrule", startRule);
8697 parser.addListener("startfontface", startRule);
8698 parser.addListener("startpage", startRule);
8699 parser.addListener("startpagemargin", startRule);
8700 parser.addListener("startkeyframerule", startRule);
8702 parser.addListener("property", function(event){
8703 var name = event.property.text.toLowerCase();
8705 if (!properties[name]){
8706 properties[name] = [];
8709 properties[name].push({ name: event.property, value : event.value, pos:num++ });
8712 parser.addListener("endrule", endRule);
8713 parser.addListener("endfontface", endRule);
8714 parser.addListener("endpage", endRule);
8715 parser.addListener("endpagemargin", endRule);
8716 parser.addListener("endkeyframerule", endRule);
8721 * Rule: You don't need to specify units when a value is 0.
8728 name: "Disallow units for 0 values",
8729 desc: "You don't need to specify units when a value is 0.",
8733 init: function(parser, reporter){
8736 //count how many times "float" is used
8737 parser.addListener("property", function(event){
8738 var parts = event.value.parts,
8743 if ((parts[i].units || parts[i].type == "percentage") && parts[i].value === 0 && parts[i].type != "time"){
8744 reporter.report("Values of 0 shouldn't have units specified.", parts[i].line, parts[i].col, rule);
8758 * Replace special characters before write to output.
8761 * - single quotes is the escape sequence for double-quotes
8762 * - & is the escape sequence for &
8763 * - < is the escape sequence for <
8764 * - > is the escape sequence for >
8766 * @param {String} message to escape
8767 * @return escaped message as {String}
8769 var xmlEscape = function(str) {
8770 if (!str || str.constructor !== String) {
8774 return str.replace(/[\"&><]/g, function(match) {
8788 CSSLint.addFormatter({
8789 //format information
8790 id: "checkstyle-xml",
8791 name: "Checkstyle XML format",
8794 * Return opening root XML tag.
8795 * @return {String} to prepend before all results
8797 startFormat: function(){
8798 return "<?xml version=\"1.0\" encoding=\"utf-8\"?><checkstyle>";
8802 * Return closing root XML tag.
8803 * @return {String} to append after all results
8805 endFormat: function(){
8806 return "</checkstyle>";
8810 * Returns message when there is a file read error.
8811 * @param {String} filename The name of the file that caused the error.
8812 * @param {String} message The error message
8813 * @return {String} The error message.
8815 readError: function(filename, message) {
8816 return "<file name=\"" + xmlEscape(filename) + "\"><error line=\"0\" column=\"0\" severty=\"error\" message=\"" + xmlEscape(message) + "\"></error></file>";
8820 * Given CSS Lint results for a file, return output for this format.
8821 * @param results {Object} with error and warning messages
8822 * @param filename {String} relative file path
8823 * @param options {Object} (UNUSED for now) specifies special handling of output
8824 * @return {String} output for results
8826 formatResults: function(results, filename, options) {
8827 var messages = results.messages,
8831 * Generate a source string for a rule.
8832 * Checkstyle source strings usually resemble Java class names e.g
8833 * net.csslint.SomeRuleName
8834 * @param {Object} rule
8835 * @return rule source as {String}
8837 var generateSource = function(rule) {
8838 if (!rule || !('name' in rule)) {
8841 return 'net.csslint.' + rule.name.replace(/\s/g,'');
8846 if (messages.length > 0) {
8847 output.push("<file name=\""+filename+"\">");
8848 CSSLint.Util.forEach(messages, function (message, i) {
8849 //ignore rollups for now
8850 if (!message.rollup) {
8851 output.push("<error line=\"" + message.line + "\" column=\"" + message.col + "\" severity=\"" + message.type + "\"" +
8852 " message=\"" + xmlEscape(message.message) + "\" source=\"" + generateSource(message.rule) +"\"/>");
8855 output.push("</file>");
8858 return output.join("");
8864 CSSLint.addFormatter({
8865 //format information
8867 name: "Compact, 'porcelain' format",
8870 * Return content to be printed before all file results.
8871 * @return {String} to prepend before all results
8873 startFormat: function() {
8878 * Return content to be printed after all file results.
8879 * @return {String} to append after all results
8881 endFormat: function() {
8886 * Given CSS Lint results for a file, return output for this format.
8887 * @param results {Object} with error and warning messages
8888 * @param filename {String} relative file path
8889 * @param options {Object} (Optional) specifies special handling of output
8890 * @return {String} output for results
8892 formatResults: function(results, filename, options) {
8893 var messages = results.messages,
8895 options = options || {};
8898 * Capitalize and return given string.
8899 * @param str {String} to capitalize
8900 * @return {String} capitalized
8902 var capitalize = function(str) {
8903 return str.charAt(0).toUpperCase() + str.slice(1);
8906 if (messages.length === 0) {
8907 return options.quiet ? "" : filename + ": Lint Free!";
8910 CSSLint.Util.forEach(messages, function(message, i) {
8911 if (message.rollup) {
8912 output += filename + ": " + capitalize(message.type) + " - " + message.message + "\n";
8914 output += filename + ": " + "line " + message.line +
8915 ", col " + message.col + ", " + capitalize(message.type) + " - " + message.message + "\n";
8923 CSSLint.addFormatter({
8924 //format information
8926 name: "CSSLint XML format",
8929 * Return opening root XML tag.
8930 * @return {String} to prepend before all results
8932 startFormat: function(){
8933 return "<?xml version=\"1.0\" encoding=\"utf-8\"?><csslint>";
8937 * Return closing root XML tag.
8938 * @return {String} to append after all results
8940 endFormat: function(){
8941 return "</csslint>";
8945 * Given CSS Lint results for a file, return output for this format.
8946 * @param results {Object} with error and warning messages
8947 * @param filename {String} relative file path
8948 * @param options {Object} (UNUSED for now) specifies special handling of output
8949 * @return {String} output for results
8951 formatResults: function(results, filename, options) {
8952 var messages = results.messages,
8956 * Replace special characters before write to output.
8959 * - single quotes is the escape sequence for double-quotes
8960 * - & is the escape sequence for &
8961 * - < is the escape sequence for <
8962 * - > is the escape sequence for >
8964 * @param {String} message to escape
8965 * @return escaped message as {String}
8967 var escapeSpecialCharacters = function(str) {
8968 if (!str || str.constructor !== String) {
8971 return str.replace(/\"/g, "'").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
8974 if (messages.length > 0) {
8975 output.push("<file name=\""+filename+"\">");
8976 CSSLint.Util.forEach(messages, function (message, i) {
8977 if (message.rollup) {
8978 output.push("<issue severity=\"" + message.type + "\" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
8980 output.push("<issue line=\"" + message.line + "\" char=\"" + message.col + "\" severity=\"" + message.type + "\"" +
8981 " reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
8984 output.push("</file>");
8987 return output.join("");
8991 CSSLint.addFormatter({
8992 //format information
8994 name: "Lint XML format",
8997 * Return opening root XML tag.
8998 * @return {String} to prepend before all results
9000 startFormat: function(){
9001 return "<?xml version=\"1.0\" encoding=\"utf-8\"?><lint>";
9005 * Return closing root XML tag.
9006 * @return {String} to append after all results
9008 endFormat: function(){
9013 * Given CSS Lint results for a file, return output for this format.
9014 * @param results {Object} with error and warning messages
9015 * @param filename {String} relative file path
9016 * @param options {Object} (UNUSED for now) specifies special handling of output
9017 * @return {String} output for results
9019 formatResults: function(results, filename, options) {
9020 var messages = results.messages,
9024 * Replace special characters before write to output.
9027 * - single quotes is the escape sequence for double-quotes
9028 * - & is the escape sequence for &
9029 * - < is the escape sequence for <
9030 * - > is the escape sequence for >
9032 * @param {String} message to escape
9033 * @return escaped message as {String}
9035 var escapeSpecialCharacters = function(str) {
9036 if (!str || str.constructor !== String) {
9039 return str.replace(/\"/g, "'").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
9042 if (messages.length > 0) {
9044 output.push("<file name=\""+filename+"\">");
9045 CSSLint.Util.forEach(messages, function (message, i) {
9046 if (message.rollup) {
9047 output.push("<issue severity=\"" + message.type + "\" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
9049 output.push("<issue line=\"" + message.line + "\" char=\"" + message.col + "\" severity=\"" + message.type + "\"" +
9050 " reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
9053 output.push("</file>");
9056 return output.join("");
9060 CSSLint.addFormatter({
9061 //format information
9066 * Return content to be printed before all file results.
9067 * @return {String} to prepend before all results
9069 startFormat: function() {
9074 * Return content to be printed after all file results.
9075 * @return {String} to append after all results
9077 endFormat: function() {
9082 * Given CSS Lint results for a file, return output for this format.
9083 * @param results {Object} with error and warning messages
9084 * @param filename {String} relative file path
9085 * @param options {Object} (Optional) specifies special handling of output
9086 * @return {String} output for results
9088 formatResults: function(results, filename, options) {
9089 var messages = results.messages,
9091 options = options || {};
9093 if (messages.length === 0) {
9094 return options.quiet ? "" : "\n\ncsslint: No errors in " + filename + ".";
9097 output = "\n\ncsslint: There are " + messages.length + " problems in " + filename + ".";
9098 var pos = filename.lastIndexOf("/"),
9099 shortFilename = filename;
9102 pos = filename.lastIndexOf("\\");
9105 shortFilename = filename.substring(pos+1);
9108 CSSLint.Util.forEach(messages, function (message, i) {
9109 output = output + "\n\n" + shortFilename;
9110 if (message.rollup) {
9111 output += "\n" + (i+1) + ": " + message.type;
9112 output += "\n" + message.message;
9114 output += "\n" + (i+1) + ": " + message.type + " at line " + message.line + ", col " + message.col;
9115 output += "\n" + message.message;
9116 output += "\n" + message.evidence;