2370 строки
72 KiB
JavaScript
2370 строки
72 KiB
JavaScript
/*jshint eqeqeq: false */
|
|
/*jshint unused: false */
|
|
/*jshint latedef: nofunc */
|
|
/*!
|
|
CSSLint
|
|
Copyright (c) 2013 Nicole Sullivan and Nicholas C. Zakas. All rights reserved.
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
*/
|
|
/* Build: v0.10.0 15-August-2013 01:07:22 */
|
|
var parserlib = require("parserlib");
|
|
/**
|
|
* Main CSSLint object.
|
|
* @class CSSLint
|
|
* @static
|
|
* @extends parserlib.util.EventTarget
|
|
*/
|
|
/*global parserlib, Reporter*/
|
|
var CSSLint = (function(){
|
|
|
|
var rules = [],
|
|
formatters = [],
|
|
embeddedRuleset = /\/\*csslint([^\*]*)\*\//,
|
|
api = new parserlib.util.EventTarget();
|
|
|
|
api.version = "0.10.0";
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Rule Management
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Adds a new rule to the engine.
|
|
* @param {Object} rule The rule to add.
|
|
* @method addRule
|
|
*/
|
|
api.addRule = function(rule){
|
|
rules.push(rule);
|
|
rules[rule.id] = rule;
|
|
};
|
|
|
|
/**
|
|
* Clears all rule from the engine.
|
|
* @method clearRules
|
|
*/
|
|
api.clearRules = function(){
|
|
rules = [];
|
|
};
|
|
|
|
/**
|
|
* Returns the rule objects.
|
|
* @return An array of rule objects.
|
|
* @method getRules
|
|
*/
|
|
api.getRules = function(){
|
|
return [].concat(rules).sort(function(a,b){
|
|
return a.id > b.id ? 1 : 0;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Returns a ruleset configuration object with all current rules.
|
|
* @return A ruleset object.
|
|
* @method getRuleset
|
|
*/
|
|
api.getRuleset = function() {
|
|
var ruleset = {},
|
|
i = 0,
|
|
len = rules.length;
|
|
|
|
while (i < len){
|
|
ruleset[rules[i++].id] = 1; //by default, everything is a warning
|
|
}
|
|
|
|
return ruleset;
|
|
};
|
|
|
|
/**
|
|
* Returns a ruleset object based on embedded rules.
|
|
* @param {String} text A string of css containing embedded rules.
|
|
* @param {Object} ruleset A ruleset object to modify.
|
|
* @return {Object} A ruleset object.
|
|
* @method getEmbeddedRuleset
|
|
*/
|
|
function applyEmbeddedRuleset(text, ruleset){
|
|
var valueMap,
|
|
embedded = text && text.match(embeddedRuleset),
|
|
rules = embedded && embedded[1];
|
|
|
|
if (rules) {
|
|
valueMap = {
|
|
"true": 2, // true is error
|
|
"": 1, // blank is warning
|
|
"false": 0, // false is ignore
|
|
|
|
"2": 2, // explicit error
|
|
"1": 1, // explicit warning
|
|
"0": 0 // explicit ignore
|
|
};
|
|
|
|
rules.toLowerCase().split(",").forEach(function(rule){
|
|
var pair = rule.split(":"),
|
|
property = pair[0] || "",
|
|
value = pair[1] || "";
|
|
|
|
ruleset[property.trim()] = valueMap[value.trim()];
|
|
});
|
|
}
|
|
|
|
return ruleset;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Formatters
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Adds a new formatter to the engine.
|
|
* @param {Object} formatter The formatter to add.
|
|
* @method addFormatter
|
|
*/
|
|
api.addFormatter = function(formatter) {
|
|
// formatters.push(formatter);
|
|
formatters[formatter.id] = formatter;
|
|
};
|
|
|
|
/**
|
|
* Retrieves a formatter for use.
|
|
* @param {String} formatId The name of the format to retrieve.
|
|
* @return {Object} The formatter or undefined.
|
|
* @method getFormatter
|
|
*/
|
|
api.getFormatter = function(formatId){
|
|
return formatters[formatId];
|
|
};
|
|
|
|
/**
|
|
* Formats the results in a particular format for a single file.
|
|
* @param {Object} result The results returned from CSSLint.verify().
|
|
* @param {String} filename The filename for which the results apply.
|
|
* @param {String} formatId The name of the formatter to use.
|
|
* @param {Object} options (Optional) for special output handling.
|
|
* @return {String} A formatted string for the results.
|
|
* @method format
|
|
*/
|
|
api.format = function(results, filename, formatId, options) {
|
|
var formatter = this.getFormatter(formatId),
|
|
result = null;
|
|
|
|
if (formatter){
|
|
result = formatter.startFormat();
|
|
result += formatter.formatResults(results, filename, options || {});
|
|
result += formatter.endFormat();
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Indicates if the given format is supported.
|
|
* @param {String} formatId The ID of the format to check.
|
|
* @return {Boolean} True if the format exists, false if not.
|
|
* @method hasFormat
|
|
*/
|
|
api.hasFormat = function(formatId){
|
|
return formatters.hasOwnProperty(formatId);
|
|
};
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Verification
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Starts the verification process for the given CSS text.
|
|
* @param {String} text The CSS text to verify.
|
|
* @param {Object} ruleset (Optional) List of rules to apply. If null, then
|
|
* all rules are used. If a rule has a value of 1 then it's a warning,
|
|
* a value of 2 means it's an error.
|
|
* @return {Object} Results of the verification.
|
|
* @method verify
|
|
*/
|
|
api.verify = function(text, ruleset){
|
|
|
|
var i = 0,
|
|
len = rules.length,
|
|
reporter,
|
|
lines,
|
|
report,
|
|
parser = new parserlib.css.Parser({ starHack: true, ieFilters: true,
|
|
underscoreHack: true, strict: false });
|
|
|
|
// normalize line endings
|
|
lines = text.replace(/\n\r?/g, "$split$").split('$split$');
|
|
|
|
if (!ruleset){
|
|
ruleset = this.getRuleset();
|
|
}
|
|
|
|
if (embeddedRuleset.test(text)){
|
|
ruleset = applyEmbeddedRuleset(text, ruleset);
|
|
}
|
|
|
|
reporter = new Reporter(lines, ruleset);
|
|
|
|
ruleset.errors = 2; //always report parsing errors as errors
|
|
for (i in ruleset){
|
|
if(ruleset.hasOwnProperty(i) && ruleset[i]){
|
|
if (rules[i]){
|
|
rules[i].init(parser, reporter);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//capture most horrible error type
|
|
try {
|
|
parser.parse(text);
|
|
} catch (ex) {
|
|
reporter.error("Fatal error, cannot continue: " + ex.message, ex.line, ex.col, {});
|
|
}
|
|
|
|
report = {
|
|
messages : reporter.messages,
|
|
stats : reporter.stats,
|
|
ruleset : reporter.ruleset
|
|
};
|
|
|
|
//sort by line numbers, rollups at the bottom
|
|
report.messages.sort(function (a, b){
|
|
if (a.rollup && !b.rollup){
|
|
return 1;
|
|
} else if (!a.rollup && b.rollup){
|
|
return -1;
|
|
} else {
|
|
return a.line - b.line;
|
|
}
|
|
});
|
|
|
|
return report;
|
|
};
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Publish the API
|
|
//-------------------------------------------------------------------------
|
|
|
|
return api;
|
|
|
|
})();
|
|
|
|
/*global CSSLint*/
|
|
/**
|
|
* An instance of Report is used to report results of the
|
|
* verification back to the main API.
|
|
* @class Reporter
|
|
* @constructor
|
|
* @param {String[]} lines The text lines of the source.
|
|
* @param {Object} ruleset The set of rules to work with, including if
|
|
* they are errors or warnings.
|
|
*/
|
|
function Reporter(lines, ruleset){
|
|
|
|
/**
|
|
* List of messages being reported.
|
|
* @property messages
|
|
* @type String[]
|
|
*/
|
|
this.messages = [];
|
|
|
|
/**
|
|
* List of statistics being reported.
|
|
* @property stats
|
|
* @type String[]
|
|
*/
|
|
this.stats = [];
|
|
|
|
/**
|
|
* Lines of code being reported on. Used to provide contextual information
|
|
* for messages.
|
|
* @property lines
|
|
* @type String[]
|
|
*/
|
|
this.lines = lines;
|
|
|
|
/**
|
|
* Information about the rules. Used to determine whether an issue is an
|
|
* error or warning.
|
|
* @property ruleset
|
|
* @type Object
|
|
*/
|
|
this.ruleset = ruleset;
|
|
}
|
|
|
|
Reporter.prototype = {
|
|
|
|
//restore constructor
|
|
constructor: Reporter,
|
|
|
|
/**
|
|
* Report an error.
|
|
* @param {String} message The message to store.
|
|
* @param {int} line The line number.
|
|
* @param {int} col The column number.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method error
|
|
*/
|
|
error: function(message, line, col, rule){
|
|
this.messages.push({
|
|
type : "error",
|
|
line : line,
|
|
col : col,
|
|
message : message,
|
|
evidence: this.lines[line-1],
|
|
rule : rule || {}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report an warning.
|
|
* @param {String} message The message to store.
|
|
* @param {int} line The line number.
|
|
* @param {int} col The column number.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method warn
|
|
* @deprecated Use report instead.
|
|
*/
|
|
warn: function(message, line, col, rule){
|
|
this.report(message, line, col, rule);
|
|
},
|
|
|
|
/**
|
|
* Report an issue.
|
|
* @param {String} message The message to store.
|
|
* @param {int} line The line number.
|
|
* @param {int} col The column number.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method report
|
|
*/
|
|
report: function(message, line, col, rule, selector){
|
|
this.messages.push({
|
|
type : this.ruleset[rule.id] == 2 ? "error" : "warning",
|
|
line : line,
|
|
col : col,
|
|
message : message,
|
|
evidence: this.lines[line-1],
|
|
rule : rule,
|
|
selector: selector
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report some informational text.
|
|
* @param {String} message The message to store.
|
|
* @param {int} line The line number.
|
|
* @param {int} col The column number.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method info
|
|
*/
|
|
info: function(message, line, col, rule){
|
|
this.messages.push({
|
|
type : "info",
|
|
line : line,
|
|
col : col,
|
|
message : message,
|
|
evidence: this.lines[line-1],
|
|
rule : rule
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report some rollup error information.
|
|
* @param {String} message The message to store.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method rollupError
|
|
*/
|
|
rollupError: function(message, rule){
|
|
this.messages.push({
|
|
type : "error",
|
|
rollup : true,
|
|
message : message,
|
|
rule : rule
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report some rollup warning information.
|
|
* @param {String} message The message to store.
|
|
* @param {Object} rule The rule this message relates to.
|
|
* @method rollupWarn
|
|
*/
|
|
rollupWarn: function(message, rule){
|
|
this.messages.push({
|
|
type : "warning",
|
|
rollup : true,
|
|
message : message,
|
|
rule : rule
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Report a statistic.
|
|
* @param {String} name The name of the stat to store.
|
|
* @param {Variant} value The value of the stat.
|
|
* @method stat
|
|
*/
|
|
stat: function(name, value){
|
|
this.stats[name] = value;
|
|
}
|
|
};
|
|
|
|
//expose for testing purposes
|
|
CSSLint._Reporter = Reporter;
|
|
|
|
/*global CSSLint*/
|
|
|
|
/*
|
|
* Utility functions that make life easier.
|
|
*/
|
|
CSSLint.Util = {
|
|
/*
|
|
* Adds all properties from supplier onto receiver,
|
|
* overwriting if the same name already exists on
|
|
* reciever.
|
|
* @param {Object} The object to receive the properties.
|
|
* @param {Object} The object to provide the properties.
|
|
* @return {Object} The receiver
|
|
*/
|
|
mix: function(receiver, supplier){
|
|
var prop;
|
|
|
|
for (prop in supplier){
|
|
if (supplier.hasOwnProperty(prop)){
|
|
receiver[prop] = supplier[prop];
|
|
}
|
|
}
|
|
|
|
return prop;
|
|
},
|
|
|
|
/*
|
|
* Polyfill for array indexOf() method.
|
|
* @param {Array} values The array to search.
|
|
* @param {Variant} value The value to search for.
|
|
* @return {int} The index of the value if found, -1 if not.
|
|
*/
|
|
indexOf: function(values, value){
|
|
if (values.indexOf){
|
|
return values.indexOf(value);
|
|
} else {
|
|
for (var i=0, len=values.length; i < len; i++){
|
|
if (values[i] === value){
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Polyfill for array forEach() method.
|
|
* @param {Array} values The array to operate on.
|
|
* @param {Function} func The function to call on each item.
|
|
* @return {void}
|
|
*/
|
|
forEach: function(values, func) {
|
|
if (values.forEach){
|
|
return values.forEach(func);
|
|
} else {
|
|
for (var i=0, len=values.length; i < len; i++){
|
|
func(values[i], i, values);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/*
|
|
* Rule: Certain properties don't play well with certain display values.
|
|
* - float should not be used with inline-block
|
|
* - height, width, margin-top, margin-bottom, float should not be used with inline
|
|
* - vertical-align should not be used with block
|
|
* - margin, float should not be used with table-*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "display-property-grouping",
|
|
name: "Require properties appropriate for display",
|
|
desc: "Certain properties shouldn't be used with certain display property values.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
var propertiesToCheck = {
|
|
display: 1,
|
|
"float": "none",
|
|
height: 1,
|
|
width: 1,
|
|
margin: 1,
|
|
"margin-left": 1,
|
|
"margin-right": 1,
|
|
"margin-bottom": 1,
|
|
"margin-top": 1,
|
|
padding: 1,
|
|
"padding-left": 1,
|
|
"padding-right": 1,
|
|
"padding-bottom": 1,
|
|
"padding-top": 1,
|
|
"vertical-align": 1
|
|
},
|
|
properties;
|
|
|
|
function reportProperty(name, display, msg){
|
|
if (properties[name]){
|
|
if (typeof propertiesToCheck[name] != "string" || properties[name].value.toLowerCase() != propertiesToCheck[name]){
|
|
reporter.report(msg || name + " can't be used with display: " + display + ".", properties[name].line, properties[name].col, rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
function startRule(){
|
|
properties = {};
|
|
}
|
|
|
|
function endRule(){
|
|
|
|
var display = properties.display ? properties.display.value : null;
|
|
if (display){
|
|
switch(display){
|
|
|
|
case "inline":
|
|
//height, width, margin-top, margin-bottom, float should not be used with inline
|
|
reportProperty("height", display);
|
|
reportProperty("width", display);
|
|
reportProperty("margin", display);
|
|
reportProperty("margin-top", display);
|
|
reportProperty("margin-bottom", display);
|
|
reportProperty("float", display, "display:inline has no effect on floated elements (but may be used to fix the IE6 double-margin bug).");
|
|
break;
|
|
|
|
case "block":
|
|
//vertical-align should not be used with block
|
|
reportProperty("vertical-align", display);
|
|
break;
|
|
|
|
case "inline-block":
|
|
//float should not be used with inline-block
|
|
reportProperty("float", display);
|
|
break;
|
|
|
|
default:
|
|
//margin, float should not be used with table
|
|
if (display.indexOf("table-") === 0){
|
|
reportProperty("margin", display);
|
|
reportProperty("margin-left", display);
|
|
reportProperty("margin-right", display);
|
|
reportProperty("margin-top", display);
|
|
reportProperty("margin-bottom", display);
|
|
reportProperty("float", display);
|
|
}
|
|
|
|
//otherwise do nothing
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase();
|
|
|
|
if (propertiesToCheck[name]){
|
|
properties[name] = { value: event.value.text, line: event.property.line, col: event.property.col };
|
|
}
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
parser.addListener("endkeyframerule", endRule);
|
|
parser.addListener("endpagemargin", endRule);
|
|
parser.addListener("endpage", endRule);
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Disallow duplicate background-images (using url).
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "duplicate-background-images",
|
|
name: "Disallow duplicate background images",
|
|
desc: "Every background-image should be unique. Use a common class for e.g. sprites.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
stack = {};
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text,
|
|
value = event.value,
|
|
i, len;
|
|
|
|
if (name.match(/background/i)) {
|
|
for (i=0, len=value.parts.length; i < len; i++) {
|
|
if (value.parts[i].type == 'uri') {
|
|
if (typeof stack[value.parts[i].uri] === 'undefined') {
|
|
stack[value.parts[i].uri] = event;
|
|
}
|
|
else {
|
|
reporter.report("Background image '" + value.parts[i].uri + "' was used multiple times, first declared at line " + stack[value.parts[i].uri].line + ", col " + stack[value.parts[i].uri].col + ".", event.line, event.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*
|
|
* Rule: Duplicate properties must appear one after the other. If an already-defined
|
|
* property appears somewhere else in the rule, then it's likely an error.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "duplicate-properties",
|
|
name: "Disallow duplicate properties",
|
|
desc: "Duplicate properties must appear one after the other.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
properties,
|
|
lastProperty;
|
|
|
|
function startRule(event){
|
|
properties = {};
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var property = event.property,
|
|
name = property.text.toLowerCase();
|
|
|
|
if (properties[name] && (lastProperty != name || properties[name] == event.value.text)){
|
|
reporter.report("Duplicate property '" + event.property + "' found.", event.line, event.col, rule);
|
|
}
|
|
|
|
properties[name] = event.value.text;
|
|
lastProperty = name;
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Style rules without any properties defined should be removed.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "empty-rules",
|
|
name: "Disallow empty rules",
|
|
desc: "Rules without any properties specified should be removed.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
count = 0;
|
|
|
|
parser.addListener("startrule", function(){
|
|
count=0;
|
|
});
|
|
|
|
parser.addListener("property", function(){
|
|
count++;
|
|
});
|
|
|
|
parser.addListener("endrule", function(event){
|
|
var selectors = event.selectors;
|
|
if (count === 0){
|
|
reporter.report("Rule is empty.", selectors[0].line, selectors[0].col, rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: There should be no syntax errors. (Duh.)
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "errors",
|
|
name: "Parsing Errors",
|
|
desc: "This rule looks for recoverable syntax errors.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("error", function(event){
|
|
reporter.error(event.message, event.line, event.col, rule);
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "fallback-colors",
|
|
name: "Require fallback colors",
|
|
desc: "For older browsers that don't support RGBA, HSL, or HSLA, provide a fallback color.",
|
|
browsers: "IE6,IE7,IE8",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
lastProperty,
|
|
propertiesToCheck = {
|
|
color: 1,
|
|
background: 1,
|
|
"border-color": 1,
|
|
"border-top-color": 1,
|
|
"border-right-color": 1,
|
|
"border-bottom-color": 1,
|
|
"border-left-color": 1,
|
|
border: 1,
|
|
"border-top": 1,
|
|
"border-right": 1,
|
|
"border-bottom": 1,
|
|
"border-left": 1,
|
|
"background-color": 1
|
|
},
|
|
properties;
|
|
|
|
function startRule(event){
|
|
properties = {};
|
|
lastProperty = null;
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var property = event.property,
|
|
name = property.text.toLowerCase(),
|
|
parts = event.value.parts,
|
|
i = 0,
|
|
colorType = "",
|
|
len = parts.length;
|
|
|
|
if(propertiesToCheck[name]){
|
|
while(i < len){
|
|
if (parts[i].type == "color"){
|
|
if ("alpha" in parts[i] || "hue" in parts[i]){
|
|
|
|
if (/([^\)]+)\(/.test(parts[i])){
|
|
colorType = RegExp.$1.toUpperCase();
|
|
}
|
|
|
|
if (!lastProperty || (lastProperty.property.text.toLowerCase() != name || lastProperty.colorType != "compat")){
|
|
reporter.report("Fallback " + name + " (hex or RGB) should precede " + colorType + " " + name + ".", event.line, event.col, rule);
|
|
}
|
|
} else {
|
|
event.colorType = "compat";
|
|
}
|
|
}
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
lastProperty = event;
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: You shouldn't use more than 10 floats. If you do, there's probably
|
|
* room for some abstraction.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "floats",
|
|
name: "Disallow too many floats",
|
|
desc: "This rule tests if the float property is used too many times",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
var count = 0;
|
|
|
|
//count how many times "float" is used
|
|
parser.addListener("property", function(event){
|
|
if (event.property.text.toLowerCase() == "float" &&
|
|
event.value.text.toLowerCase() != "none"){
|
|
count++;
|
|
}
|
|
});
|
|
|
|
//report the results
|
|
parser.addListener("endstylesheet", function(){
|
|
reporter.stat("floats", count);
|
|
if (count >= 10){
|
|
reporter.rollupWarn("Too many floats (" + count + "), you're probably using them for layout. Consider using a grid system instead.", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Avoid too many @font-face declarations in the same stylesheet.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "font-faces",
|
|
name: "Don't use too many web fonts",
|
|
desc: "Too many different web fonts in the same stylesheet.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
count = 0;
|
|
|
|
|
|
parser.addListener("startfontface", function(){
|
|
count++;
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function(){
|
|
if (count > 5){
|
|
reporter.rollupWarn("Too many @font-face declarations (" + count + ").", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: You shouldn't need more than 9 font-size declarations.
|
|
*/
|
|
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "font-sizes",
|
|
name: "Disallow too many font sizes",
|
|
desc: "Checks the number of font-size declarations.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
count = 0;
|
|
|
|
//check for use of "font-size"
|
|
parser.addListener("property", function(event){
|
|
if (event.property == "font-size"){
|
|
count++;
|
|
}
|
|
});
|
|
|
|
//report the results
|
|
parser.addListener("endstylesheet", function(){
|
|
reporter.stat("font-sizes", count);
|
|
if (count >= 10){
|
|
reporter.rollupWarn("Too many font-size declarations (" + count + "), abstraction needed.", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
/*
|
|
* Rule: Don't use IDs for selectors.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "ids",
|
|
name: "Disallow IDs in selectors",
|
|
desc: "Selectors should not contain IDs.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
modifier,
|
|
idCount,
|
|
i, j, k;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
idCount = 0;
|
|
|
|
for (j=0; j < selector.parts.length; j++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (modifier.type == "id"){
|
|
idCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (idCount == 1){
|
|
reporter.report("Don't use IDs in selectors.", selector.line, selector.col, rule);
|
|
} else if (idCount > 1){
|
|
reporter.report(idCount + " IDs in the selector, really?", selector.line, selector.col, rule);
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use @import, use <link> instead.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "import",
|
|
name: "Disallow @import",
|
|
desc: "Don't use @import, use <link> instead.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("import", function(event){
|
|
reporter.report("@import prevents parallel downloads, use <link> instead.", event.line, event.col, rule);
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Make sure !important is not overused, this could lead to specificity
|
|
* war. Display a warning on !important declarations, an error if it's
|
|
* used more at least 10 times.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "important",
|
|
name: "Disallow !important",
|
|
desc: "Be careful when using !important declaration",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
count = 0;
|
|
|
|
//warn that important is used and increment the declaration counter
|
|
parser.addListener("property", function(event){
|
|
if (event.important === true){
|
|
count++;
|
|
reporter.report("Use of !important", event.line, event.col, rule);
|
|
}
|
|
});
|
|
|
|
//if there are more than 10, show an error
|
|
parser.addListener("endstylesheet", function(){
|
|
reporter.stat("important", count);
|
|
if (count >= 10){
|
|
reporter.rollupWarn("Too many !important declarations (" + count + "), try to use less than 10 to avoid specificity issues.", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Properties should be known (listed in CSS3 specification) or
|
|
* be a vendor-prefixed property.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "known-properties",
|
|
name: "Require use of known properties",
|
|
desc: "Properties should be known (listed in CSS3 specification) or be a vendor-prefixed property.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase();
|
|
|
|
// the check is handled entirely by the parser-lib (https://github.com/nzakas/parser-lib)
|
|
if (event.invalid) {
|
|
reporter.report(event.invalid.message, event.line, event.col, rule);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: outline: none or outline: 0 should only be used in a :focus rule
|
|
* and only if there are other properties in the same rule.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "outline-none",
|
|
name: "Disallow outline: none",
|
|
desc: "Use of outline: none or outline: 0 should be limited to :focus rules.",
|
|
browsers: "All",
|
|
tags: ["Accessibility"],
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
lastRule;
|
|
|
|
function startRule(event){
|
|
if (event.selectors){
|
|
lastRule = {
|
|
line: event.line,
|
|
col: event.col,
|
|
selectors: event.selectors,
|
|
propCount: 0,
|
|
outline: false
|
|
};
|
|
} else {
|
|
lastRule = null;
|
|
}
|
|
}
|
|
|
|
function endRule(event){
|
|
if (lastRule){
|
|
if (lastRule.outline){
|
|
if (lastRule.selectors.toString().toLowerCase().indexOf(":focus") == -1){
|
|
reporter.report("Outlines should only be modified using :focus.", lastRule.line, lastRule.col, rule);
|
|
} else if (lastRule.propCount == 1) {
|
|
reporter.report("Outlines shouldn't be hidden unless other visual changes are made.", lastRule.line, lastRule.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase(),
|
|
value = event.value;
|
|
|
|
if (lastRule){
|
|
lastRule.propCount++;
|
|
if (name == "outline" && (value == "none" || value == "0")){
|
|
lastRule.outline = true;
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
parser.addListener("endpage", endRule);
|
|
parser.addListener("endpagemargin", endRule);
|
|
parser.addListener("endkeyframerule", endRule);
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use classes or IDs with elements (a.foo or a#foo).
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "overqualified-elements",
|
|
name: "Disallow overqualified elements",
|
|
desc: "Don't use classes or IDs with elements (a.foo or a#foo).",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
classes = {};
|
|
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
modifier,
|
|
i, j, k;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
|
|
for (j=0; j < selector.parts.length; j++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (part.elementName && modifier.type == "id"){
|
|
reporter.report("Element (" + part + ") is overqualified, just use " + modifier + " without element name.", part.line, part.col, rule);
|
|
} else if (modifier.type == "class"){
|
|
|
|
if (!classes[modifier]){
|
|
classes[modifier] = [];
|
|
}
|
|
classes[modifier].push({ modifier: modifier, part: part });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function(){
|
|
|
|
var prop;
|
|
for (prop in classes){
|
|
if (classes.hasOwnProperty(prop)){
|
|
|
|
//one use means that this is overqualified
|
|
if (classes[prop].length == 1 && classes[prop][0].part.elementName){
|
|
reporter.report("Element (" + classes[prop][0].part + ") is overqualified, just use " + classes[prop][0].modifier + " without element name.", classes[prop][0].part.line, classes[prop][0].part.col, rule);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Headings (h1-h6) should not be qualified (namespaced).
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "qualified-headings",
|
|
name: "Disallow qualified headings",
|
|
desc: "Headings should not be qualified (namespaced).",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
i, j;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
|
|
for (j=0; j < selector.parts.length; j++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
if (part.elementName && /h[1-6]/.test(part.elementName.toString()) && j > 0){
|
|
reporter.report("Heading (" + part.elementName + ") should not be qualified.", part.line, part.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Selectors that look like regular expressions are slow and should be avoided.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "regex-selectors",
|
|
name: "Disallow selectors that look like regexs",
|
|
desc: "Selectors that look like regular expressions are slow and should be avoided.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
modifier,
|
|
i, j, k;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
for (j=0; j < selector.parts.length; j++){
|
|
part = selector.parts[j];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (modifier.type == "attribute"){
|
|
if (/([\~\|\^\$\*]=)/.test(modifier)){
|
|
reporter.report("Attribute selectors with " + RegExp.$1 + " are slow!", modifier.line, modifier.col, rule);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Total number of rules should not exceed x.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "rules-count",
|
|
name: "Rules Count",
|
|
desc: "Track how many rules there are.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
count = 0;
|
|
|
|
//count each rule
|
|
parser.addListener("startrule", function(){
|
|
count++;
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function(){
|
|
reporter.stat("rule-count", count);
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Warn people with approaching the IE 4095 limit
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "selector-max-approaching",
|
|
name: "Warn when approaching the 4095 selector limit for IE",
|
|
desc: "Will warn when selector count is >= 3800 selectors.",
|
|
browsers: "IE",
|
|
|
|
//initialization
|
|
init: function(parser, reporter) {
|
|
var rule = this, count = 0;
|
|
|
|
parser.addListener('startrule', function(event) {
|
|
count += event.selectors.length;
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function() {
|
|
if (count >= 3800) {
|
|
reporter.report("You have " + count + " selectors. Internet Explorer supports a maximum of 4095 selectors per stylesheet. Consider refactoring.",0,0,rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
/*
|
|
* Rule: Warn people past the IE 4095 limit
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "selector-max",
|
|
name: "Error when past the 4095 selector limit for IE",
|
|
desc: "Will error when selector count is > 4095.",
|
|
browsers: "IE",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this, count = 0;
|
|
|
|
parser.addListener('startrule',function(event) {
|
|
count += event.selectors.length;
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function() {
|
|
if (count > 4095) {
|
|
reporter.report("You have " + count + " selectors. Internet Explorer supports a maximum of 4095 selectors per stylesheet. Consider refactoring.",0,0,rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Use shorthand properties where possible.
|
|
*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "shorthand",
|
|
name: "Require shorthand properties",
|
|
desc: "Use shorthand properties where possible.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
prop, i, len,
|
|
propertiesToCheck = {},
|
|
properties,
|
|
mapping = {
|
|
"margin": [
|
|
"margin-top",
|
|
"margin-bottom",
|
|
"margin-left",
|
|
"margin-right"
|
|
],
|
|
"padding": [
|
|
"padding-top",
|
|
"padding-bottom",
|
|
"padding-left",
|
|
"padding-right"
|
|
]
|
|
};
|
|
|
|
//initialize propertiesToCheck
|
|
for (prop in mapping){
|
|
if (mapping.hasOwnProperty(prop)){
|
|
for (i=0, len=mapping[prop].length; i < len; i++){
|
|
propertiesToCheck[mapping[prop][i]] = prop;
|
|
}
|
|
}
|
|
}
|
|
|
|
function startRule(event){
|
|
properties = {};
|
|
}
|
|
|
|
//event handler for end of rules
|
|
function endRule(event){
|
|
|
|
var prop, i, len, total;
|
|
|
|
//check which properties this rule has
|
|
for (prop in mapping){
|
|
if (mapping.hasOwnProperty(prop)){
|
|
total=0;
|
|
|
|
for (i=0, len=mapping[prop].length; i < len; i++){
|
|
total += properties[mapping[prop][i]] ? 1 : 0;
|
|
}
|
|
|
|
if (total == mapping[prop].length){
|
|
reporter.report("The properties " + mapping[prop].join(", ") + " can be replaced by " + prop + ".", event.line, event.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
|
|
//check for use of "font-size"
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.toString().toLowerCase(),
|
|
value = event.value.parts[0].value;
|
|
|
|
if (propertiesToCheck[name]){
|
|
properties[name] = 1;
|
|
}
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use properties with a star prefix.
|
|
*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "star-property-hack",
|
|
name: "Disallow properties with a star prefix",
|
|
desc: "Checks for the star property hack (targets IE6/7)",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
//check if property name starts with "*"
|
|
parser.addListener("property", function(event){
|
|
var property = event.property;
|
|
|
|
if (property.hack == "*") {
|
|
reporter.report("Property with star prefix found.", event.property.line, event.property.col, rule);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*
|
|
* Rule: Don't use text-indent for image replacement if you need to support rtl.
|
|
*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "text-indent",
|
|
name: "Disallow negative text-indent",
|
|
desc: "Checks for text indent less than -99px",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
textIndent,
|
|
direction;
|
|
|
|
|
|
function startRule(event){
|
|
textIndent = false;
|
|
direction = "inherit";
|
|
}
|
|
|
|
//event handler for end of rules
|
|
function endRule(event){
|
|
if (textIndent && direction != "ltr"){
|
|
reporter.report("Negative text-indent doesn't work well with RTL. If you use text-indent for image replacement explicitly set direction for that item to ltr.", textIndent.line, textIndent.col, rule);
|
|
}
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
|
|
//check for use of "font-size"
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.toString().toLowerCase(),
|
|
value = event.value;
|
|
|
|
if (name == "text-indent" && value.parts[0].value < -99){
|
|
textIndent = event.property;
|
|
} else if (name == "direction" && value == "ltr"){
|
|
direction = "ltr";
|
|
}
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use properties with a underscore prefix.
|
|
*
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "underscore-property-hack",
|
|
name: "Disallow properties with an underscore prefix",
|
|
desc: "Checks for the underscore property hack (targets IE6)",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
//check if property name starts with "_"
|
|
parser.addListener("property", function(event){
|
|
var property = event.property;
|
|
|
|
if (property.hack == "_") {
|
|
reporter.report("Property with underscore prefix found.", event.property.line, event.property.col, rule);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*
|
|
* Rule: Headings (h1-h6) should be defined only once.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "unique-headings",
|
|
name: "Headings should only be defined once",
|
|
desc: "Headings should be defined only once.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
var headings = {
|
|
h1: 0,
|
|
h2: 0,
|
|
h3: 0,
|
|
h4: 0,
|
|
h5: 0,
|
|
h6: 0
|
|
};
|
|
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
pseudo,
|
|
i, j;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
part = selector.parts[selector.parts.length-1];
|
|
|
|
if (part.elementName && /(h[1-6])/i.test(part.elementName.toString())){
|
|
|
|
for (j=0; j < part.modifiers.length; j++){
|
|
if (part.modifiers[j].type == "pseudo"){
|
|
pseudo = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!pseudo){
|
|
headings[RegExp.$1]++;
|
|
if (headings[RegExp.$1] > 1) {
|
|
reporter.report("Heading (" + part.elementName + ") has already been defined.", part.line, part.col, rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
parser.addListener("endstylesheet", function(event){
|
|
var prop,
|
|
messages = [];
|
|
|
|
for (prop in headings){
|
|
if (headings.hasOwnProperty(prop)){
|
|
if (headings[prop] > 1){
|
|
messages.push(headings[prop] + " " + prop + "s");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (messages.length){
|
|
reporter.rollupWarn("You have " + messages.join(", ") + " defined in this stylesheet.", rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use universal selector because it's slow.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "universal-selector",
|
|
name: "Disallow universal selector",
|
|
desc: "The universal selector (*) is known to be slow.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("startrule", function(event){
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
modifier,
|
|
i, j, k;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
|
|
part = selector.parts[selector.parts.length-1];
|
|
if (part.elementName == "*"){
|
|
reporter.report(rule.desc, part.line, part.col, rule);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: Don't use unqualified attribute selectors because they're just like universal selectors.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "unqualified-attributes",
|
|
name: "Disallow unqualified attribute selectors",
|
|
desc: "Unqualified attribute selectors are known to be slow.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
parser.addListener("startrule", function(event){
|
|
|
|
var selectors = event.selectors,
|
|
selector,
|
|
part,
|
|
modifier,
|
|
i, j, k;
|
|
|
|
for (i=0; i < selectors.length; i++){
|
|
selector = selectors[i];
|
|
|
|
part = selector.parts[selector.parts.length-1];
|
|
if (part.type == parser.SELECTOR_PART_TYPE){
|
|
for (k=0; k < part.modifiers.length; k++){
|
|
modifier = part.modifiers[k];
|
|
if (modifier.type == "attribute" && (!part.elementName || part.elementName == "*")){
|
|
reporter.report(rule.desc, part.line, part.col, rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: When using a vendor-prefixed property, make sure to
|
|
* include the standard one.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "vendor-prefix",
|
|
name: "Require standard property with vendor prefix",
|
|
desc: "When using a vendor-prefixed property, make sure to include the standard one.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this,
|
|
properties,
|
|
num,
|
|
propertiesToCheck = {
|
|
"-webkit-border-radius": "border-radius",
|
|
"-webkit-border-top-left-radius": "border-top-left-radius",
|
|
"-webkit-border-top-right-radius": "border-top-right-radius",
|
|
"-webkit-border-bottom-left-radius": "border-bottom-left-radius",
|
|
"-webkit-border-bottom-right-radius": "border-bottom-right-radius",
|
|
|
|
"-o-border-radius": "border-radius",
|
|
"-o-border-top-left-radius": "border-top-left-radius",
|
|
"-o-border-top-right-radius": "border-top-right-radius",
|
|
"-o-border-bottom-left-radius": "border-bottom-left-radius",
|
|
"-o-border-bottom-right-radius": "border-bottom-right-radius",
|
|
|
|
"-moz-border-radius": "border-radius",
|
|
"-moz-border-radius-topleft": "border-top-left-radius",
|
|
"-moz-border-radius-topright": "border-top-right-radius",
|
|
"-moz-border-radius-bottomleft": "border-bottom-left-radius",
|
|
"-moz-border-radius-bottomright": "border-bottom-right-radius",
|
|
|
|
"-moz-column-count": "column-count",
|
|
"-webkit-column-count": "column-count",
|
|
|
|
"-moz-column-gap": "column-gap",
|
|
"-webkit-column-gap": "column-gap",
|
|
|
|
"-moz-column-rule": "column-rule",
|
|
"-webkit-column-rule": "column-rule",
|
|
|
|
"-moz-column-rule-style": "column-rule-style",
|
|
"-webkit-column-rule-style": "column-rule-style",
|
|
|
|
"-moz-column-rule-color": "column-rule-color",
|
|
"-webkit-column-rule-color": "column-rule-color",
|
|
|
|
"-moz-column-rule-width": "column-rule-width",
|
|
"-webkit-column-rule-width": "column-rule-width",
|
|
|
|
"-moz-column-width": "column-width",
|
|
"-webkit-column-width": "column-width",
|
|
|
|
"-webkit-column-span": "column-span",
|
|
"-webkit-columns": "columns",
|
|
|
|
"-moz-box-shadow": "box-shadow",
|
|
"-webkit-box-shadow": "box-shadow",
|
|
|
|
"-moz-transform" : "transform",
|
|
"-webkit-transform" : "transform",
|
|
"-o-transform" : "transform",
|
|
"-ms-transform" : "transform",
|
|
|
|
"-moz-transform-origin" : "transform-origin",
|
|
"-webkit-transform-origin" : "transform-origin",
|
|
"-o-transform-origin" : "transform-origin",
|
|
"-ms-transform-origin" : "transform-origin",
|
|
|
|
"-moz-box-sizing" : "box-sizing",
|
|
"-webkit-box-sizing" : "box-sizing",
|
|
|
|
"-moz-user-select" : "user-select",
|
|
"-khtml-user-select" : "user-select",
|
|
"-webkit-user-select" : "user-select"
|
|
};
|
|
|
|
//event handler for beginning of rules
|
|
function startRule(){
|
|
properties = {};
|
|
num=1;
|
|
}
|
|
|
|
//event handler for end of rules
|
|
function endRule(event){
|
|
var prop,
|
|
i, len,
|
|
standard,
|
|
needed,
|
|
actual,
|
|
needsStandard = [];
|
|
|
|
for (prop in properties){
|
|
if (properties.hasOwnProperty(prop)){
|
|
if (propertiesToCheck[prop]){
|
|
needsStandard.push({ actual: prop, needed: propertiesToCheck[prop]});
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i=0, len=needsStandard.length; i < len; i++){
|
|
needed = needsStandard[i].needed;
|
|
actual = needsStandard[i].actual;
|
|
|
|
if (!properties[needed]){
|
|
reporter.report("Missing standard property '" + needed + "' to go along with '" + actual + "'.", properties[actual][0].name.line, properties[actual][0].name.col, rule);
|
|
} else {
|
|
//make sure standard property is last
|
|
if (properties[needed][0].pos < properties[actual][0].pos){
|
|
reporter.report("Standard property '" + needed + "' should come after vendor-prefixed property '" + actual + "'.", properties[actual][0].name.line, properties[actual][0].name.col, rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
parser.addListener("startrule", startRule);
|
|
parser.addListener("startfontface", startRule);
|
|
parser.addListener("startpage", startRule);
|
|
parser.addListener("startpagemargin", startRule);
|
|
parser.addListener("startkeyframerule", startRule);
|
|
|
|
parser.addListener("property", function(event){
|
|
var name = event.property.text.toLowerCase();
|
|
|
|
if (!properties[name]){
|
|
properties[name] = [];
|
|
}
|
|
|
|
properties[name].push({ name: event.property, value : event.value, pos:num++ });
|
|
});
|
|
|
|
parser.addListener("endrule", endRule);
|
|
parser.addListener("endfontface", endRule);
|
|
parser.addListener("endpage", endRule);
|
|
parser.addListener("endpagemargin", endRule);
|
|
parser.addListener("endkeyframerule", endRule);
|
|
}
|
|
|
|
});
|
|
/*
|
|
* Rule: You don't need to specify units when a value is 0.
|
|
*/
|
|
/*global CSSLint*/
|
|
CSSLint.addRule({
|
|
|
|
//rule information
|
|
id: "zero-units",
|
|
name: "Disallow units for 0 values",
|
|
desc: "You don't need to specify units when a value is 0.",
|
|
browsers: "All",
|
|
|
|
//initialization
|
|
init: function(parser, reporter){
|
|
var rule = this;
|
|
|
|
//count how many times "float" is used
|
|
parser.addListener("property", function(event){
|
|
var parts = event.value.parts,
|
|
i = 0,
|
|
len = parts.length;
|
|
|
|
while(i < len){
|
|
if ((parts[i].units || parts[i].type == "percentage") && parts[i].value === 0 && parts[i].type != "time"){
|
|
reporter.report("Values of 0 shouldn't have units specified.", parts[i].line, parts[i].col, rule);
|
|
}
|
|
i++;
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
/*global CSSLint*/
|
|
(function() {
|
|
|
|
/**
|
|
* Replace special characters before write to output.
|
|
*
|
|
* Rules:
|
|
* - single quotes is the escape sequence for double-quotes
|
|
* - & is the escape sequence for &
|
|
* - < is the escape sequence for <
|
|
* - > is the escape sequence for >
|
|
*
|
|
* @param {String} message to escape
|
|
* @return escaped message as {String}
|
|
*/
|
|
var xmlEscape = function(str) {
|
|
if (!str || str.constructor !== String) {
|
|
return "";
|
|
}
|
|
|
|
return str.replace(/[\"&><]/g, function(match) {
|
|
switch (match) {
|
|
case "\"":
|
|
return """;
|
|
case "&":
|
|
return "&";
|
|
case "<":
|
|
return "<";
|
|
case ">":
|
|
return ">";
|
|
}
|
|
});
|
|
};
|
|
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "checkstyle-xml",
|
|
name: "Checkstyle XML format",
|
|
|
|
/**
|
|
* Return opening root XML tag.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function(){
|
|
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><checkstyle>";
|
|
},
|
|
|
|
/**
|
|
* Return closing root XML tag.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function(){
|
|
return "</checkstyle>";
|
|
},
|
|
|
|
/**
|
|
* Returns message when there is a file read error.
|
|
* @param {String} filename The name of the file that caused the error.
|
|
* @param {String} message The error message
|
|
* @return {String} The error message.
|
|
*/
|
|
readError: function(filename, message) {
|
|
return "<file name=\"" + xmlEscape(filename) + "\"><error line=\"0\" column=\"0\" severty=\"error\" message=\"" + xmlEscape(message) + "\"></error></file>";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (UNUSED for now) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = [];
|
|
|
|
/**
|
|
* Generate a source string for a rule.
|
|
* Checkstyle source strings usually resemble Java class names e.g
|
|
* net.csslint.SomeRuleName
|
|
* @param {Object} rule
|
|
* @return rule source as {String}
|
|
*/
|
|
var generateSource = function(rule) {
|
|
if (!rule || !('name' in rule)) {
|
|
return "";
|
|
}
|
|
return 'net.csslint.' + rule.name.replace(/\s/g,'');
|
|
};
|
|
|
|
|
|
|
|
if (messages.length > 0) {
|
|
output.push("<file name=\""+filename+"\">");
|
|
CSSLint.Util.forEach(messages, function (message, i) {
|
|
//ignore rollups for now
|
|
if (!message.rollup) {
|
|
output.push("<error line=\"" + message.line + "\" column=\"" + message.col + "\" severity=\"" + message.type + "\"" +
|
|
" message=\"" + xmlEscape(message.message) + "\" source=\"" + generateSource(message.rule) +"\"/>");
|
|
}
|
|
});
|
|
output.push("</file>");
|
|
}
|
|
|
|
return output.join("");
|
|
}
|
|
});
|
|
|
|
}());
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "compact",
|
|
name: "Compact, 'porcelain' format",
|
|
|
|
/**
|
|
* Return content to be printed before all file results.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function() {
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Return content to be printed after all file results.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function() {
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (Optional) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = "";
|
|
options = options || {};
|
|
|
|
/**
|
|
* Capitalize and return given string.
|
|
* @param str {String} to capitalize
|
|
* @return {String} capitalized
|
|
*/
|
|
var capitalize = function(str) {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
};
|
|
|
|
if (messages.length === 0) {
|
|
return options.quiet ? "" : filename + ": Lint Free!";
|
|
}
|
|
|
|
CSSLint.Util.forEach(messages, function(message, i) {
|
|
if (message.rollup) {
|
|
output += filename + ": " + capitalize(message.type) + " - " + message.message + "\n";
|
|
} else {
|
|
output += filename + ": " + "line " + message.line +
|
|
", col " + message.col + ", " + capitalize(message.type) + " - " + message.message + "\n";
|
|
}
|
|
});
|
|
|
|
return output;
|
|
}
|
|
});
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "csslint-xml",
|
|
name: "CSSLint XML format",
|
|
|
|
/**
|
|
* Return opening root XML tag.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function(){
|
|
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><csslint>";
|
|
},
|
|
|
|
/**
|
|
* Return closing root XML tag.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function(){
|
|
return "</csslint>";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (UNUSED for now) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = [];
|
|
|
|
/**
|
|
* Replace special characters before write to output.
|
|
*
|
|
* Rules:
|
|
* - single quotes is the escape sequence for double-quotes
|
|
* - & is the escape sequence for &
|
|
* - < is the escape sequence for <
|
|
* - > is the escape sequence for >
|
|
*
|
|
* @param {String} message to escape
|
|
* @return escaped message as {String}
|
|
*/
|
|
var escapeSpecialCharacters = function(str) {
|
|
if (!str || str.constructor !== String) {
|
|
return "";
|
|
}
|
|
return str.replace(/\"/g, "'").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
};
|
|
|
|
if (messages.length > 0) {
|
|
output.push("<file name=\""+filename+"\">");
|
|
CSSLint.Util.forEach(messages, function (message, i) {
|
|
if (message.rollup) {
|
|
output.push("<issue severity=\"" + message.type + "\" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
|
|
} else {
|
|
output.push("<issue line=\"" + message.line + "\" char=\"" + message.col + "\" severity=\"" + message.type + "\"" +
|
|
" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
|
|
}
|
|
});
|
|
output.push("</file>");
|
|
}
|
|
|
|
return output.join("");
|
|
}
|
|
});
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "junit-xml",
|
|
name: "JUNIT XML format",
|
|
|
|
/**
|
|
* Return opening root XML tag.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function(){
|
|
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><testsuites>";
|
|
},
|
|
|
|
/**
|
|
* Return closing root XML tag.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function() {
|
|
return "</testsuites>";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (UNUSED for now) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
|
|
var messages = results.messages,
|
|
output = [],
|
|
tests = {
|
|
'error': 0,
|
|
'failure': 0
|
|
};
|
|
|
|
/**
|
|
* Generate a source string for a rule.
|
|
* JUNIT source strings usually resemble Java class names e.g
|
|
* net.csslint.SomeRuleName
|
|
* @param {Object} rule
|
|
* @return rule source as {String}
|
|
*/
|
|
var generateSource = function(rule) {
|
|
if (!rule || !('name' in rule)) {
|
|
return "";
|
|
}
|
|
return 'net.csslint.' + rule.name.replace(/\s/g,'');
|
|
};
|
|
|
|
/**
|
|
* Replace special characters before write to output.
|
|
*
|
|
* Rules:
|
|
* - single quotes is the escape sequence for double-quotes
|
|
* - < is the escape sequence for <
|
|
* - > is the escape sequence for >
|
|
*
|
|
* @param {String} message to escape
|
|
* @return escaped message as {String}
|
|
*/
|
|
var escapeSpecialCharacters = function(str) {
|
|
|
|
if (!str || str.constructor !== String) {
|
|
return "";
|
|
}
|
|
|
|
return str.replace(/\"/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
|
|
};
|
|
|
|
if (messages.length > 0) {
|
|
|
|
messages.forEach(function (message, i) {
|
|
|
|
// since junit has no warning class
|
|
// all issues as errors
|
|
var type = message.type === 'warning' ? 'error' : message.type;
|
|
|
|
//ignore rollups for now
|
|
if (!message.rollup) {
|
|
|
|
// build the test case seperately, once joined
|
|
// we'll add it to a custom array filtered by type
|
|
output.push("<testcase time=\"0\" name=\"" + generateSource(message.rule) + "\">");
|
|
output.push("<" + type + " message=\"" + escapeSpecialCharacters(message.message) + "\"><![CDATA[" + message.line + ':' + message.col + ':' + escapeSpecialCharacters(message.evidence) + "]]></" + type + ">");
|
|
output.push("</testcase>");
|
|
|
|
tests[type] += 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
output.unshift("<testsuite time=\"0\" tests=\"" + messages.length + "\" skipped=\"0\" errors=\"" + tests.error + "\" failures=\"" + tests.failure + "\" package=\"net.csslint\" name=\"" + filename + "\">");
|
|
output.push("</testsuite>");
|
|
|
|
}
|
|
|
|
return output.join("");
|
|
|
|
}
|
|
});
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "lint-xml",
|
|
name: "Lint XML format",
|
|
|
|
/**
|
|
* Return opening root XML tag.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function(){
|
|
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><lint>";
|
|
},
|
|
|
|
/**
|
|
* Return closing root XML tag.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function(){
|
|
return "</lint>";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (UNUSED for now) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = [];
|
|
|
|
/**
|
|
* Replace special characters before write to output.
|
|
*
|
|
* Rules:
|
|
* - single quotes is the escape sequence for double-quotes
|
|
* - & is the escape sequence for &
|
|
* - < is the escape sequence for <
|
|
* - > is the escape sequence for >
|
|
*
|
|
* @param {String} message to escape
|
|
* @return escaped message as {String}
|
|
*/
|
|
var escapeSpecialCharacters = function(str) {
|
|
if (!str || str.constructor !== String) {
|
|
return "";
|
|
}
|
|
return str.replace(/\"/g, "'").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
};
|
|
|
|
if (messages.length > 0) {
|
|
|
|
output.push("<file name=\""+filename+"\">");
|
|
CSSLint.Util.forEach(messages, function (message, i) {
|
|
if (message.rollup) {
|
|
output.push("<issue severity=\"" + message.type + "\" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
|
|
} else {
|
|
output.push("<issue line=\"" + message.line + "\" char=\"" + message.col + "\" severity=\"" + message.type + "\"" +
|
|
" reason=\"" + escapeSpecialCharacters(message.message) + "\" evidence=\"" + escapeSpecialCharacters(message.evidence) + "\"/>");
|
|
}
|
|
});
|
|
output.push("</file>");
|
|
}
|
|
|
|
return output.join("");
|
|
}
|
|
});
|
|
/*global CSSLint*/
|
|
CSSLint.addFormatter({
|
|
//format information
|
|
id: "text",
|
|
name: "Plain Text",
|
|
|
|
/**
|
|
* Return content to be printed before all file results.
|
|
* @return {String} to prepend before all results
|
|
*/
|
|
startFormat: function() {
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Return content to be printed after all file results.
|
|
* @return {String} to append after all results
|
|
*/
|
|
endFormat: function() {
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Given CSS Lint results for a file, return output for this format.
|
|
* @param results {Object} with error and warning messages
|
|
* @param filename {String} relative file path
|
|
* @param options {Object} (Optional) specifies special handling of output
|
|
* @return {String} output for results
|
|
*/
|
|
formatResults: function(results, filename, options) {
|
|
var messages = results.messages,
|
|
output = "";
|
|
options = options || {};
|
|
|
|
if (messages.length === 0) {
|
|
return options.quiet ? "" : "\n\ncsslint: No errors in " + filename + ".";
|
|
}
|
|
|
|
output = "\n\ncsslint: There are " + messages.length + " problems in " + filename + ".";
|
|
var pos = filename.lastIndexOf("/"),
|
|
shortFilename = filename;
|
|
|
|
if (pos === -1){
|
|
pos = filename.lastIndexOf("\\");
|
|
}
|
|
if (pos > -1){
|
|
shortFilename = filename.substring(pos+1);
|
|
}
|
|
|
|
CSSLint.Util.forEach(messages, function (message, i) {
|
|
output = output + "\n\n" + shortFilename;
|
|
if (message.rollup) {
|
|
output += "\n" + (i+1) + ": " + message.type;
|
|
output += "\n" + message.message;
|
|
} else {
|
|
output += "\n" + (i+1) + ": " + message.type + " at line " + message.line + ", col " + message.col;
|
|
output += "\n" + message.message;
|
|
output += "\n" + message.evidence;
|
|
}
|
|
});
|
|
|
|
return output;
|
|
}
|
|
});
|
|
exports.CSSLint = CSSLint;
|