stylus/dist/js/csslint/csslint.js

1872 lines
59 KiB
JavaScript
Raw Normal View History

/*
Modded by tophf <github.com/tophf>
========== Original disclaimer:
Copyright (c) 2016 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.
*/
/* global parserlib */
'use strict';
2021-01-05 13:46:21 +00:00
//#region Reporter
class Reporter {
/**
* 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.
* @param {Object} allow - explicitly allowed lines
* @param {[][]} ignore - list of line ranges to be ignored
*/
constructor(lines, ruleset, allow, ignore) {
this.messages = [];
this.stats = [];
this.lines = lines;
this.ruleset = ruleset;
this.allow = allow || {};
this.ignore = ignore || [];
}
2021-01-05 13:46:21 +00:00
error(message, {line = 1, col = 1}, rule = {}) {
this.messages.push({
2021-01-05 13:46:21 +00:00
type: 'error',
evidence: this.lines[line - 1],
line, col,
message,
rule,
});
}
2021-01-05 13:46:21 +00:00
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({
2021-01-05 13:46:21 +00:00
type: this.ruleset[rule.id] === 2 ? 'error' : 'warning',
evidence: this.lines[line - 1],
line, col,
message,
rule,
});
}
2021-01-05 13:46:21 +00:00
info(message, {line = 1, col = 1}, rule) {
this.messages.push({
2021-01-05 13:46:21 +00:00
type: 'info',
evidence: this.lines[line - 1],
line, col,
message,
rule,
});
}
rollupError(message, rule) {
this.messages.push({
2021-01-05 13:46:21 +00:00
type: 'error',
rollup: true,
message,
rule,
});
}
rollupWarn(message, rule) {
this.messages.push({
2021-01-05 13:46:21 +00:00
type: 'warning',
rollup: true,
message,
rule,
});
}
stat(name, value) {
this.stats[name] = value;
}
}
2021-01-05 13:46:21 +00:00
//#endregion
//#region CSSLint
//eslint-disable-next-line no-var
var CSSLint = (() => {
const RX_EMBEDDED = /\/\*\s*csslint\s+((?:[^*]|\*(?!\/))+?)\*\//ig;
const EBMEDDED_RULE_VALUE_MAP = {
// error
2021-01-05 13:46:21 +00:00
'true': 2,
'2': 2,
// warning
2021-01-05 13:46:21 +00:00
'': 1,
'1': 1,
// ignore
'false': 0,
2021-01-05 13:46:21 +00:00
'0': 0,
};
2021-01-05 13:46:21 +00:00
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(), {
2021-01-05 13:46:21 +00:00
/**
* 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;
},
}),
2021-01-05 13:46:21 +00:00
rules,
2021-01-05 13:46:21 +00:00
getRuleList() {
return Object.values(rules)
.sort((a, b) => a.id < b.id ? -1 : a.id > b.id);
},
2021-01-05 13:46:21 +00:00
getRuleSet() {
const ruleset = {};
// by default, everything is a warning
2021-01-05 13:46:21 +00:00
for (const id in rules) ruleset[id] = 1;
return ruleset;
},
/**
* 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.
*/
verify(text, ruleset) {
2021-01-05 13:46:21 +00:00
if (!ruleset) ruleset = this.getRuleSet();
const allow = {};
const ignore = [];
RX_EMBEDDED.lastIndex =
text.lastIndexOf('/*',
text.indexOf('csslint',
text.indexOf('/*') + 1 || text.length) + 1);
if (RX_EMBEDDED.lastIndex >= 0) {
ruleset = Object.assign({}, ruleset);
applyEmbeddedOverrides(text, ruleset, allow, ignore);
}
const parser = new parserlib.css.Parser({
2021-01-05 13:46:21 +00:00
starHack: true,
ieFilters: true,
underscoreHack: true,
2021-01-05 13:46:21 +00:00
strict: false,
});
const reporter = new Reporter([], ruleset, allow, ignore);
// always report parsing errors as errors
ruleset.errors = 2;
2021-01-05 13:46:21 +00:00
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];
const reuseCache = !prevOverrides || JSON.stringify(prevOverrides) === JSON.stringify(newOvr);
prevOverrides = newOvr;
try {
parser.parse(text, {reuseCache});
} catch (ex) {
2021-01-05 13:46:21 +00:00
reporter.error('Fatal error, cannot continue!\n' + ex.stack, ex, {});
}
const report = {
messages: reporter.messages,
2021-01-05 13:46:21 +00:00
stats: reporter.stats,
ruleset: reporter.ruleset,
allow: reporter.allow,
ignore: reporter.ignore,
};
// sort by line numbers, rollups at the bottom
report.messages.sort((a, b) =>
a.rollup && !b.rollup ? 1 :
!a.rollup && b.rollup ? -1 :
a.line - b.line);
parserlib.cache.feedback(report);
return report;
},
});
// Example 1:
/* csslint ignore:start */
/*
the chunk of code where errors won't be reported
the chunk's start is hardwired to the line of the opening comment
the chunk's end is hardwired to the line of the closing comment
*/
/* csslint ignore:end */
// Example 2:
// allow rule violations on the current line:
// foo: bar; /* csslint allow:rulename1,rulename2,... */
/* csslint allow:rulename1,rulename2,... */ // foo: bar;
// Example 3:
/* csslint rulename1 */
/* csslint rulename2:N */
/* csslint rulename3:N, rulename4:N */
/* entire code is affected;
* comments futher down the code extend/override previous comments of this kind
* values for N (without the backquotes):
`2` or `true` means "error"
`1` or omitted means "warning" (when omitting, the colon can be omitted too)
`0` or `false` means "ignore"
*/
function applyEmbeddedOverrides(text, ruleset, allow, ignore) {
let ignoreStart = null;
let ignoreEnd = null;
let lineno = 0;
let eol = -1;
let m;
while ((m = RX_EMBEDDED.exec(text))) {
// account for the lines between the previous and current match
while (eol <= m.index) {
eol = text.indexOf('\n', eol + 1);
if (eol < 0) eol = text.length;
lineno++;
}
const ovr = m[1].toLowerCase();
const cmd = ovr.split(':', 1)[0];
const i = cmd.length + 1;
switch (cmd.trim()) {
case 'allow': {
const allowRuleset = {};
let num = 0;
ovr.slice(i).split(',').forEach(allowRule => {
allowRuleset[allowRule.trim()] = true;
num++;
});
if (num) allow[lineno] = allowRuleset;
break;
}
case 'ignore':
if (ovr.includes('start')) {
ignoreStart = ignoreStart || lineno;
break;
}
if (ovr.includes('end')) {
ignoreEnd = lineno;
if (ignoreStart && ignoreEnd) {
ignore.push([ignoreStart, ignoreEnd]);
ignoreStart = ignoreEnd = null;
}
}
break;
default:
ovr.slice(i).split(',').forEach(rule => {
const pair = rule.split(':');
const property = pair[0] || '';
const value = pair[1] || '';
const mapped = EBMEDDED_RULE_VALUE_MAP[value.trim()];
ruleset[property.trim()] = mapped === undefined ? 1 : mapped;
});
}
}
// Close remaining ignore block, if any
if (ignoreStart) {
ignore.push([ignoreStart, lineno]);
}
}
})();
2021-01-05 13:46:21 +00:00
//#endregion
//#region Util
CSSLint.Util = {
2021-01-05 13:46:21 +00:00
/** 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 [
'fontface',
'keyframerule',
'page',
'pagemargin',
'rule',
'viewport',
]) {
if (start) parser.addListener('start' + e, start);
if (end) parser.addListener('end' + e, end);
}
if (property) parser.addListener('property', property);
},
2021-01-05 13:46:21 +00:00
registerShorthandEvents(parser, {property, end}) {
const {shorthands, shorthandsFor} = CSSLint.Util;
let props, inRule;
2021-01-05 13:46:21 +00:00
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() {
const WSC = 'width|style|color';
const TBLR = 'top|bottom|left|right';
const shorthands = Object.create(null);
const shorthandsFor = Object.create(null);
for (const [sh, pattern, ...args] of [
['animation', '%-1',
'name|duration|timing-function|delay|iteration-count|direction|fill-mode|play-state'],
['background', '%-1', 'image|size|position|repeat|origin|clip|attachment|color'],
['border', '%-1-2', TBLR, WSC],
['border-top', '%-1', WSC],
['border-left', '%-1', WSC],
['border-right', '%-1', WSC],
['border-bottom', '%-1', WSC],
['border-block-end', '%-1', WSC],
['border-block-start', '%-1', WSC],
['border-image', '%-1', 'source|slice|width|outset|repeat'],
['border-inline-end', '%-1', WSC],
['border-inline-start', '%-1', WSC],
['border-radius', 'border-1-2-radius', 'top|bottom', 'left|right'],
['border-color', 'border-1-color', TBLR],
['border-style', 'border-1-style', TBLR],
['border-width', 'border-1-width', TBLR],
['column-rule', '%-1', WSC],
['columns', 'column-1', 'width|count'],
['flex', '%-1', 'grow|shrink|basis'],
['flex-flow', 'flex-1', 'direction|wrap'],
['font', '%-style|%-variant|%-weight|%-stretch|%-size|%-family|line-height'],
['grid', '%-1',
'template-rows|template-columns|template-areas|' +
'auto-rows|auto-columns|auto-flow|column-gap|row-gap'],
['grid-area', 'grid-1-2', 'row|column', 'start|end'],
['grid-column', '%-1', 'start|end'],
['grid-gap', 'grid-1-gap', 'row|column'],
['grid-row', '%-1', 'start|end'],
['grid-template', '%-1', 'columns|rows|areas'],
['list-style', 'list-1', 'type|position|image'],
['margin', '%-1', TBLR],
['mask', '%-1', 'image|mode|position|size|repeat|origin|clip|composite'],
['outline', '%-1', WSC],
['padding', '%-1', TBLR],
['text-decoration', '%-1', 'color|style|line'],
['text-emphasis', '%-1', 'style|color'],
['transition', '%-1', 'delay|duration|property|timing-function'],
]) {
let res = pattern.replace(/%/g, sh);
args.forEach((arg, i) => {
res = arg.replace(/[^|]+/g, res.replace(new RegExp(`${i + 1}`, 'g'), '$$&'));
});
(shorthands[sh] = res.split('|')).forEach(r => {
shorthandsFor[r] = sh;
});
}
Object.defineProperties(CSSLint.Util, {
shorthands: {value: shorthands},
shorthandsFor: {value: shorthandsFor},
});
return shorthands;
},
get shorthandsFor() {
return CSSLint.Util.shorthandsFor ||
CSSLint.Util.shorthands && CSSLint.Util.shorthandsFor;
},
};
2021-01-05 13:46:21 +00:00
//#endregion
//#region Rules
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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) {
2021-01-05 13:46:21 +00:00
reporter.report('Adjoining classes: ' + selector.text, part, rule);
}
}
}
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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;
2021-01-05 13:46:21 +00:00
},
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] = {
2021-01-05 13:46:21 +00:00
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;
}
2021-01-05 13:46:21 +00:00
},
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];
if (parts.length !== 2 || Number(parts[0].value) !== 0) {
2021-01-05 13:46:21 +00:00
reporter.report(
`Using ${size} with ${prop} can sometimes make elements larger than you expect.`,
{line, col}, rule);
}
}
}
2021-01-05 13:46:21 +00:00
},
});
}];
2021-01-05 13:46:21 +00:00
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'],
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
parser.addListener('property', event => {
if (CSSLint.Util.getPropName(event.property) === 'box-sizing') {
reporter.report(rule.desc, event, rule);
}
2021-01-05 13:46:21 +00:00
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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;
}
2021-01-05 13:46:21 +00:00
}
// 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);
});
}];
2021-01-05 13:46:21 +00:00
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;
for (const prop in compatiblePrefixes) {
const variations = compatiblePrefixes[prop].split(' ').map(s => `-${s}-${prop}`);
compatiblePrefixes[prop] = variations;
applyTo.push(...variations);
}
2021-01-05 13:46:21 +00:00
parser.addListener('startrule', () => {
started++;
properties = [];
});
2021-01-05 13:46:21 +00:00
parser.addListener('startkeyframes', event => {
started++;
inKeyFrame = event.prefix || true;
if (inKeyFrame && typeof inKeyFrame === 'string') {
inKeyFrame = '-' + inKeyFrame + '-';
}
});
2021-01-05 13:46:21 +00:00
parser.addListener('endkeyframes', () => {
started--;
inKeyFrame = false;
});
2021-01-05 13:46:21 +00:00
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);
});
2021-01-05 13:46:21 +00:00
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);
}
}
2021-01-05 13:46:21 +00:00
}
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(
2021-01-05 13:46:21 +00:00
`'${item}' is compatible with ${spec} and should be included as well.`,
value.actualNodes[0], rule);
}
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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 = {};
2021-01-05 13:46:21 +00:00
},
property(event) {
if (!inRule) return;
const name = CSSLint.Util.getPropName(event.property);
if (name in propertiesToCheck) {
properties[name] = {
value: event.value.text,
2021-01-05 13:46:21 +00:00
line: event.property.line,
col: event.property.col,
};
}
2021-01-05 13:46:21 +00:00
},
end() {
inRule = false;
const display = properties.display && properties.display.value;
if (!display) return;
switch (display.toLowerCase()) {
case 'inline':
['height', 'width', 'margin', 'margin-top', 'margin-bottom']
.forEach(p => reportProperty(p, display));
reportProperty('float', display,
2021-01-05 13:46:21 +00:00
"'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
2021-01-05 13:46:21 +00:00
if (/^table-/i.test(display)) {
['margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'float']
.forEach(p => reportProperty(p, display));
}
}
2021-01-05 13:46:21 +00:00
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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}.`,
2021-01-05 13:46:21 +00:00
event, rule);
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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();
2021-01-05 13:46:21 +00:00
const last = props[name];
const dupValue = last === event.value.text;
if (last && (lastName !== name || dupValue)) {
reporter.report(`${dupValue ? 'Duplicate' : 'Ungrouped duplicate'} '${property}'.`,
event, rule);
}
2021-01-05 13:46:21 +00:00
props[name] = event.value.text;
lastName = name;
2021-01-05 13:46:21 +00:00
},
end() {
inRule = false;
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
});
}];
2021-01-05 13:46:21 +00:00
CSSLint.addRule['errors'] = [{
name: 'Parsing Errors',
desc: 'This rule looks for recoverable syntax errors.',
browsers: 'All',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
parser.addListener('error', e => reporter.error(e.message, e, rule));
}];
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',
2021-01-05 13:46:21 +00:00
}, (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) {
2021-01-05 13:46:21 +00:00
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 ||
2021-01-05 13:46:21 +00:00
lastProperty.colorType !== 'compat' ||
CSSLint.Util.getPropName(lastProperty.property) !== name) {
reporter.report(`Fallback ${name} (hex or RGB) should precede ${colorType} ${name}.`,
2021-01-05 13:46:21 +00:00
event, rule);
}
}
lastProperty = event;
2021-01-05 13:46:21 +00:00
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
}
});
}];
CSSLint.addRule['globals-in-document'] = [{
name: 'Warn about global @ rules inside @-moz-document',
desc: 'Warn about @import, @charset, @namespace inside @-moz-document',
browsers: 'All',
}, (rule, parser, reporter) => {
let level = 0;
let index = 0;
parser.addListener('startdocument', () => level++);
parser.addListener('enddocument', () => level-- * index++);
const check = event => {
if (level && index) {
reporter.report(`A nested @${event.type} is valid only if this @-moz-document section ` +
'is the first one matched for any given URL.', event, rule);
}
};
parser.addListener('import', check);
parser.addListener('charset', check);
parser.addListener('namespace', check);
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
let gradients;
CSSLint.Util.registerRuleEvents(parser, {
start() {
gradients = {
2021-01-05 13:46:21 +00:00
moz: 0,
webkit: 0,
oldWebkit: 0,
2021-01-05 13:46:21 +00:00
o: 0,
};
2021-01-05 13:46:21 +00:00
},
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;
}
2021-01-05 13:46:21 +00:00
},
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) {
reporter.report(`Missing vendor-prefixed CSS gradients for ${missing.join(', ')}.`,
2021-01-05 13:46:21 +00:00
event.selectors[0], rule);
}
2021-01-05 13:46:21 +00:00
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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'), 0)
: sum, 0);
2021-01-05 13:46:21 +00:00
if (cnt) {
reporter.report(`Id in selector${cnt > 1 ? '!'.repeat(cnt) : '.'}`, sel, rule);
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
}
});
}];
2021-01-05 13:46:21 +00:00
CSSLint.addRule['import'] = [{
name: 'Disallow @import',
desc: "Don't use @import, use <link> instead.",
url: 'https://github.com/CSSLint/csslint/wiki/Disallow-%40import',
browsers: 'All',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
parser.addListener('import', e => {
reporter.report('@import prevents parallel downloads, use <link> instead.', e, rule);
});
}];
2021-01-05 13:46:21 +00:00
CSSLint.addRule['important'] = [{
name: 'Disallow !important',
desc: 'Be careful when using !important declaration',
url: 'https://github.com/CSSLint/csslint/wiki/Disallow-%21important',
browsers: 'All',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
let count = 0;
parser.addListener('property', event => {
if (event.important) {
count++;
2021-01-05 13:46:21 +00:00
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);
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
parser.addListener('property', event => {
const inv = event.invalid;
if (inv) reporter.report(inv.message, inv, rule);
});
}];
CSSLint.addRule['known-pseudos'] = [{
name: 'Require use of known pseudo selectors',
url: 'https://developer.mozilla.org/docs/Learn/CSS/Building_blocks/Selectors/Pseudo-classes_and_pseudo-elements',
browsers: 'All',
}, (rule, parser, reporter) => {
// 1 = requires ":"
// 2 = requires "::"
const Func = 4; // must be :function()
const FuncToo = 8; // both :function() and :non-function
const WK = 0x10;
const Moz = 0x20;
const definitions = {
// elements
'after': 1 + 2, // also allows ":"
'backdrop': 2,
'before': 1 + 2, // also allows ":"
'cue': 2,
'cue-region': 2,
'file-selector-button': 2,
'first-letter': 1 + 2, // also allows ":"
'first-line': 1 + 2, // also allows ":"
'grammar-error': 2,
'highlight': 2 + Func,
'marker': 2,
'part': 2 + Func,
'placeholder': 2 + Moz,
'selection': 2 + Moz,
'slotted': 2 + Func,
'spelling-error': 2,
'target-text': 2,
// classes
'active': 1,
'any-link': 1 + Moz + WK,
'autofill': 1 + WK,
'blank': 1,
'checked': 1,
'current': 1 + FuncToo,
'default': 1,
'defined': 1,
'dir': 1 + Func,
'disabled': 1,
'drop': 1,
'empty': 1,
'enabled': 1,
'first': 1,
'first-child': 1,
'first-of-type': 1,
'focus': 1,
'focus-visible': 1,
'focus-within': 1,
'fullscreen': 1,
'future': 1,
'has': 1 + Func,
'host': 1 + FuncToo,
'host-context': 1 + Func,
'hover': 1,
'in-range': 1,
'indeterminate': 1,
'invalid': 1,
'is': 1 + Func,
'lang': 1 + Func,
'last-child': 1,
'last-of-type': 1,
'left': 1,
'link': 1,
'local-link': 1,
'not': 1 + Func,
'nth-child': 1 + Func,
'nth-col': 1 + Func,
'nth-last-child': 1 + Func,
'nth-last-col': 1 + Func,
'nth-last-of-type': 1 + Func,
'nth-of-type': 1 + Func,
'only-child': 1,
'only-of-type': 1,
'optional': 1,
'out-of-range': 1,
'past': 1,
'paused': 1,
'picture-in-picture': 1,
'placeholder-shown': 1,
'playing': 1,
'read-only': 1,
'read-write': 1,
'required': 1,
'right': 1,
'root': 1,
'scope': 1,
'state': 1 + Func,
'target': 1,
'target-within': 1,
'user-invalid': 1,
'valid': 1,
'visited': 1,
'where': 1 + Func,
'xr-overlay': 1,
// ::-webkit-scrollbar specific classes
'corner-present': 1,
'decrement': 1,
'double-button': 1,
'end': 1,
'horizontal': 1,
'increment': 1,
'no-button': 1,
'single-button': 1,
'start': 1,
'vertical': 1,
'window-inactive': 1 + Moz,
};
const definitionsPrefixed = {
'any': 1 + Func + Moz + WK,
'calendar-picker-indicator': 2 + WK,
'clear-button': 2 + WK,
'color-swatch': 2 + WK,
'color-swatch-wrapper': 2 + WK,
'date-and-time-value': 2 + WK,
'datetime-edit': 2 + WK,
'datetime-edit-ampm-field': 2 + WK,
'datetime-edit-day-field': 2 + WK,
'datetime-edit-fields-wrapper': 2 + WK,
'datetime-edit-hour-field': 2 + WK,
'datetime-edit-millisecond-field': 2 + WK,
'datetime-edit-minute-field': 2 + WK,
'datetime-edit-month-field': 2 + WK,
'datetime-edit-second-field': 2 + WK,
'datetime-edit-text': 2 + WK,
'datetime-edit-week-field': 2 + WK,
'datetime-edit-year-field': 2 + WK,
'drag': 1 + WK,
'drag-over': 1 + Moz,
'file-upload-button': 2 + WK,
'focus-inner': 2 + Moz,
'focusring': 1 + Moz,
'full-page-media': 1 + WK,
'full-screen': 1 + Moz + WK,
'full-screen-ancestor': 1 + Moz + WK,
'inner-spin-button': 2 + WK,
'input-placeholder': 1 + 2 + WK + Moz,
'loading': 1 + Moz,
'media-controls': 2 + WK,
'media-controls-current-time-display': 2 + WK,
'media-controls-enclosure': 2 + WK,
'media-controls-fullscreen-button': 2 + WK,
'media-controls-mute-button': 2 + WK,
'media-controls-overlay-enclosure': 2 + WK,
'media-controls-overlay-play-button': 2 + WK,
'media-controls-panel': 2 + WK,
'media-controls-play-button': 2 + WK,
'media-controls-time-remaining-display': 2 + WK,
'media-controls-timeline': 2 + WK,
'media-controls-timeline-container': 2 + WK,
'media-controls-toggle-closed-captions-button': 2 + WK,
'media-controls-volume-slider': 2 + WK,
'media-slider-container': 2 + WK,
'media-slider-thumb': 2 + WK,
'media-text-track-container': 2 + WK,
'media-text-track-display': 2 + WK,
'media-text-track-region': 2 + WK,
'media-text-track-region-container': 2 + WK,
'meter-bar': 2 + WK,
'meter-even-less-good-value': 2 + WK,
'meter-inner-element': 2 + WK,
'meter-optimum-value': 2 + WK,
'meter-suboptimum-value': 2 + WK,
'progress-bar': 2 + WK,
'progress-inner-element': 2 + WK,
'progress-value': 2 + WK,
'resizer': 2 + WK,
'scrollbar': 2 + WK,
'scrollbar-button': 2 + WK,
'scrollbar-corner': 2 + WK,
'scrollbar-thumb': 2 + WK,
'scrollbar-track': 2 + WK,
'scrollbar-track-piece': 2 + WK,
'search-cancel-button': 2 + WK,
'slider-container': 2 + WK,
'slider-runnable-track': 2 + WK,
'slider-thumb': 2 + WK,
'textfield-decoration-container': 2 + WK,
};
const rx = /^(:+)(?:-(\w+)-)?([^(]+)(\()?/i;
const allowsFunc = Func + FuncToo;
const allowsPrefix = WK + Moz;
const {lower} = parserlib.util;
const checkSelector = ({parts}) => {
for (const {modifiers} of parts || []) {
if (!modifiers) continue;
for (const mod of modifiers) {
if (mod.type === 'pseudo') {
const {text} = mod;
const [all, colons, prefix, name, paren] = rx.exec(lower(text)) || 0;
const defPrefixed = definitionsPrefixed[name];
const def = definitions[name] || defPrefixed;
for (const err of !def ? ['Unknown pseudo'] : [
colons.length > 1
? !(def & 2) && 'Must use : in'
: !(def & 1) && all !== ':-moz-placeholder' && 'Must use :: in',
paren
? !(def & allowsFunc) && 'Unexpected ( in'
: (def & Func) && 'Must use ( after',
prefix ?
(
!(def & allowsPrefix) ||
prefix === 'webkit' && !(def & WK) ||
prefix === 'moz' && !(def & Moz)
) && 'Unexpected prefix in'
: defPrefixed && `Must use ${
(def & WK) && (def & Moz) && '-webkit- or -moz-' ||
(def & WK) && '-webkit-' || '-moz-'} prefix in`,
]) {
if (err) reporter.report(`${err} ${text.slice(0, all.length)}`, mod, rule);
}
} else if (mod.args) {
mod.args.forEach(checkSelector);
}
}
}
};
parser.addListener('startrule', e => e.selectors.forEach(checkSelector));
parser.addListener('supportsSelector', e => checkSelector(e.selector));
}];
2021-01-05 13:46:21 +00:00
CSSLint.addRule['order-alphabetical'] = [{
name: 'Alphabetical order',
desc: 'Assure properties are in alphabetical order',
browsers: 'All',
2021-01-05 13:46:21 +00:00
}, (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;
}
2021-01-05 13:46:21 +00:00
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
tags: ['Accessibility'],
}, (rule, parser, reporter) => {
let lastRule;
CSSLint.Util.registerRuleEvents(parser, {
start(event) {
lastRule = !event.selectors ? null : {
2021-01-05 13:46:21 +00:00
line: event.line,
col: event.col,
selectors: event.selectors,
propCount: 0,
2021-01-05 13:46:21 +00:00
outline: false,
};
2021-01-05 13:46:21 +00:00
},
property(event) {
if (!lastRule) return;
lastRule.propCount++;
2021-01-05 13:46:21 +00:00
if (CSSLint.Util.getPropName(event.property) === 'outline' && /^(none|0)$/i.test(event.value)) {
lastRule.outline = true;
}
2021-01-05 13:46:21 +00:00
},
end() {
const {outline, selectors, propCount} = lastRule || {};
lastRule = null;
if (!outline) return;
2021-01-05 13:46:21 +00:00
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.",
2021-01-05 13:46:21 +00:00
lastRule, rule);
}
2021-01-05 13:46:21 +00:00
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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});
}
}
}
2021-01-05 13:46:21 +00:00
}
});
// 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);
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
}
2021-01-05 13:46:21 +00:00
first = false;
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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) {
2021-01-05 13:46:21 +00:00
if (mod.type === 'attribute' && /([~|^$*]=)/.test(mod)) {
reporter.report(`Slow attribute selector ${RegExp.$1}.`, mod, rule);
}
}
}
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
CSSLint.addRule['rules-count'] = [{
name: 'Rules Count',
desc: 'Track how many rules there are.',
browsers: 'All',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
let count = 0;
parser.addListener('startrule', () => count++);
parser.addListener('endstylesheet', () => reporter.stat('rule-count', count));
}];
CSSLint.addRule['selector-max'] = [{
name: 'Error when past the 4095 selector limit for IE',
desc: 'Will error when selector count is > 4095.',
browsers: 'IE',
2021-01-05 13:46:21 +00:00
}, (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);
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
CSSLint.rules['selector-max'].init(rule, parser, reporter, Number(rule.desc.match(/\d+/)[0]));
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
}
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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));
}
2021-01-05 13:46:21 +00:00
}
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
}
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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])) {
2021-01-05 13:46:21 +00:00
reporter.report('Complex selector inside :not().', args[0], rule);
}
}
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
parser.addListener('property', ({property}) => {
if (property.hack === '*') {
reporter.report('IE star prefix.', property, rule);
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
let textIndent, isLtr;
CSSLint.Util.registerRuleEvents(parser, {
start() {
textIndent = false;
2021-01-05 13:46:21 +00:00
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;
2021-01-05 13:46:21 +00:00
} else if (name === 'direction' && /^ltr$/i.test(value)) {
isLtr = true;
}
2021-01-05 13:46:21 +00:00
},
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);
}
2021-01-05 13:46:21 +00:00
},
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
parser.addListener('property', ({property}) => {
if (property.hack === '_') {
reporter.report('IE underscore prefix.', property, rule);
}
});
}];
2021-01-05 13:46:21 +00:00
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',
}, (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);
}
2021-01-05 13:46:21 +00:00
}
});
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);
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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);
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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) {
2021-01-05 13:46:21 +00:00
reporter.report(rule.desc, part, rule);
}
}
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
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',
2021-01-05 13:46:21 +00:00
}, (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;
2021-01-05 13:46:21 +00:00
},
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({
actual: prop,
needed: propertiesToCheck[prop],
});
}
}
for (const {needed, actual} of needsStandard) {
2021-01-05 13:46:21 +00:00
const unit = properties[actual][0].name;
if (!properties[needed]) {
reporter.report(`Missing standard property '${needed}' to go along with '${actual}'.`,
2021-01-05 13:46:21 +00:00
unit, rule);
} else if (properties[needed][0].pos < properties[actual][0].pos) {
2021-01-05 13:46:21 +00:00
reporter.report(
`Standard property '${needed}' should come after vendor-prefixed property '${actual}'.`,
unit, rule);
}
}
2021-01-05 13:46:21 +00:00
},
});
}];
2021-01-05 13:46:21 +00:00
CSSLint.addRule['warnings'] = [{
name: 'Parsing warnings',
desc: 'This rule looks for parser warnings.',
browsers: 'All',
2021-01-05 13:46:21 +00:00
}, (rule, parser, reporter) => {
parser.addListener('warning', e => reporter.report(e.message, e, rule));
}];
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);
}
2021-01-05 13:46:21 +00:00
}
});
}];
2021-01-05 13:46:21 +00:00
//#endregion