UNPKG

2gis-maps

Version:

Interactive 2GIS maps API, based on Leaflet

1,513 lines (1,290 loc) 94.3 kB
/*! 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){ this.messages.push({ type : this.ruleset[rule.id] == 2 ? "error" : "warning", line : line, col : col, message : message, evidence: this.lines[line-1], rule : rule }); }, /** * Report some informational text. * @param {String} message The message to store. * @param {int} line The line number. * @param {int} col The column number. * @param {Object} rule The rule this message relates to. * @method info */ info: function(message, line, col, rule){ this.messages.push({ type : "info", line : line, col : col, message : message, evidence: this.lines[line-1], rule : rule }); }, /** * Report some rollup error information. * @param {String} message The message to store. * @param {Object} rule The rule this message relates to. * @method rollupError */ rollupError: function(message, rule){ this.messages.push({ type : "error", rollup : true, message : message, rule : rule }); }, /** * Report some rollup warning information. * @param {String} message The message to store. * @param {Object} rule The rule this message relates to. * @method rollupWarn */ rollupWarn: function(message, rule){ this.messages.push({ type : "warning", rollup : true, message : message, rule : rule }); }, /** * Report a statistic. * @param {String} name The name of the stat to store. * @param {Variant} value The value of the stat. * @method stat */ stat: function(name, value){ this.stats[name] = value; } }; //expose for testing purposes CSSLint._Reporter = Reporter; /*global CSSLint*/ /* * Utility functions that make life easier. */ CSSLint.Util = { /* * Adds all properties from supplier onto receiver, * overwriting if the same name already exists on * reciever. * @param {Object} The object to receive the properties. * @param {Object} The object to provide the properties. * @return {Object} The receiver */ mix: function(receiver, supplier){ var prop; for (prop in supplier){ if (supplier.hasOwnProperty(prop)){ receiver[prop] = supplier[prop]; } } return prop; }, /* * Polyfill for array indexOf() method. * @param {Array} values The array to search. * @param {Variant} value The value to search for. * @return {int} The index of the value if found, -1 if not. */ indexOf: function(values, value){ if (values.indexOf){ return values.indexOf(value); } else { for (var i=0, len=values.length; i < len; i++){ if (values[i] === value){ return i; } } return -1; } }, /* * Polyfill for array forEach() method. * @param {Array} values The array to operate on. * @param {Function} func The function to call on each item. * @return {void} */ forEach: function(values, func) { if (values.forEach){ return values.forEach(func); } else { for (var i=0, len=values.length; i < len; i++){ func(values[i], i, values); } } } }; /*global CSSLint*/ /* * Rule: Don't use adjoining classes (.foo.bar). */ CSSLint.addRule({ //rule information id: "adjoining-classes", name: "Disallow adjoining classes", desc: "Don't use adjoining classes.", browsers: "IE6", //initialization init: function(parser, reporter){ var rule = this; parser.addListener("startrule", function(event){ var selectors = event.selectors, selector, part, modifier, classCount, i, j, k; for (i=0; i < selectors.length; i++){ selector = selectors[i]; for (j=0; j < selector.parts.length; j++){ part = selector.parts[j]; if (part.type == parser.SELECTOR_PART_TYPE){ classCount = 0; for (k=0; k < part.modifiers.length; k++){ modifier = part.modifiers[k]; if (modifier.type == "class"){ classCount++; } if (classCount > 1){ reporter.report("Don't use adjoining classes.", part.line, part.col, rule); } } } } } }); } }); /*global CSSLint*/ /* * Rule: Don't use width or height when using padding or border. */ CSSLint.addRule({ //rule information id: "box-model", name: "Beware of broken box size", desc: "Don't use width or height when using padding or border.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this, widthProperties = { border: 1, "border-left": 1, "border-right": 1, padding: 1, "padding-left": 1, "padding-right": 1 }, heightProperties = { border: 1, "border-bottom": 1, "border-top": 1, padding: 1, "padding-bottom": 1, "padding-top": 1 }, properties, boxSizing = false; function startRule(){ properties = {}; boxSizing = false; } function endRule(){ var prop, value; if (!boxSizing) { if (properties.height){ for (prop in heightProperties){ if (heightProperties.hasOwnProperty(prop) && properties[prop]){ value = properties[prop].value; //special case for padding if (!(prop == "padding" && value.parts.length === 2 && value.parts[0].value === 0)){ reporter.report("Using height with " + prop + " can sometimes make elements larger than you expect.", properties[prop].line, properties[prop].col, rule); } } } } if (properties.width){ for (prop in widthProperties){ if (widthProperties.hasOwnProperty(prop) && properties[prop]){ value = properties[prop].value; if (!(prop == "padding" && value.parts.length === 2 && value.parts[1].value === 0)){ reporter.report("Using width with " + prop + " can sometimes make elements larger than you expect.", properties[prop].line, properties[prop].col, rule); } } } } } } parser.addListener("startrule", startRule); parser.addListener("startfontface", startRule); parser.addListener("startpage", startRule); parser.addListener("startpagemargin", startRule); parser.addListener("startkeyframerule", startRule); parser.addListener("property", function(event){ var name = event.property.text.toLowerCase(); if (heightProperties[name] || widthProperties[name]){ if (!/^0\S*$/.test(event.value) && !(name == "border" && event.value == "none")){ properties[name] = { line: event.property.line, col: event.property.col, value: event.value }; } } else { if (/^(width|height)/i.test(name) && /^(length|percentage)/.test(event.value.parts[0].type)){ properties[name] = 1; } else if (name == "box-sizing") { boxSizing = true; } } }); parser.addListener("endrule", endRule); parser.addListener("endfontface", endRule); parser.addListener("endpage", endRule); parser.addListener("endpagemargin", endRule); parser.addListener("endkeyframerule", endRule); } }); /*global CSSLint*/ /* * Rule: box-sizing doesn't work in IE6 and IE7. */ CSSLint.addRule({ //rule information id: "box-sizing", name: "Disallow use of box-sizing", desc: "The box-sizing properties isn't supported in IE6 and IE7.", browsers: "IE6, IE7", tags: ["Compatibility"], //initialization init: function(parser, reporter){ var rule = this; parser.addListener("property", function(event){ var name = event.property.text.toLowerCase(); if (name == "box-sizing"){ reporter.report("The box-sizing property isn't supported in IE6 and IE7.", event.line, event.col, rule); } }); } }); /* * Rule: Use the bulletproof @font-face syntax to avoid 404's in old IE * (http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax) */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "bulletproof-font-face", name: "Use the bulletproof @font-face syntax", desc: "Use the bulletproof @font-face syntax to avoid 404's in old IE (http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax).", browsers: "All", //initialization init: function(parser, reporter){ var rule = this, count = 0, fontFaceRule = false, firstSrc = true, ruleFailed = false, line, col; // Mark the start of a @font-face declaration so we only test properties inside it parser.addListener("startfontface", function(event){ fontFaceRule = true; }); parser.addListener("property", function(event){ // If we aren't inside an @font-face declaration then just return if (!fontFaceRule) { return; } var propertyName = event.property.toString().toLowerCase(), value = event.value.toString(); // Set the line and col numbers for use in the endfontface listener line = event.line; col = event.col; // This is the property that we care about, we can ignore the rest if (propertyName === 'src') { var regex = /^\s?url\(['"].+\.eot\?.*['"]\)\s*format\(['"]embedded-opentype['"]\).*$/i; // We need to handle the advanced syntax with two src properties if (!value.match(regex) && firstSrc) { ruleFailed = true; firstSrc = false; } else if (value.match(regex) && !firstSrc) { ruleFailed = false; } } }); // Back to normal rules that we don't need to test parser.addListener("endfontface", function(event){ fontFaceRule = false; if (ruleFailed) { reporter.report("@font-face declaration doesn't follow the fontspring bulletproof syntax.", line, col, rule); } }); } }); /* * Rule: Include all compatible vendor prefixes to reach a wider * range of users. */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "compatible-vendor-prefixes", name: "Require compatible vendor prefixes", desc: "Include all compatible vendor prefixes to reach a wider range of users.", browsers: "All", //initialization init: function (parser, reporter) { var rule = this, compatiblePrefixes, properties, prop, variations, prefixed, i, len, inKeyFrame = false, arrayPush = Array.prototype.push, applyTo = []; // See http://peter.sh/experiments/vendor-prefixed-css-property-overview/ for details compatiblePrefixes = { "animation" : "webkit moz", "animation-delay" : "webkit moz", "animation-direction" : "webkit moz", "animation-duration" : "webkit moz", "animation-fill-mode" : "webkit moz", "animation-iteration-count" : "webkit moz", "animation-name" : "webkit moz", "animation-play-state" : "webkit moz", "animation-timing-function" : "webkit moz", "appearance" : "webkit moz", "border-end" : "webkit moz", "border-end-color" : "webkit moz", "border-end-style" : "webkit moz", "border-end-width" : "webkit moz", "border-image" : "webkit moz o", "border-radius" : "webkit", "border-start" : "webkit moz", "border-start-color" : "webkit moz", "border-start-style" : "webkit moz", "border-start-width" : "webkit moz", "box-align" : "webkit moz ms", "box-direction" : "webkit moz ms", "box-flex" : "webkit moz ms", "box-lines" : "webkit ms", "box-ordinal-group" : "webkit moz ms", "box-orient" : "webkit moz ms", "box-pack" : "webkit moz ms", "box-sizing" : "webkit moz", "box-shadow" : "webkit moz", "column-count" : "webkit moz ms", "column-gap" : "webkit moz ms", "column-rule" : "webkit moz ms", "column-rule-color" : "webkit moz ms", "column-rule-style" : "webkit moz ms", "column-rule-width" : "webkit moz ms", "column-width" : "webkit moz ms", "hyphens" : "epub moz", "line-break" : "webkit ms", "margin-end" : "webkit moz", "margin-start" : "webkit moz", "marquee-speed" : "webkit wap", "marquee-style" : "webkit wap", "padding-end" : "webkit moz", "padding-start" : "webkit moz", "tab-size" : "moz o", "text-size-adjust" : "webkit ms", "transform" : "webkit moz ms o", "transform-origin" : "webkit moz ms o", "transition" : "webkit moz o", "transition-delay" : "webkit moz o", "transition-duration" : "webkit moz o", "transition-property" : "webkit moz o", "transition-timing-function" : "webkit moz o", "user-modify" : "webkit moz", "user-select" : "webkit moz ms", "word-break" : "epub ms", "writing-mode" : "epub ms" }; for (prop in compatiblePrefixes) { if (compatiblePrefixes.hasOwnProperty(prop)) { variations = []; prefixed = compatiblePrefixes[prop].split(' '); for (i = 0, len = prefixed.length; i < len; i++) { variations.push('-' + prefixed[i] + '-' + prop); } compatiblePrefixes[prop] = variations; arrayPush.apply(applyTo, variations); } } parser.addListener("startrule", function () { properties = []; }); parser.addListener("startkeyframes", function (event) { inKeyFrame = event.prefix || true; }); parser.addListener("endkeyframes", function (event) { inKeyFrame = false; }); parser.addListener("property", function (event) { var name = event.property; if (CSSLint.Util.indexOf(applyTo, name.text) > -1) { // e.g., -moz-transform is okay to be alone in @-moz-keyframes if (!inKeyFrame || typeof inKeyFrame != "string" || name.text.indexOf("-" + inKeyFrame + "-") !== 0) { properties.push(name); } } }); parser.addListener("endrule", function (event) { if (!properties.length) { return; } var propertyGroups = {}, i, len, name, prop, variations, value, full, actual, item, propertiesSpecified; for (i = 0, len = properties.length; i < len; i++) { name = properties[i]; for (prop in compatiblePrefixes) { if (compatiblePrefixes.hasOwnProperty(prop)) { variations = compatiblePrefixes[prop]; if (CSSLint.Util.indexOf(variations, name.text) > -1) { if (!propertyGroups[prop]) { propertyGroups[prop] = { full : variations.slice(0), actual : [], actualNodes: [] }; } if (CSSLint.Util.indexOf(propertyGroups[prop].actual, name.text) === -1) { propertyGroups[prop].actual.push(name.text); propertyGroups[prop].actualNodes.push(name); } } } } } for (prop in propertyGroups) { if (propertyGroups.hasOwnProperty(prop)) { value = propertyGroups[prop]; full = value.full; actual = value.actual; if (full.length > actual.length) { for (i = 0, len = full.length; i < len; i++) { item = full[i]; if (CSSLint.Util.indexOf(actual, item) === -1) { propertiesSpecified = (actual.length === 1) ? actual[0] : (actual.length == 2) ? actual.join(" and ") : actual.join(", "); reporter.report("The property " + item + " is compatible with " + propertiesSpecified + " and should be included as well.", value.actualNodes[0].line, value.actualNodes[0].col, rule); } } } } } }); } }); /* * Rule: Certain properties don't play well with certain display values. * - float should not be used with inline-block * - height, width, margin-top, margin-bottom, float should not be used with inline * - vertical-align should not be used with block * - margin, float should not be used with table-* */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "display-property-grouping", name: "Require properties appropriate for display", desc: "Certain properties shouldn't be used with certain display property values.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this; var propertiesToCheck = { display: 1, "float": "none", height: 1, width: 1, margin: 1, "margin-left": 1, "margin-right": 1, "margin-bottom": 1, "margin-top": 1, padding: 1, "padding-left": 1, "padding-right": 1, "padding-bottom": 1, "padding-top": 1, "vertical-align": 1 }, properties; function reportProperty(name, display, msg){ if (properties[name]){ if (typeof propertiesToCheck[name] != "string" || properties[name].value.toLowerCase() != propertiesToCheck[name]){ reporter.report(msg || name + " can't be used with display: " + display + ".", properties[name].line, properties[name].col, rule); } } } function startRule(){ properties = {}; } function endRule(){ var display = properties.display ? properties.display.value : null; if (display){ switch(display){ case "inline": //height, width, margin-top, margin-bottom, float should not be used with inline reportProperty("height", display); reportProperty("width", display); reportProperty("margin", display); reportProperty("margin-top", display); reportProperty("margin-bottom", display); reportProperty("float", display, "display:inline has no effect on floated elements (but may be used to fix the IE6 double-margin bug)."); break; case "block": //vertical-align should not be used with block reportProperty("vertical-align", display); break; case "inline-block": //float should not be used with inline-block reportProperty("float", display); break; default: //margin, float should not be used with table if (display.indexOf("table-") === 0){ reportProperty("margin", display); reportProperty("margin-left", display); reportProperty("margin-right", display); reportProperty("margin-top", display); reportProperty("margin-bottom", display); reportProperty("float", display); } //otherwise do nothing } } } parser.addListener("startrule", startRule); parser.addListener("startfontface", startRule); parser.addListener("startkeyframerule", startRule); parser.addListener("startpagemargin", startRule); parser.addListener("startpage", startRule); parser.addListener("property", function(event){ var name = event.property.text.toLowerCase(); if (propertiesToCheck[name]){ properties[name] = { value: event.value.text, line: event.property.line, col: event.property.col }; } }); parser.addListener("endrule", endRule); parser.addListener("endfontface", endRule); parser.addListener("endkeyframerule", endRule); parser.addListener("endpagemargin", endRule); parser.addListener("endpage", endRule); } }); /* * Rule: Disallow duplicate background-images (using url). */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "duplicate-background-images", name: "Disallow duplicate background images", desc: "Every background-image should be unique. Use a common class for e.g. sprites.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this, stack = {}; parser.addListener("property", function(event){ var name = event.property.text, value = event.value, i, len; if (name.match(/background/i)) { for (i=0, len=value.parts.length; i < len; i++) { if (value.parts[i].type == 'uri') { if (typeof stack[value.parts[i].uri] === 'undefined') { stack[value.parts[i].uri] = event; } else { reporter.report("Background image '" + value.parts[i].uri + "' was used multiple times, first declared at line " + stack[value.parts[i].uri].line + ", col " + stack[value.parts[i].uri].col + ".", event.line, event.col, rule); } } } } }); } }); /* * Rule: Duplicate properties must appear one after the other. If an already-defined * property appears somewhere else in the rule, then it's likely an error. */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "duplicate-properties", name: "Disallow duplicate properties", desc: "Duplicate properties must appear one after the other.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this, properties, lastProperty; function startRule(event){ properties = {}; } parser.addListener("startrule", startRule); parser.addListener("startfontface", startRule); parser.addListener("startpage", startRule); parser.addListener("startpagemargin", startRule); parser.addListener("startkeyframerule", startRule); parser.addListener("property", function(event){ var property = event.property, name = property.text.toLowerCase(); if (properties[name] && (lastProperty != name || properties[name] == event.value.text)){ reporter.report("Duplicate property '" + event.property + "' found.", event.line, event.col, rule); } properties[name] = event.value.text; lastProperty = name; }); } }); /* * Rule: Style rules without any properties defined should be removed. */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "empty-rules", name: "Disallow empty rules", desc: "Rules without any properties specified should be removed.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this, count = 0; parser.addListener("startrule", function(){ count=0; }); parser.addListener("property", function(){ count++; }); parser.addListener("endrule", function(event){ var selectors = event.selectors; if (count === 0){ reporter.report("Rule is empty.", selectors[0].line, selectors[0].col, rule); } }); } }); /* * Rule: There should be no syntax errors. (Duh.) */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "errors", name: "Parsing Errors", desc: "This rule looks for recoverable syntax errors.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this; parser.addListener("error", function(event){ reporter.error(event.message, event.line, event.col, rule); }); } }); /*global CSSLint*/ CSSLint.addRule({ //rule information id: "fallback-colors", name: "Require fallback colors", desc: "For older browsers that don't support RGBA, HSL, or HSLA, provide a fallback color.", browsers: "IE6,IE7,IE8", //initialization init: function(parser, reporter){ var rule = this, lastProperty, propertiesToCheck = { color: 1, background: 1, "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: When using a vendor-prefixed gradient, make sure to use them all. */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "gradients", name: "Require all gradient definitions", desc: "When using a vendor-prefixed gradient, make sure to use them all.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this, gradients; parser.addListener("startrule", function(){ gradients = { moz: 0, webkit: 0, oldWebkit: 0, o: 0 }; }); parser.addListener("property", function(event){ if (/\-(moz|o|webkit)(?:\-(?:linear|radial))\-gradient/i.test(event.value)){ gradients[RegExp.$1] = 1; } else if (/\-webkit\-gradient/i.test(event.value)){ gradients.oldWebkit = 1; } }); parser.addListener("endrule", function(event){ var missing = []; if (!gradients.moz){ missing.push("Firefox 3.6+"); } if (!gradients.webkit){ missing.push("Webkit (Safari 5+, Chrome)"); } if (!gradients.oldWebkit){ missing.push("Old Webkit (Safari 4+, Chrome)"); } if (!gradients.o){ missing.push("Opera 11.1+"); } if (missing.length && missing.length < 4){ reporter.report("Missing vendor-prefixed CSS gradients for " + missing.join(", ") + ".", event.selectors[0].line, event.selectors[0].col, rule); } }); } }); /* * Rule: Don't use IDs for selectors. */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "ids", name: "Disallow IDs in selectors", desc: "Selectors should not contain IDs.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this; parser.addListener("startrule", function(event){ var selectors = event.selectors, selector, part, modifier, idCount, i, j, k; for (i=0; i < selectors.length; i++){ selector = selectors[i]; idCount = 0; for (j=0; j < selector.parts.length; j++){ part = selector.parts[j]; if (part.type == parser.SELECTOR_PART_TYPE){ for (k=0; k < part.modifiers.length; k++){ modifier = part.modifiers[k]; if (modifier.type == "id"){ idCount++; } } } } if (idCount == 1){ reporter.report("Don't use IDs in selectors.", selector.line, selector.col, rule); } else if (idCount > 1){ reporter.report(idCount + " IDs in the selector, really?", selector.line, selector.col, rule); } } }); } }); /* * Rule: Don't use @import, use <link> instead. */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "import", name: "Disallow @import", desc: "Don't use @import, use <link> instead.", browsers: "All", //initialization init: function(parser, reporter){ var rule = this; parser.addListener("import", function(event){ reporter.report("@import prevents parallel downloads, use <link> instead.", event.line, event.col, rule); }); } }); /* * Rule: Make sure !important is not overused, this could lead to specificity * war. Display a warning on !important declarations, an error if it's * used more at least 10 times. */ /*global CSSLint*/ CSSLint.addRule({ //rule information id: "important", name: "Disallow !important", desc: "Be careful when using !important declaration", browsers: "All", //initialization init: function(parser, reporter){ var rule = this, count = 0; //warn that important is used and increment the declaration counter parser.addListener("property", function(event){ if (event.important === true){ count++; reporter.report("Use of !important", event.line, event.col, rule); } }); //if there are more than 10, show an e