diff --git a/edit/editor-worker.js b/edit/editor-worker.js index e84b753f..68ffce6b 100644 --- a/edit/editor-worker.js +++ b/edit/editor-worker.js @@ -43,7 +43,7 @@ csslint() { require(['/js/csslint/csslint']); - return CSSLint.getRules().map(rule => { + return CSSLint.getRuleList().map(rule => { const output = {}; for (const [key, value] of Object.entries(rule)) { if (typeof value !== 'function') { diff --git a/js/csslint/csslint.js b/js/csslint/csslint.js index c37bb32d..8ad8c3a7 100644 --- a/js/csslint/csslint.js +++ b/js/csslint/csslint.js @@ -26,7 +26,7 @@ THE SOFTWARE. /* global parserlib */ 'use strict'; -//region Reporter +//#region Reporter class Reporter { /** @@ -49,9 +49,9 @@ class Reporter { this.ignore = ignore || []; } - error(message, line, col, rule = {}) { + error(message, {line = 1, col = 1}, rule = {}) { this.messages.push({ - type: 'error', + type: 'error', evidence: this.lines[line - 1], line, col, message, @@ -59,13 +59,13 @@ class Reporter { }); } - report(message, line, col, rule) { + report(message, {line = 1, col = 1}, rule) { if (line in this.allow && rule.id in this.allow[line] || this.ignore.some(range => range[0] <= line && line <= range[1])) { return; } this.messages.push({ - type: this.ruleset[rule.id] === 2 ? 'error' : 'warning', + type: this.ruleset[rule.id] === 2 ? 'error' : 'warning', evidence: this.lines[line - 1], line, col, message, @@ -73,9 +73,9 @@ class Reporter { }); } - info(message, line, col, rule) { + info(message, {line = 1, col = 1}, rule) { this.messages.push({ - type: 'info', + type: 'info', evidence: this.lines[line - 1], line, col, message, @@ -85,7 +85,7 @@ class Reporter { rollupError(message, rule) { this.messages.push({ - type: 'error', + type: 'error', rollup: true, message, rule, @@ -94,7 +94,7 @@ class Reporter { rollupWarn(message, rule) { this.messages.push({ - type: 'warning', + type: 'warning', rollup: true, message, rule, @@ -106,8 +106,8 @@ class Reporter { } } -//endregion -//region CSSLint +//#endregion +//#region CSSLint //eslint-disable-next-line no-var var CSSLint = (() => { @@ -115,45 +115,46 @@ var CSSLint = (() => { const RX_EMBEDDED = /\/\*\s*csslint\s+((?:[^*]|\*(?!\/))+?)\*\//ig; const EBMEDDED_RULE_VALUE_MAP = { // error - 'true': 2, - '2': 2, + 'true': 2, + '2': 2, // warning - '': 1, - '1': 1, + '': 1, + '1': 1, // ignore 'false': 0, - '0': 0, + '0': 0, }; - const rules = []; + const rules = Object.create(null); // previous CSSLint overrides are used to decide whether the parserlib's cache should be reset let prevOverrides; return Object.assign(new parserlib.util.EventTarget(), { + /** + * This Proxy allows for direct property assignment of individual rules + * so that "Go to symbol" command can be used in IDE to find a rule by id + * as well as reduce the indentation thanks to the use of array literals. + */ + addRule: new Proxy(rules, { + set(_, id, [rule, init]) { + rules[id] = rule; + rule.id = id; + rule.init = init; + return true; + }, + }), - addRule(rule) { - rules.push(rule); - rules[rule.id] = rule; + rules, + + getRuleList() { + return Object.values(rules) + .sort((a, b) => a.id < b.id ? -1 : a.id > b.id); }, - clearRules() { - rules.length = 0; - }, - - getRules() { - return rules - .slice() - .sort((a, b) => - a.id < b.id ? -1 : - a.id > b.id ? 1 : 0); - }, - - getRuleset() { + getRuleSet() { const ruleset = {}; // by default, everything is a warning - for (const rule of rules) { - ruleset[rule.id] = 1; - } + for (const id in rules) ruleset[id] = 1; return ruleset; }, @@ -167,7 +168,7 @@ var CSSLint = (() => { */ verify(text, ruleset) { - if (!ruleset) ruleset = this.getRuleset(); + if (!ruleset) ruleset = this.getRuleSet(); const allow = {}; const ignore = []; @@ -181,20 +182,20 @@ var CSSLint = (() => { } const parser = new parserlib.css.Parser({ - starHack: true, - ieFilters: true, + starHack: true, + ieFilters: true, underscoreHack: true, - strict: false, + strict: false, }); const reporter = new Reporter([], ruleset, allow, ignore); // always report parsing errors as errors ruleset.errors = 2; - Object.keys(ruleset).forEach(id => - ruleset[id] && - rules[id] && - rules[id].init(parser, reporter)); + for (const [id, mode] of Object.entries(ruleset)) { + const rule = mode && rules[id]; + if (rule) rule.init(rule, parser, reporter); + } // TODO: when ruleset is unchanged we can try to invalidate only line ranges in 'allow' and 'ignore' const newOvr = [ruleset, allow, ignore]; @@ -204,16 +205,15 @@ var CSSLint = (() => { try { parser.parse(text, {reuseCache}); } catch (ex) { - reporter.error('Fatal error, cannot continue!\n' + ex.stack, - ex.line || 1, ex.col || 1, {}); + reporter.error('Fatal error, cannot continue!\n' + ex.stack, ex, {}); } const report = { messages: reporter.messages, - stats: reporter.stats, - ruleset: reporter.ruleset, - allow: reporter.allow, - ignore: reporter.ignore, + stats: reporter.stats, + ruleset: reporter.ruleset, + allow: reporter.allow, + ignore: reporter.ignore, }; // sort by line numbers, rollups at the bottom @@ -322,21 +322,24 @@ var CSSLint = (() => { } })(); -//endregion -//region Util +//#endregion +//#region Util CSSLint.Util = { - registerBlockEvents(parser, start, end, property) { + /** Gets the lower-cased text without vendor prefix */ + getPropName(prop) { + return prop._propName || + (prop._propName = prop.text.replace(parserlib.util.rxVendorPrefix, '').toLowerCase()); + }, + + registerRuleEvents(parser, {start, property, end}) { for (const e of [ - 'document', 'fontface', 'keyframerule', - 'media', 'page', 'pagemargin', 'rule', - 'supports', 'viewport', ]) { if (start) parser.addListener('start' + e, start); @@ -345,35 +348,32 @@ CSSLint.Util = { if (property) parser.addListener('property', property); }, - registerShorthandEvents(parser, {property, endRule}) { + registerShorthandEvents(parser, {property, end}) { const {shorthands, shorthandsFor} = CSSLint.Util; let props, inRule; - parser.addListener('startrule', onStartRule); - parser.addListener('startfontface', onStartRule); - parser.addListener('property', onProperty); - parser.addListener('endrule', onEndRule); - parser.addListener('endfontface', onEndRule); - function onStartRule() { - inRule = true; - props = null; - } - function onProperty(event) { - if (!inRule) return; - const name = event.property.text.toLowerCase(); - const sh = shorthandsFor[name]; - if (sh) { - if (!props) props = {}; - (props[sh] || (props[sh] = {}))[name] = event; - } else if (property && props && name in shorthands) { - property(event, props, name); - } - } - function onEndRule(event) { - inRule = false; - if (endRule && props) { - endRule(event, props); - } - } + CSSLint.Util.registerRuleEvents(parser, { + start() { + inRule = true; + props = null; + }, + property(event) { + if (!inRule) return; + const name = CSSLint.Util.getPropName(event.property); + const sh = shorthandsFor[name]; + if (sh) { + if (!props) props = {}; + (props[sh] || (props[sh] = {}))[name] = event; + } else if (property && props && name in shorthands) { + property(event, props, name); + } + }, + end(event) { + inRule = false; + if (end && props) { + end(event, props); + } + }, + }); }, get shorthands() { @@ -442,404 +442,341 @@ CSSLint.Util = { }, }; -//endregion -//region Rules +//#endregion +//#region Rules -CSSLint.addRule({ - id: 'adjoining-classes', - name: 'Disallow adjoining classes', - desc: "Don't use adjoining classes.", - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-adjoining-classes', +CSSLint.addRule['adjoining-classes'] = [{ + name: 'Disallow adjoining classes', + desc: "Don't use adjoining classes.", + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-adjoining-classes', browsers: 'IE6', - - init(parser, reporter) { - parser.addListener('startrule', event => { - for (const selector of event.selectors) { - for (const part of selector.parts) { - if (part.type !== parser.SELECTOR_PART_TYPE) continue; +}, (rule, parser, reporter) => { + parser.addListener('startrule', event => { + for (const selector of event.selectors) { + for (const part of selector.parts) { + if (part.type === parser.SELECTOR_PART_TYPE) { let classCount = 0; for (const modifier of part.modifiers) { classCount += modifier.type === 'class'; if (classCount > 1) { - reporter.report('Adjoining classes: ' + selector.text, part.line, part.col, this); + reporter.report('Adjoining classes: ' + selector.text, part, rule); } } } } - }); - }, -}); + } + }); +}]; -CSSLint.addRule({ - id: 'box-model', - name: 'Beware of broken box size', - desc: "Don't use width or height when using padding or border.", - url: 'https://github.com/CSSLint/csslint/wiki/Beware-of-box-model-size', +CSSLint.addRule['box-model'] = [{ + name: 'Beware of broken box size', + desc: "Don't use width or height when using padding or border.", + url: 'https://github.com/CSSLint/csslint/wiki/Beware-of-box-model-size', browsers: 'All', - - init(parser, reporter) { - const sizeProps = { - width: [ - 'border', - 'border-left', - 'border-right', - 'padding', - 'padding-left', - 'padding-right', - ], - height: [ - 'border', - 'border-bottom', - 'border-top', - 'padding', - 'padding-bottom', - 'padding-top', - ], - }; - let properties = {}; - let boxSizing = false; - let started = 0; - - const startRule = () => { - started = 1; +}, (rule, parser, reporter) => { + const sizeProps = { + width: ['border', 'border-left', 'border-right', 'padding', 'padding-left', 'padding-right'], + height: ['border', 'border-bottom', 'border-top', 'padding', 'padding-bottom', 'padding-top'], + }; + let properties = {}; + let boxSizing = false; + let inRule; + CSSLint.Util.registerRuleEvents(parser, { + start() { + inRule = true; properties = {}; boxSizing = false; - }; - - const property = event => { - if (!started) return; - const name = event.property.text.toLowerCase(); - + }, + property(event) { + if (!inRule) return; + const name = CSSLint.Util.getPropName(event.property); if (sizeProps.width.includes(name) || sizeProps.height.includes(name)) { - if (!/^0+\D*$/.test(event.value) && (name !== 'border' || !/^none$/i.test(event.value))) { properties[name] = { - line: event.property.line, - col: event.property.col, + 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; } - }; - - const endRule = () => { - started = 0; + }, + end() { + inRule = false; if (boxSizing) return; - for (const size in sizeProps) { if (!properties[size]) continue; - for (const prop of sizeProps[size]) { if (prop !== 'padding' || !properties[prop]) continue; - const {value: {parts}, line, col} = properties[prop].value; if (parts.length !== 2 || Number(parts[0].value) !== 0) { - reporter.report(`Using ${size} with ${prop} can sometimes make elements larger than you expect.`, - line, col, this); + reporter.report( + `Using ${size} with ${prop} can sometimes make elements larger than you expect.`, + {line, col}, rule); } } } - }; + }, + }); +}]; - CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property); - }, -}); - -CSSLint.addRule({ - id: 'box-sizing', - name: 'Disallow use of box-sizing', - desc: "The box-sizing properties isn't supported in IE6 and IE7.", - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-box-sizing', +CSSLint.addRule['box-sizing'] = [{ + name: 'Disallow use of box-sizing', + desc: "'box-sizing' isn't supported in IE6-7.", + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-box-sizing', browsers: 'IE6, IE7', tags: ['Compatibility'], - - init(parser, reporter) { - parser.addListener('property', event => { - if (event.property.text.toLowerCase() === 'box-sizing') { - reporter.report(this.desc, event.line, event.col, this); - } - }); - }, -}); - -CSSLint.addRule({ - 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).', - url: 'https://github.com/CSSLint/csslint/wiki/Bulletproof-font-face', - browsers: 'All', - - init(parser, reporter) { - const regex = /^\s?url\(['"].+\.eot\?.*['"]\)\s*format\(['"]embedded-opentype['"]\).*$/i; - let firstSrc = true; - let ruleFailed = false; - let line, col; - - // Mark the start of a @font-face declaration so we only test properties inside it - parser.addListener('startfontface', () => { - parser.addListener('property', property); - }); - - function property(event) { - const propertyName = event.property.toString().toLowerCase(); - if (propertyName !== 'src') return; - - const value = event.value.toString(); - line = event.line; - col = event.col; - - const matched = regex.test(value); - if (firstSrc && !matched) { - ruleFailed = true; - firstSrc = false; - } else if (!firstSrc && matched) { - ruleFailed = false; - } +}, (rule, parser, reporter) => { + parser.addListener('property', event => { + if (CSSLint.Util.getPropName(event.property) === 'box-sizing') { + reporter.report(rule.desc, event, rule); } + }); +}]; - // Back to normal rules that we don't need to test - parser.addListener('endfontface', () => { - parser.removeListener('property', property); - if (!ruleFailed) return; - reporter.report("@font-face declaration doesn't follow the fontspring bulletproof syntax.", - line, col, this); - }); - }, -}); - -CSSLint.addRule({ - id: 'compatible-vendor-prefixes', - name: 'Require compatible vendor prefixes', - desc: 'Include all compatible vendor prefixes to reach a wider range of users.', - url: 'https://github.com/CSSLint/csslint/wiki/Require-compatible-vendor-prefixes', +CSSLint.addRule['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', + url: 'https://github.com/CSSLint/csslint/wiki/Bulletproof-font-face', browsers: 'All', - - init(parser, reporter) { - // See http://peter.sh/experiments/vendor-prefixed-css-property-overview/ for details - const compatiblePrefixes = { - 'animation': 'webkit', - 'animation-delay': 'webkit', - 'animation-direction': 'webkit', - 'animation-duration': 'webkit', - 'animation-fill-mode': 'webkit', - 'animation-iteration-count': 'webkit', - 'animation-name': 'webkit', - 'animation-play-state': 'webkit', - 'animation-timing-function': 'webkit', - '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', - 'box-direction': 'webkit moz', - 'box-flex': 'webkit moz', - 'box-lines': 'webkit', - 'box-ordinal-group': 'webkit moz', - 'box-orient': 'webkit moz', - 'box-pack': 'webkit moz', - 'box-sizing': '', - 'box-shadow': '', - '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', - 'flex': 'webkit ms', - 'flex-basis': 'webkit', - 'flex-direction': 'webkit ms', - 'flex-flow': 'webkit', - 'flex-grow': 'webkit', - 'flex-shrink': 'webkit', - '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 ms', - 'transform-origin': 'webkit ms', - 'transition': '', - 'transition-delay': '', - 'transition-duration': '', - 'transition-property': '', - 'transition-timing-function': '', - 'user-modify': 'webkit moz', - 'user-select': 'webkit moz ms', - 'word-break': 'epub ms', - 'writing-mode': 'epub ms', - }; - const applyTo = []; - let properties = []; - let inKeyFrame = false; - let started = 0; - - for (const prop in compatiblePrefixes) { - const variations = compatiblePrefixes[prop].split(' ').map(s => `-${s}-${prop}`); - compatiblePrefixes[prop] = variations; - applyTo.push(...variations); +}, (rule, parser, reporter) => { + const regex = /^\s?url\(['"].+\.eot\?.*['"]\)\s*format\(['"]embedded-opentype['"]\).*$/i; + let firstSrc = true; + let ruleFailed = false; + let pos; + // Mark the start of a @font-face declaration so we only test properties inside it + parser.addListener('startfontface', () => { + parser.addListener('property', property); + }); + function property(event) { + if (CSSLint.Util.getPropName(event.property) !== 'src') return; + const value = event.value.toString(); + pos = event; + const matched = regex.test(value); + if (firstSrc && !matched) { + ruleFailed = true; + firstSrc = false; + } else if (!firstSrc && matched) { + ruleFailed = false; } + } + // Back to normal rules that we don't need to test + parser.addListener('endfontface', () => { + parser.removeListener('property', property); + if (!ruleFailed) return; + reporter.report("@font-face declaration doesn't follow the fontspring bulletproof syntax.", + pos, rule); + }); +}]; - parser.addListener('startrule', () => { - started++; - properties = []; - }); +CSSLint.addRule['compatible-vendor-prefixes'] = [{ + name: 'Require compatible vendor prefixes', + desc: 'Include all compatible vendor prefixes to reach a wider range of users.', + url: 'https://github.com/CSSLint/csslint/wiki/Require-compatible-vendor-prefixes', + browsers: 'All', +}, (rule, parser, reporter) => { + // See http://peter.sh/experiments/vendor-prefixed-css-property-overview/ for details + const compatiblePrefixes = { + 'animation': 'webkit', + 'animation-delay': 'webkit', + 'animation-direction': 'webkit', + 'animation-duration': 'webkit', + 'animation-fill-mode': 'webkit', + 'animation-iteration-count': 'webkit', + 'animation-name': 'webkit', + 'animation-play-state': 'webkit', + 'animation-timing-function': 'webkit', + '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', + 'box-direction': 'webkit moz', + 'box-flex': 'webkit moz', + 'box-lines': 'webkit', + 'box-ordinal-group': 'webkit moz', + 'box-orient': 'webkit moz', + 'box-pack': 'webkit moz', + 'box-sizing': '', + 'box-shadow': '', + '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', + 'flex': 'webkit ms', + 'flex-basis': 'webkit', + 'flex-direction': 'webkit ms', + 'flex-flow': 'webkit', + 'flex-grow': 'webkit', + 'flex-shrink': 'webkit', + '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 ms', + 'transform-origin': 'webkit ms', + 'transition': '', + 'transition-delay': '', + 'transition-duration': '', + 'transition-property': '', + 'transition-timing-function': '', + 'user-modify': 'webkit moz', + 'user-select': 'webkit moz ms', + 'word-break': 'epub ms', + 'writing-mode': 'epub ms', + }; + const applyTo = []; + let properties = []; + let inKeyFrame = false; + let started = 0; - parser.addListener('startkeyframes', event => { - started++; - inKeyFrame = event.prefix || true; - if (inKeyFrame && typeof inKeyFrame === 'string') { - inKeyFrame = '-' + inKeyFrame + '-'; - } - }); + for (const prop in compatiblePrefixes) { + const variations = compatiblePrefixes[prop].split(' ').map(s => `-${s}-${prop}`); + compatiblePrefixes[prop] = variations; + applyTo.push(...variations); + } - parser.addListener('endkeyframes', () => { - started--; - inKeyFrame = false; - }); + parser.addListener('startrule', () => { + started++; + properties = []; + }); - parser.addListener('property', event => { - if (!started) return; - const name = event.property.text; - if (inKeyFrame && - typeof inKeyFrame === 'string' && - name.startsWith(inKeyFrame) || - applyTo.indexOf(name) < 0) { - return; - } - properties.push(event.property); - }); + parser.addListener('startkeyframes', event => { + started++; + inKeyFrame = event.prefix || true; + if (inKeyFrame && typeof inKeyFrame === 'string') { + inKeyFrame = '-' + inKeyFrame + '-'; + } + }); - parser.addListener('endrule', () => { - started = 0; - if (!properties.length) return; - const propertyGroups = {}; + parser.addListener('endkeyframes', () => { + started--; + inKeyFrame = false; + }); - for (const name of properties) { - for (const prop in compatiblePrefixes) { - const variations = compatiblePrefixes[prop]; - if (variations.indexOf(name.text) <= -1) continue; + parser.addListener('property', event => { + if (!started) return; + const name = event.property.text; + if (inKeyFrame && + typeof inKeyFrame === 'string' && + name.startsWith(inKeyFrame) || + !applyTo.includes(name)) { + return; + } + properties.push(event.property); + }); - if (!propertyGroups[prop]) { - propertyGroups[prop] = { - full: variations.slice(0), - actual: [], - actualNodes: [], - }; - } - - if (propertyGroups[prop].actual.indexOf(name.text) === -1) { - propertyGroups[prop].actual.push(name.text); - propertyGroups[prop].actualNodes.push(name); - } + parser.addListener('endrule', () => { + started = false; + if (!properties.length) return; + const groups = {}; + for (const name of properties) { + for (const prop in compatiblePrefixes) { + const variations = compatiblePrefixes[prop]; + if (!variations.includes(name.text)) { + continue; + } + if (!groups[prop]) { + groups[prop] = { + full: variations.slice(0), + actual: [], + actualNodes: [], + }; + } + if (!groups[prop].actual.includes(name.text)) { + groups[prop].actual.push(name.text); + groups[prop].actualNodes.push(name); } } - - for (const prop in propertyGroups) { - const value = propertyGroups[prop]; - const actual = value.actual; - if (value.full.length <= actual.length) continue; - - for (const item of value.full) { - if (actual.indexOf(item) !== -1) continue; - - const propertiesSpecified = - actual.length === 1 ? - actual[0] : - actual.length === 2 ? - actual.join(' and ') : - actual.join(', '); - - const {line, col} = value.actualNodes[0]; + } + for (const prop in groups) { + const value = groups[prop]; + const actual = value.actual; + const len = actual.length; + if (value.full.length <= len) continue; + for (const item of value.full) { + if (!actual.includes(item)) { + const spec = len === 1 ? actual[0] : len === 2 ? actual.join(' and ') : actual.join(', '); reporter.report( - `The property ${item} is compatible with ${propertiesSpecified} and should be included as well.`, - line, col, this); + `'${item}' is compatible with ${spec} and should be included as well.`, + value.actualNodes[0], rule); } } - }); - }, -}); + } + }); +}]; -CSSLint.addRule({ - id: 'display-property-grouping', - name: 'Require properties appropriate for display', - desc: "Certain properties shouldn't be used with certain display property values.", - url: 'https://github.com/CSSLint/csslint/wiki/Require-properties-appropriate-for-display', +CSSLint.addRule['display-property-grouping'] = [{ + name: 'Require properties appropriate for display', + desc: "Certain properties shouldn't be used with certain display property values.", + url: 'https://github.com/CSSLint/csslint/wiki/Require-properties-appropriate-for-display', browsers: 'All', - - init(parser, reporter) { - const 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, - }; - let properties; - let started = 0; - - const startRule = () => { - started = 1; +}, (rule, parser, reporter) => { + const 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, + }; + let properties; + let inRule; + const reportProperty = (name, display, msg) => { + const prop = properties[name]; + if (prop && propertiesToCheck[name] !== prop.value.toLowerCase()) { + reporter.report(msg || `'${name}' can't be used with display: ${display}.`, prop, rule); + } + }; + CSSLint.Util.registerRuleEvents(parser, { + start() { + inRule = true; properties = {}; - }; - - const property = event => { - if (!started) return; - const name = event.property.text.toLowerCase(); + }, + property(event) { + if (!inRule) return; + const name = CSSLint.Util.getPropName(event.property); if (name in propertiesToCheck) { properties[name] = { value: event.value.text, - line: event.property.line, - col: event.property.col, + line: event.property.line, + col: event.property.col, }; } - }; - - const reportProperty = (name, display, msg) => { - const prop = properties[name]; - if (!prop) return; - - const toCheck = propertiesToCheck[name]; - if (typeof toCheck === 'string' && toCheck === prop.value.toLowerCase()) return; - - const {line, col} = prop; - reporter.report(msg || `${name} can't be used with display: ${display}.`, - line, col, this); - }; - - const endRule = () => { - started = 0; + }, + end() { + inRule = false; const display = properties.display && properties.display.value; if (!display) return; @@ -850,7 +787,7 @@ CSSLint.addRule({ .forEach(p => reportProperty(p, display)); reportProperty('float', display, - 'display:inline has no effect on floated elements ' + + "'display:inline' has no effect on floated elements " + '(but may be used to fix the IE6 double-margin bug).'); break; @@ -866,962 +803,783 @@ CSSLint.addRule({ default: // margin, float should not be used with table - if (display.indexOf('table-') !== 0) { - return; + if (/^table-/i.test(display)) { + ['margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'float'] + .forEach(p => reportProperty(p, display)); } - ['margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'float'] - .forEach(p => reportProperty(p, display)); } - }; + }, + }); +}]; - CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property); - }, -}); - -CSSLint.addRule({ - id: 'duplicate-background-images', - name: 'Disallow duplicate background images', - desc: 'Every background-image should be unique. Use a common class for e.g. sprites.', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-duplicate-background-images', +CSSLint.addRule['duplicate-background-images'] = [{ + name: 'Disallow duplicate background images', + desc: 'Every background-image should be unique. Use a common class for e.g. sprites.', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-duplicate-background-images', browsers: 'All', - - init(parser, reporter) { - const stack = {}; - - parser.addListener('property', event => { - const name = event.property.text; - if (!name.match(/background/i)) return; - - for (const part of event.value.parts) { - if (part.type !== 'uri') continue; - - const uri = stack[part.uri]; - if (uri === undefined) { - stack[part.uri] = event; - continue; - } - +}, (rule, parser, reporter) => { + const stack = {}; + parser.addListener('property', event => { + if (!/^-(webkit|moz|ms|o)-background(-image)$/i.test(event.property.text)) { + return; + } + for (const part of event.value.parts) { + if (part.type !== 'uri') continue; + const uri = stack[part.uri]; + if (!uri) { + stack[part.uri] = event; + } else { reporter.report( `Background image '${part.uri}' was used multiple times, ` + `first declared at line ${uri.line}, col ${uri.col}.`, - event.line, event.col, this); + event, rule); } - }); - }, -}); + } + }); +}]; -CSSLint.addRule({ - id: 'duplicate-properties', - name: 'Disallow duplicate properties', - desc: 'Duplicate properties must appear one after the other.', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-duplicate-properties', +CSSLint.addRule['duplicate-properties'] = [{ + name: 'Disallow duplicate properties', + desc: 'Duplicate properties must appear one after the other. ' + + 'Exact duplicates are always reported.', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-duplicate-properties', browsers: 'All', - - init(parser, reporter) { - let properties, lastName; - let started = 0; - - const startRule = () => { - started = 1; - properties = {}; - }; - - const endRule = () => { - started = 0; - properties = {}; - }; - - const property = event => { - if (!started) return; +}, (rule, parser, reporter) => { + let props, lastName, inRule; + CSSLint.Util.registerRuleEvents(parser, { + start() { + inRule = true; + props = {}; + }, + property(event) { + if (!inRule) return; const property = event.property; const name = property.text.toLowerCase(); - const last = properties[name]; - if (last && (lastName !== name || last === event.value.text)) { - reporter.report(`Duplicate property '${property}' found.`, event.line, event.col, this); + const last = props[name]; + const dupValue = last === event.value.text; + if (last && (lastName !== name || dupValue)) { + reporter.report(`${dupValue ? 'Duplicate' : 'Ungrouped duplicate'} '${property}'.`, + event, rule); } - properties[name] = event.value.text; + props[name] = event.value.text; lastName = name; - }; + }, + end() { + inRule = false; + }, + }); +}]; - CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property); - }, -}); - -CSSLint.addRule({ - id: 'empty-rules', - name: 'Disallow empty rules', - desc: 'Rules without any properties specified should be removed.', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-empty-rules', +CSSLint.addRule['empty-rules'] = [{ + name: 'Disallow empty rules', + desc: 'Rules without any properties specified should be removed.', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-empty-rules', browsers: 'All', +}, (rule, parser, reporter) => { + let count = 0; + parser.addListener('startrule', () => (count = 0)); + parser.addListener('property', () => count++); + parser.addListener('endrule', event => { + if (!count) reporter.report('Empty rule.', event.selectors[0], rule); + }); +}]; - init(parser, reporter) { - let count = 0; - parser.addListener('startrule', () => (count = 0)); - parser.addListener('property', () => count++); - parser.addListener('endrule', event => { - if (!count) { - const {line, col} = event.selectors[0]; - reporter.report('Rule is empty.', line, col, this); - } - }); - }, - -}); - -CSSLint.addRule({ - id: 'errors', - name: 'Parsing Errors', - desc: 'This rule looks for recoverable syntax errors.', +CSSLint.addRule['errors'] = [{ + name: 'Parsing Errors', + desc: 'This rule looks for recoverable syntax errors.', browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('error', e => reporter.error(e.message, e, rule)); +}]; - init(parser, reporter) { - parser.addListener('error', ({message, line, col}) => { - reporter.error(message, line, col, this); - }); - }, -}); - -CSSLint.addRule({ - id: 'warnings', - name: 'Parsing warnings', - desc: 'This rule looks for parser warnings.', - browsers: 'All', - - init(parser, reporter) { - parser.addListener('warning', ({message, line, col}) => { - reporter.report(message, line, col, this); - }); - }, -}); - -CSSLint.addRule({ - id: 'fallback-colors', - name: 'Require fallback colors', - desc: "For older browsers that don't support RGBA, HSL, or HSLA, provide a fallback color.", - url: 'https://github.com/CSSLint/csslint/wiki/Require-fallback-colors', +CSSLint.addRule['fallback-colors'] = [{ + name: 'Require fallback colors', + desc: "For older browsers that don't support RGBA, HSL, or HSLA, provide a fallback color.", + url: 'https://github.com/CSSLint/csslint/wiki/Require-fallback-colors', browsers: 'IE6,IE7,IE8', - - init(parser, reporter) { - const propertiesToCheck = new Set([ - 'color', - 'background', - 'border-color', - 'border-top-color', - 'border-right-color', - 'border-bottom-color', - 'border-left-color', - 'border', - 'border-top', - 'border-right', - 'border-bottom', - 'border-left', - 'background-color', - ]); - let lastProperty; - const startRule = () => (lastProperty = null); - - CSSLint.Util.registerBlockEvents(parser, startRule, null, event => { - const name = event.property.text.toLowerCase(); +}, (rule, parser, reporter) => { + const propertiesToCheck = new Set([ + 'color', + 'background', + 'border-color', + 'border-top-color', + 'border-right-color', + 'border-bottom-color', + 'border-left-color', + 'border', + 'border-top', + 'border-right', + 'border-bottom', + 'border-left', + 'background-color', + ]); + let lastProperty; + CSSLint.Util.registerRuleEvents(parser, { + start() { + lastProperty = null; + }, + property(event) { + const name = CSSLint.Util.getPropName(event.property); if (!propertiesToCheck.has(name)) { lastProperty = event; return; } - let colorType = ''; for (const part of event.value.parts) { - if (part.type !== 'color') continue; - + if (part.type !== 'color') { + continue; + } if (!('alpha' in part || 'hue' in part)) { event.colorType = 'compat'; continue; } - if (/([^)]+)\(/.test(part)) { colorType = RegExp.$1.toUpperCase(); } - if (!lastProperty || - lastProperty.property.text.toLowerCase() !== name || - lastProperty.colorType !== 'compat') { + lastProperty.colorType !== 'compat' || + CSSLint.Util.getPropName(lastProperty.property) !== name) { reporter.report(`Fallback ${name} (hex or RGB) should precede ${colorType} ${name}.`, - event.line, event.col, this); + event, rule); } } lastProperty = event; - }); - }, -}); + }, + }); +}]; -CSSLint.addRule({ - id: 'floats', - name: 'Disallow too many floats', - desc: 'This rule tests if the float property is used too many times', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-too-many-floats', +CSSLint.addRule['floats'] = [{ + name: 'Disallow too many floats', + desc: 'This rule tests if the float property too many times', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-too-many-floats', browsers: 'All', +}, (rule, parser, reporter) => { + let count = 0; + parser.addListener('property', ({property, value}) => { + count += + CSSLint.Util.getPropName(property) === 'float' && + value.text.toLowerCase() !== 'none'; + }); + parser.addListener('endstylesheet', () => { + 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); + } + }); +}]; - init(parser, reporter) { - let count = 0; - - parser.addListener('property', ({property, value}) => { - count += - property.text.toLowerCase() === 'float' && - value.text.toLowerCase() !== 'none'; - }); - - parser.addListener('endstylesheet', () => { - 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.', this); - } - }); - }, - -}); - -CSSLint.addRule({ - id: 'font-faces', - name: "Don't use too many web fonts", - desc: 'Too many different web fonts in the same stylesheet.', - url: 'https://github.com/CSSLint/csslint/wiki/Don%27t-use-too-many-web-fonts', +CSSLint.addRule['font-faces'] = [{ + name: "Don't use too many web fonts", + desc: 'Too many different web fonts in the same stylesheet.', + url: 'https://github.com/CSSLint/csslint/wiki/Don%27t-use-too-many-web-fonts', browsers: 'All', +}, (rule, parser, reporter) => { + let count = 0; + parser.addListener('startfontface', () => count++); + parser.addListener('endstylesheet', () => { + if (count > 5) { + reporter.rollupWarn(`Too many @font-face declarations (${count}).`, rule); + } + }); +}]; - init(parser, reporter) { - let count = 0; - parser.addListener('startfontface', () => count++); - parser.addListener('endstylesheet', () => { - if (count > 5) { - reporter.rollupWarn(`Too many @font-face declarations (${count}).`, this); - } - }); - }, - -}); - -CSSLint.addRule({ - id: 'font-sizes', - name: 'Disallow too many font sizes', - desc: 'Checks the number of font-size declarations.', - url: 'https://github.com/CSSLint/csslint/wiki/Don%27t-use-too-many-font-size-declarations', +CSSLint.addRule['font-sizes'] = [{ + name: 'Disallow too many font sizes', + desc: 'Checks the number of font-size declarations.', + url: 'https://github.com/CSSLint/csslint/wiki/Don%27t-use-too-many-font-size-declarations', browsers: 'All', +}, (rule, parser, reporter) => { + let count = 0; + parser.addListener('property', event => { + count += CSSLint.Util.getPropName(event.property) === 'font-size'; + }); + parser.addListener('endstylesheet', () => { + reporter.stat('font-sizes', count); + if (count >= 10) { + reporter.rollupWarn(`Too many font-size declarations (${count}), abstraction needed.`, rule); + } + }); +}]; - init(parser, reporter) { - let count = 0; - - parser.addListener('property', event => { - count += event.property.toString() === 'font-size'; - }); - - parser.addListener('endstylesheet', () => { - reporter.stat('font-sizes', count); - if (count >= 10) { - reporter.rollupWarn('Too many font-size declarations (' + count + '), abstraction needed.', this); - } - }); - }, - -}); - -CSSLint.addRule({ - - id: 'gradients', - name: 'Require all gradient definitions', - desc: 'When using a vendor-prefixed gradient, make sure to use them all.', - url: 'https://github.com/CSSLint/csslint/wiki/Require-all-gradient-definitions', +CSSLint.addRule['gradients'] = [{ + name: 'Require all gradient definitions', + desc: 'When using a vendor-prefixed gradient, make sure to use them all.', + url: 'https://github.com/CSSLint/csslint/wiki/Require-all-gradient-definitions', browsers: 'All', - - init(parser, reporter) { - let gradients; - - parser.addListener('startrule', () => { +}, (rule, parser, reporter) => { + let gradients; + CSSLint.Util.registerRuleEvents(parser, { + start() { gradients = { - moz: 0, - webkit: 0, + moz: 0, + webkit: 0, oldWebkit: 0, - o: 0, + o: 0, }; - }); - - parser.addListener('property', event => { + }, + property(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', event => { + }, + end(event) { const 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) { - const {line, col} = event.selectors[0]; reporter.report(`Missing vendor-prefixed CSS gradients for ${missing.join(', ')}.`, - line, col, this); + event.selectors[0], rule); } - }); - }, -}); + }, + }); +}]; -CSSLint.addRule({ - id: 'ids', - name: 'Disallow IDs in selectors', - desc: 'Selectors should not contain IDs.', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-IDs-in-selectors', +CSSLint.addRule['ids'] = [{ + name: 'Disallow IDs in selectors', + desc: 'Selectors should not contain IDs.', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-IDs-in-selectors', browsers: 'All', - - init(parser, reporter) { - parser.addListener('startrule', event => { - for (const {line, col, parts} of event.selectors) { - const idCount = - parts.reduce((sum = 0, {type, modifiers}) => - type === parser.SELECTOR_PART_TYPE ? - modifiers.reduce(sum, mod => sum + (mod.type === 'id')) : - sum); - if (idCount === 1) { - reporter.report("Don't use IDs in selectors.", line, col, this); - } else if (idCount > 1) { - reporter.report(idCount + ' IDs in the selector, really?', line, col, this); - } +}, (rule, parser, reporter) => { + parser.addListener('startrule', event => { + for (const sel of event.selectors) { + const cnt = + sel.parts.reduce((sum = 0, {type, modifiers}) => + type === parser.SELECTOR_PART_TYPE + ? modifiers.reduce(sum, mod => sum + (mod.type === 'id')) + : sum); + if (cnt) { + reporter.report(`Id in selector${cnt > 1 ? '!'.repeat(cnt) : '.'}`, sel, rule); } - }); - }, -}); + } + }); +}]; -CSSLint.addRule({ - id: 'import-ie-limit', - name: '@import limit on IE6-IE9', - desc: 'IE6-9 supports up to 31 @import per stylesheet', +CSSLint.addRule['import-ie-limit'] = [{ + name: '@import limit on IE6-IE9', + desc: 'IE6-9 supports up to 31 @import per stylesheet', browsers: 'IE6, IE7, IE8, IE9', +}, (rule, parser, reporter) => { + const MAX_IMPORT_COUNT = 31; + let count = 0; + parser.addListener('startpage', () => (count = 0)); + parser.addListener('import', () => count++); + parser.addListener('endstylesheet', () => { + if (count > MAX_IMPORT_COUNT) { + reporter.rollupError( + `Too many @import rules (${count}). IE6-9 supports up to 31 import per stylesheet.`, + rule); + } + }); +}]; - init(parser, reporter) { - const MAX_IMPORT_COUNT = 31; - let count = 0; - parser.addListener('startpage', () => (count = 0)); - parser.addListener('import', () => count++); - parser.addListener('endstylesheet', () => { - if (count > MAX_IMPORT_COUNT) { - reporter.rollupError( - `Too many @import rules (${count}). IE6-9 supports up to 31 import per stylesheet.`, - this); - } - }); - }, -}); - -CSSLint.addRule({ - id: 'import', - name: 'Disallow @import', - desc: "Don't use @import, use instead.", - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-%40import', +CSSLint.addRule['import'] = [{ + name: 'Disallow @import', + desc: "Don't use @import, use instead.", + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-%40import', browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('import', e => { + reporter.report('@import prevents parallel downloads, use instead.', e, rule); + }); +}]; - init(parser, reporter) { - parser.addListener('import', ({line, col}) => { - reporter.report('@import prevents parallel downloads, use instead.', line, col, this); - }); - }, -}); - -CSSLint.addRule({ - id: 'important', - name: 'Disallow !important', - desc: 'Be careful when using !important declaration', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-%21important', +CSSLint.addRule['important'] = [{ + name: 'Disallow !important', + desc: 'Be careful when using !important declaration', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-%21important', browsers: 'All', - - init(parser, reporter) { - let count = 0; - - parser.addListener('property', event => { - if (!event.important) return; +}, (rule, parser, reporter) => { + let count = 0; + parser.addListener('property', event => { + if (event.important) { count++; - reporter.report('Use of !important', event.line, event.col, this); - }); + reporter.report('!important.', event, rule); + } + }); + parser.addListener('endstylesheet', () => { + 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); + } + }); +}]; - parser.addListener('endstylesheet', () => { - reporter.stat('important', count); - if (count >= 10) { - reporter.rollupWarn( - `Too many !important declarations (${count}), ` + - 'try to use less than 10 to avoid specificity issues.', this); +CSSLint.addRule['known-properties'] = [{ + name: 'Require use of known properties', + desc: 'Properties should be known (listed in CSS3 specification) or be a vendor-prefixed property.', + url: 'https://github.com/CSSLint/csslint/wiki/Require-use-of-known-properties', + browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('property', event => { + const inv = event.invalid; + if (inv) reporter.report(inv.message, inv, rule); + }); +}]; + +CSSLint.addRule['order-alphabetical'] = [{ + name: 'Alphabetical order', + desc: 'Assure properties are in alphabetical order', + browsers: 'All', +}, (rule, parser, reporter) => { + let last, failed; + CSSLint.Util.registerRuleEvents(parser, { + start() { + last = ''; + failed = false; + }, + property(event) { + if (!failed) { + const name = CSSLint.Util.getPropName(event.property); + if (name < last) { + reporter.report(`Non-alphabetical order: '${name}'.`, event, rule); + failed = true; + } + last = name; } - }); - }, + }, + }); +}]; -}); - -CSSLint.addRule({ - id: 'known-properties', - name: 'Require use of known properties', - desc: 'Properties should be known (listed in CSS3 specification) or be a vendor-prefixed property.', - url: 'https://github.com/CSSLint/csslint/wiki/Require-use-of-known-properties', +CSSLint.addRule['outline-none'] = [{ + name: 'Disallow outline: none', + desc: 'Use of outline: none or outline: 0 should be limited to :focus rules.', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-outline%3Anone', browsers: 'All', - - init(parser, reporter) { - parser.addListener('property', event => { - const inv = event.invalid; - if (inv) reporter.report(inv.message, inv.line, inv.col, this); - }); - }, -}); - -CSSLint.addRule({ - id: 'order-alphabetical', - name: 'Alphabetical order', - desc: 'Assure properties are in alphabetical order', - browsers: 'All', - - init(parser, reporter) { - let properties; - let started = 0; - - const startRule = () => { - started = 1; - properties = []; - }; - - const property = event => { - if (!started) return; - const name = event.property.text; - const lowerCasePrefixLessName = name.toLowerCase().replace(/^-.*?-/, ''); - properties.push(lowerCasePrefixLessName); - }; - - const endRule = event => { - started = 0; - if (properties.join(',') !== properties.sort().join(',')) { - reporter.report("Rule doesn't have all its properties in alphabetical order.", event.line, event.col, this); - } - }; - - CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property); - }, -}); - -CSSLint.addRule({ - id: 'outline-none', - name: 'Disallow outline: none', - desc: 'Use of outline: none or outline: 0 should be limited to :focus rules.', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-outline%3Anone', - browsers: 'All', - tags: ['Accessibility'], - - init(parser, reporter) { - let lastRule; - - const startRule = event => { + tags: ['Accessibility'], +}, (rule, parser, reporter) => { + let lastRule; + CSSLint.Util.registerRuleEvents(parser, { + start(event) { lastRule = !event.selectors ? null : { - line: event.line, - col: event.col, + line: event.line, + col: event.col, selectors: event.selectors, propCount: 0, - outline: false, + outline: false, }; - }; - - const property = event => { + }, + property(event) { if (!lastRule) return; - const name = event.property.text.toLowerCase(); - const value = event.value; lastRule.propCount++; - if (name === 'outline' && /^(none|0)$/i.test(value)) { + if (CSSLint.Util.getPropName(event.property) === 'outline' && /^(none|0)$/i.test(event.value)) { lastRule.outline = true; } - }; - - const endRule = () => { - const {outline, selectors, propCount, line, col} = lastRule || {}; + }, + end() { + const {outline, selectors, propCount} = lastRule || {}; lastRule = null; if (!outline) return; - if (selectors.toString().toLowerCase().indexOf(':focus') === -1) { - reporter.report('Outlines should only be modified using :focus.', line, col, this); + if (!/:focus/i.test(selectors)) { + reporter.report('Outlines should only be modified using :focus.', lastRule, rule); } else if (propCount === 1) { reporter.report("Outlines shouldn't be hidden unless other visual changes are made.", - line, col, this); + lastRule, rule); } - }; + }, + }); +}]; - CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property); - }, -}); - -CSSLint.addRule({ - id: 'overqualified-elements', - name: 'Disallow overqualified elements', - desc: "Don't use classes or IDs with elements (a.foo or a#foo).", - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-overqualified-elements', +CSSLint.addRule['overqualified-elements'] = [{ + name: 'Disallow overqualified elements', + desc: "Don't use classes or IDs with elements (a.foo or a#foo).", + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-overqualified-elements', browsers: 'All', +}, (rule, parser, reporter) => { + const classes = {}; + const report = (part, mod) => { + reporter.report(`'${part}' is overqualified, just use '${mod}' without element name.`, + part, rule); + }; + parser.addListener('startrule', event => { + for (const selector of event.selectors) { + for (const part of selector.parts) { + if (part.type !== parser.SELECTOR_PART_TYPE) continue; + for (const mod of part.modifiers) { + if (part.elementName && mod.type === 'id') { + report(part, mod); + } else if (mod.type === 'class') { + (classes[mod] || (classes[mod] = [])) + .push({modifier: mod, part}); + } + } + } + } + }); + // one use means that this is overqualified + parser.addListener('endstylesheet', () => { + for (const prop of Object.values(classes)) { + const {part, modifier} = prop[0]; + if (part.elementName && prop.length === 1) { + report(part, modifier); + } + } + }); +}]; - init(parser, reporter) { - const classes = {}; +CSSLint.addRule['qualified-headings'] = [{ + name: 'Disallow qualified headings', + desc: 'Headings should not be qualified (namespaced).', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-qualified-headings', + browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('startrule', event => { + for (const selector of event.selectors) { + let first = true; + for (const part of selector.parts) { + const name = part.elementName; + if (!first && + name && + part.type === parser.SELECTOR_PART_TYPE && + /h[1-6]/.test(name.toString())) { + reporter.report(`Heading '${name}' should not be qualified.`, part, rule); + } + first = false; + } + } + }); +}]; - parser.addListener('startrule', event => { - for (const selector of event.selectors) { - for (const part of selector.parts) { - if (part.type !== parser.SELECTOR_PART_TYPE) continue; +CSSLint.addRule['regex-selectors'] = [{ + name: 'Disallow selectors that look like regexs', + desc: 'Selectors that look like regular expressions are slow and should be avoided.', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-selectors-that-look-like-regular-expressions', + browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('startrule', event => { + for (const selector of event.selectors) { + for (const part of selector.parts) { + if (part.type === parser.SELECTOR_PART_TYPE) { for (const mod of part.modifiers) { - if (part.elementName && mod.type === 'id') { - reporter.report('Element (' + part + ') is overqualified, just use ' + mod + - ' without element name.', part.line, part.col, this); - } else if (mod.type === 'class') { - let classMods = classes[mod]; - if (!classMods) classMods = classes[mod] = []; - classMods.push({modifier: mod, part}); + if (mod.type === 'attribute' && /([~|^$*]=)/.test(mod)) { + reporter.report(`Slow attribute selector ${RegExp.$1}.`, mod, rule); } } } } - }); + } + }); +}]; - // one use means that this is overqualified - parser.addListener('endstylesheet', () => { - for (const prop in classes) { - const {part, modifier} = classes[prop][0]; - if (part.elementName && classes[prop].length === 1) { - reporter.report(`Element (${part}) is overqualified, just use ${modifier} without element name.`, - part.line, part.col, this); - } - } - }); - }, -}); - -CSSLint.addRule({ - id: 'qualified-headings', - name: 'Disallow qualified headings', - desc: 'Headings should not be qualified (namespaced).', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-qualified-headings', +CSSLint.addRule['rules-count'] = [{ + name: 'Rules Count', + desc: 'Track how many rules there are.', browsers: 'All', +}, (rule, parser, reporter) => { + let count = 0; + parser.addListener('startrule', () => count++); + parser.addListener('endstylesheet', () => reporter.stat('rule-count', count)); +}]; - init(parser, reporter) { - parser.addListener('startrule', event => { - for (const selector of event.selectors) { - let first = true; - for (const part of selector.parts) { - const name = part.elementName; - if (!first && - name && - part.type === parser.SELECTOR_PART_TYPE && - /h[1-6]/.test(name.toString())) { - reporter.report(`Heading (${name}) should not be qualified.`, - part.line, part.col, this); - } - first = false; - } - } - }); - }, - -}); - -CSSLint.addRule({ - id: 'regex-selectors', - name: 'Disallow selectors that look like regexs', - desc: 'Selectors that look like regular expressions are slow and should be avoided.', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-selectors-that-look-like-regular-expressions', - browsers: 'All', - - init(parser, reporter) { - parser.addListener('startrule', event => { - for (const selector of event.selectors) { - for (const part of selector.parts) { - if (part.type !== parser.SELECTOR_PART_TYPE) continue; - for (const mod of part.modifiers) { - if (mod.type !== 'attribute' || !/([~|^$*]=)/.test(mod)) continue; - reporter.report(`Attribute selectors with ${RegExp.$1} are slow!`, - mod.line, mod.col, this); - } - } - } - }); - }, - -}); - -CSSLint.addRule({ - id: 'rules-count', - name: 'Rules Count', - desc: 'Track how many rules there are.', - browsers: 'All', - - init(parser, reporter) { - let count = 0; - parser.addListener('startrule', () => count++); - parser.addListener('endstylesheet', () => reporter.stat('rule-count', count)); - }, -}); - -CSSLint.addRule({ - id: 'selector-max-approaching', - name: 'Warn when approaching the 4095 selector limit for IE', - desc: 'Will warn when selector count is >= 3800 selectors.', +CSSLint.addRule['selector-max'] = [{ + name: 'Error when past the 4095 selector limit for IE', + desc: 'Will error when selector count is > 4095.', browsers: 'IE', +}, (rule, parser, reporter, limit = 4095) => { + let count = 0; + parser.addListener('startrule', event => { + count += event.selectors.length; + }); + parser.addListener('endstylesheet', () => { + if (count > limit) { + reporter.report(count + ' selectors found. ' + + 'Internet Explorer supports a maximum of 4095 selectors per stylesheet. ' + + 'Consider refactoring.', {}, rule); + } + }); +}]; - init(parser, reporter) { - let count = 0; - parser.addListener('startrule', event => (count += event.selectors.length)); - parser.addListener('endstylesheet', () => { - if (count >= 3800) { - reporter.report( - `You have ${count} selectors. ` + - 'Internet Explorer supports a maximum of 4095 selectors per stylesheet. ' + - 'Consider refactoring.', 0, 0, this); - } - }); - }, - -}); - -CSSLint.addRule({ - id: 'selector-max', - name: 'Error when past the 4095 selector limit for IE', - desc: 'Will error when selector count is > 4095.', +CSSLint.addRule['selector-max-approaching'] = [{ + name: 'Warn when approaching the 4095 selector limit for IE', + desc: 'Will warn when selector count is >= 3800 selectors.', browsers: 'IE', +}, (rule, parser, reporter) => { + CSSLint.rules['selector-max'].init(rule, parser, reporter, Number(rule.desc.match(/\d+/)[0])); +}]; - init(parser, reporter) { - let count = 0; - parser.addListener('startrule', event => (count += event.selectors.length)); - parser.addListener('endstylesheet', () => { - if (count > 4095) { - reporter.report( - `You have ${count} selectors. ` + - 'Internet Explorer supports a maximum of 4095 selectors per stylesheet. ' + - 'Consider refactoring.', 0, 0, this); - } - }); - }, -}); - -CSSLint.addRule({ - id: 'selector-newline', - name: 'Disallow new-line characters in selectors', - desc: 'New-line characters in selectors are usually a forgotten comma and not a descendant combinator.', +CSSLint.addRule['selector-newline'] = [{ + name: 'Disallow new-line characters in selectors', + desc: 'New-line characters in selectors are usually a forgotten comma and not a descendant combinator.', browsers: 'All', - - init(parser, reporter) { - parser.addListener('startrule', event => { - for (const {parts} of event.selectors) { - for (let i = 0, p, pn; i < parts.length - 1 && (p = parts[i]); i++) { - if (p.type === 'descendant' && (pn = parts[i + 1]).line > p.line) { - reporter.report('newline character found in selector (forgot a comma?)', - pn.line, pn.col, this); - } +}, (rule, parser, reporter) => { + parser.addListener('startrule', event => { + for (const {parts} of event.selectors) { + for (let i = 0, p, pn; i < parts.length - 1 && (p = parts[i]); i++) { + if (p.type === 'descendant' && (pn = parts[i + 1]).line > p.line) { + reporter.report('Line break in selector (forgot a comma?)', pn, rule); } } - }); - }, -}); + } + }); +}]; -CSSLint.addRule({ - id: 'shorthand', - name: 'Require shorthand properties', - desc: 'Use shorthand properties where possible.', - url: 'https://github.com/CSSLint/csslint/wiki/Require-shorthand-properties', +CSSLint.addRule['shorthand'] = [{ + name: 'Require shorthand properties', + desc: 'Use shorthand properties where possible.', + url: 'https://github.com/CSSLint/csslint/wiki/Require-shorthand-properties', browsers: 'All', - - init(parser, reporter) { - const {shorthands} = CSSLint.Util; - CSSLint.Util.registerShorthandEvents(parser, { - endRule: (event, props) => { - for (const [sh, events] of Object.entries(props)) { - const names = Object.keys(events); - if (names.length === shorthands[sh].length) { - const msg = `'${sh}' shorthand can replace '${names.join("' + '")}'`; - names.forEach(n => reporter.report(msg, events[n].line, events[n].col, this)); - } +}, (rule, parser, reporter) => { + const {shorthands} = CSSLint.Util; + CSSLint.Util.registerShorthandEvents(parser, { + end(event, props) { + for (const [sh, events] of Object.entries(props)) { + const names = Object.keys(events); + if (names.length === shorthands[sh].length) { + const msg = `'${sh}' shorthand can replace '${names.join("' + '")}'`; + names.forEach(n => reporter.report(msg, events[n], rule)); } - }, - }); - }, -}); + } + }, + }); +}]; -CSSLint.addRule({ - id: 'shorthand-overrides', +CSSLint.addRule['shorthand-overrides'] = [{ name: 'Avoid shorthands that override individual properties', desc: 'Avoid shorthands like `background: foo` that follow individual properties ' + 'like `background-image: bar` thus overriding them', browsers: 'All', +}, (rule, parser, reporter) => { + CSSLint.Util.registerShorthandEvents(parser, { + property(event, props, name) { + const ovr = props[name]; + if (ovr) { + delete props[name]; + reporter.report(`'${event.property}' overrides '${Object.keys(ovr).join("', '")}' above.`, + event, rule); + } + }, + }); +}]; - init(parser, reporter) { - CSSLint.Util.registerShorthandEvents(parser, { - property: (event, props, name) => { - const ovr = props[name]; - if (ovr) { - delete props[name]; - reporter.report(`'${event.property}' overrides '${Object.keys(ovr).join("', '")}' above.`, - event.line, event.col, this); - } - }, - }); - }, -}); - -CSSLint.addRule({ - id: 'simple-not', - name: 'Require use of simple selectors inside :not()', - desc: 'A complex selector inside :not() is only supported by CSS4-compliant browsers.', +CSSLint.addRule['simple-not'] = [{ + name: 'Require use of simple selectors inside :not()', + desc: 'A complex selector inside :not() is only supported by CSS4-compliant browsers.', browsers: 'All', - - init(parser, reporter) { - parser.addListener('startrule', e => { - for (const sel of e.selectors) { - if (!/:not\(/i.test(sel.text)) continue; - for (const part of sel.parts) { - if (!part.modifiers) continue; - for (const mod of part.modifiers) { - if (mod.type !== 'not') continue; - const {args} = mod; - const {parts} = args[0]; - if (args.length > 1 || +}, (rule, parser, reporter) => { + parser.addListener('startrule', e => { + for (const sel of e.selectors) { + if (!/:not\(/i.test(sel.text)) continue; + for (const part of sel.parts) { + if (!part.modifiers) continue; + for (const mod of part.modifiers) { + if (mod.type !== 'not') continue; + const {args} = mod; + const {parts} = args[0]; + if (args.length > 1 || parts.length !== 1 || parts[0].modifiers.length + (parts[0].elementName ? 1 : 0) > 1 || /^:not\(/i.test(parts[0])) { - reporter.report( - `Simple selector expected, but found '${args.join(', ')}'`, - args[0].line, args[0].col, this); - } + reporter.report('Complex selector inside :not().', args[0], rule); } } } - }); - }, -}); + } + }); +}]; -CSSLint.addRule({ - id: 'star-property-hack', - name: 'Disallow properties with a star prefix', - desc: 'Checks for the star property hack (targets IE6/7)', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-star-hack', +CSSLint.addRule['star-property-hack'] = [{ + name: 'Disallow properties with a star prefix', + desc: 'Checks for the star property hack (targets IE6/7)', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-star-hack', browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('property', ({property}) => { + if (property.hack === '*') { + reporter.report('IE star prefix.', property, rule); + } + }); +}]; - init(parser, reporter) { - parser.addListener('property', ({property: {hack, line, col}}) => { - if (hack === '*') { - reporter.report('Property with star prefix found.', line, col, this); - } - }); - }, -}); - -CSSLint.addRule({ - id: 'text-indent', - name: 'Disallow negative text-indent', - desc: 'Checks for text indent less than -99px', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-negative-text-indent', +CSSLint.addRule['text-indent'] = [{ + name: 'Disallow negative text-indent', + desc: 'Checks for text indent less than -99px', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-negative-text-indent', browsers: 'All', - - init(parser, reporter) { - let textIndent, direction; - - const startRule = () => { +}, (rule, parser, reporter) => { + let textIndent, isLtr; + CSSLint.Util.registerRuleEvents(parser, { + start() { textIndent = false; - direction = 'inherit'; - }; - - const endRule = () => { - 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, this); - } - }; - - parser.addListener('startrule', startRule); - parser.addListener('startfontface', startRule); - - parser.addListener('property', event => { - const name = event.property.toString().toLowerCase(); + isLtr = false; + }, + property(event) { + const name = CSSLint.Util.getPropName(event.property); const value = event.value; - if (name === 'text-indent' && value.parts[0].value < -99) { textIndent = event.property; - } else if (name === 'direction' && value.toString().toLowerCase() === 'ltr') { - direction = 'ltr'; + } else if (name === 'direction' && /^ltr$/i.test(value)) { + isLtr = true; } - }); + }, + end() { + if (textIndent && !isLtr) { + 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, rule); + } + }, + }); +}]; - parser.addListener('endrule', endRule); - parser.addListener('endfontface', endRule); - }, -}); - -CSSLint.addRule({ - id: 'underscore-property-hack', - name: 'Disallow properties with an underscore prefix', - desc: 'Checks for the underscore property hack (targets IE6)', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-underscore-hack', +CSSLint.addRule['underscore-property-hack'] = [{ + name: 'Disallow properties with an underscore prefix', + desc: 'Checks for the underscore property hack (targets IE6)', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-underscore-hack', browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('property', ({property}) => { + if (property.hack === '_') { + reporter.report('IE underscore prefix.', property, rule); + } + }); +}]; - init(parser, reporter) { - parser.addListener('property', ({property: {hack, line, col}}) => { - if (hack === '_') { - reporter.report('Property with underscore prefix found.', line, col, this); - } - }); - }, -}); - -CSSLint.addRule({ - id: 'unique-headings', - name: 'Headings should only be defined once', - desc: 'Headings should be defined only once.', - url: 'https://github.com/CSSLint/csslint/wiki/Headings-should-only-be-defined-once', +CSSLint.addRule['unique-headings'] = [{ + name: 'Headings should only be defined once', + desc: 'Headings should be defined only once.', + url: 'https://github.com/CSSLint/csslint/wiki/Headings-should-only-be-defined-once', browsers: 'All', - - init(parser, reporter) { - const headings = new Array(6).fill(0); - - parser.addListener('startrule', event => { - for (const {parts} of event.selectors) { - const part = parts[parts.length - 1]; - if (!part.elementName || !/h([1-6])/i.test(part.elementName)) continue; - if (part.modifiers.some(mod => mod.type === 'pseudo')) continue; - if (++headings[Number(RegExp.$1) - 1] > 1) { - reporter.report(`Heading (${part.elementName}) has already been defined.`, - part.line, part.col, this); - } +}, (rule, parser, reporter) => { + const headings = new Array(6).fill(0); + parser.addListener('startrule', event => { + for (const {parts} of event.selectors) { + const p = parts[parts.length - 1]; + if (/h([1-6])/i.test(p.elementName) && + !p.modifiers.some(mod => mod.type === 'pseudo') && + ++headings[RegExp.$1 - 1] > 1) { + reporter.report(`Heading ${p.elementName} has already been defined.`, p, rule); } - }); + } + }); + parser.addListener('endstylesheet', () => { + const stats = headings + .filter(h => h > 1) + .map((h, i) => `${h} H${i + 1}s`); + if (stats.length) { + reporter.rollupWarn(stats.join(', '), rule); + } + }); +}]; - parser.addListener('endstylesheet', () => { - const messages = headings - .filter(h => h > 1) - .map((h, i) => `${h} H${i + 1}s`); - if (messages.length) { - reporter.rollupWarn(`You have ${messages.join(', ')} defined in this stylesheet.`, this); - } - }); - }, -}); - -CSSLint.addRule({ - id: 'universal-selector', - name: 'Disallow universal selector', - desc: 'The universal selector (*) is known to be slow.', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-universal-selector', +CSSLint.addRule['universal-selector'] = [{ + name: 'Disallow universal selector', + desc: 'The universal selector (*) is known to be slow.', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-universal-selector', browsers: 'All', - - init(parser, reporter) { - parser.addListener('startrule', event => { - for (const {parts} of event.selectors) { - const part = parts[parts.length - 1]; - if (part.elementName === '*') { - reporter.report(this.desc, part.line, part.col, this); - } +}, (rule, parser, reporter) => { + parser.addListener('startrule', event => { + for (const {parts} of event.selectors) { + const part = parts[parts.length - 1]; + if (part.elementName === '*') { + reporter.report(rule.desc, part, rule); } - }); - }, -}); + } + }); +}]; -CSSLint.addRule({ - id: 'unqualified-attributes', - name: 'Disallow unqualified attribute selectors', - desc: 'Unqualified attribute selectors are known to be slow.', - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-unqualified-attribute-selectors', +CSSLint.addRule['unqualified-attributes'] = [{ + name: 'Disallow unqualified attribute selectors', + desc: 'Unqualified attribute selectors are known to be slow.', + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-unqualified-attribute-selectors', browsers: 'All', - - init(parser, reporter) { - parser.addListener('startrule', event => { - for (const {parts} of event.selectors) { - const part = parts[parts.length - 1]; - if (part.type !== parser.SELECTOR_PART_TYPE) continue; - if (part.modifiers.some(mod => mod.type === 'class' || mod.type === 'id')) continue; - +}, (rule, parser, reporter) => { + parser.addListener('startrule', event => { + for (const {parts} of event.selectors) { + const part = parts[parts.length - 1]; + if (part.type === parser.SELECTOR_PART_TYPE && + !part.modifiers.some(mod => mod.type === 'class' || mod.type === 'id')) { const isUnqualified = !part.elementName || part.elementName === '*'; for (const mod of part.modifiers) { if (mod.type === 'attribute' && isUnqualified) { - reporter.report(this.desc, part.line, part.col, this); + reporter.report(rule.desc, part, rule); } } } - }); - }, -}); + } + }); +}]; -CSSLint.addRule({ - id: 'vendor-prefix', - name: 'Require standard property with vendor prefix', - desc: 'When using a vendor-prefixed property, make sure to include the standard one.', - url: 'https://github.com/CSSLint/csslint/wiki/Require-standard-property-with-vendor-prefix', +CSSLint.addRule['vendor-prefix'] = [{ + name: 'Require standard property with vendor prefix', + desc: 'When using a vendor-prefixed property, make sure to include the standard one.', + url: 'https://github.com/CSSLint/csslint/wiki/Require-standard-property-with-vendor-prefix', browsers: 'All', - - init(parser, reporter) { - const 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', - }; - let properties, num, started; - - const startRule = () => { - started = 1; +}, (rule, parser, reporter) => { + const 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', + }; + let properties, num, inRule; + CSSLint.Util.registerRuleEvents(parser, { + start() { + inRule = true; properties = {}; num = 1; - }; - - const endRule = () => { - started = 0; + }, + property(event) { + if (!inRule) return; + const name = CSSLint.Util.getPropName(event.property); + let prop = properties[name]; + if (!prop) prop = properties[name] = []; + prop.push({ + name: event.property, + value: event.value, + pos: num++, + }); + }, + end() { + inRule = false; const needsStandard = []; - for (const prop in properties) { if (prop in propertiesToCheck) { needsStandard.push({ @@ -1830,49 +1588,42 @@ CSSLint.addRule({ }); } } - for (const {needed, actual} of needsStandard) { - const {line, col} = properties[actual][0].name; + const unit = properties[actual][0].name; if (!properties[needed]) { reporter.report(`Missing standard property '${needed}' to go along with '${actual}'.`, - line, col, this); + unit, rule); } else if (properties[needed][0].pos < properties[actual][0].pos) { - reporter.report(`Standard property '${needed}' should come after vendor-prefixed property '${actual}'.`, - line, col, this); + reporter.report( + `Standard property '${needed}' should come after vendor-prefixed property '${actual}'.`, + unit, rule); } } - }; + }, + }); +}]; - CSSLint.Util.registerBlockEvents(parser, startRule, endRule, event => { - if (!started) return; - const name = event.property.text.toLowerCase(); - let prop = properties[name]; - if (!prop) prop = properties[name] = []; - prop.push({ - name: event.property, - value: event.value, - pos: num++, - }); - }); - }, -}); - -CSSLint.addRule({ - id: 'zero-units', - name: 'Disallow units for 0 values', - desc: "You don't need to specify units when a value is 0.", - url: 'https://github.com/CSSLint/csslint/wiki/Disallow-units-for-zero-values', +CSSLint.addRule['warnings'] = [{ + name: 'Parsing warnings', + desc: 'This rule looks for parser warnings.', browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('warning', e => reporter.report(e.message, e, rule)); +}]; - init(parser, reporter) { - parser.addListener('property', event => { - for (const {units, type, value, line, col} of event.value.parts) { - if ((units || type === 'percentage') && value === 0 && type !== 'time') { - reporter.report("Values of 0 shouldn't have units specified.", line, col, this); - } +CSSLint.addRule['zero-units'] = [{ + name: 'Disallow units for 0 values', + desc: "You don't need to specify units when a value is 0.", + url: 'https://github.com/CSSLint/csslint/wiki/Disallow-units-for-zero-values', + browsers: 'All', +}, (rule, parser, reporter) => { + parser.addListener('property', event => { + for (const p of event.value.parts) { + if (p.value === 0 && (p.units || p.type === 'percentage') && p.type !== 'time') { + reporter.report("'0' value with redundant units.", p, rule); } - }); - }, -}); + } + }); +}]; -//endregion +//#endregion diff --git a/js/csslint/parserlib.js b/js/csslint/parserlib.js index ed077e3f..517a6dc7 100644 --- a/js/csslint/parserlib.js +++ b/js/csslint/parserlib.js @@ -4666,6 +4666,7 @@ self.parserlib = (() => { SyntaxUnit, EventTarget, TokenStreamBase, + rxVendorPrefix, }, cache: parserCache, };