CSSLint and parserlib (#646)
* CSSLint: add mask-image https://drafts.fxtf.org/css-masking-1/#the-mask-image * CSSLint: update <image> type https://drafts.csswg.org/css-images-3/#typedef-image * CodeMirror CSS mode: add 'mask-image' * CodeMirror CSS mode: add CSS Round Display L1 https://www.w3.org/TR/css-round-display-1/ * CSSLint: CSS Round Display L1 (ED 2018-09-26) https://drafts.csswg.org/css-round-display/ * CSSLint: CSS Environment Variables L1 (ED 2018-08-03) https://drafts.csswg.org/css-env-1/ * CSSLint: parts of CSS Overflow Module L3 (WD 2018-07-31) only overflow-* properties are added since the rest seem tentative https://www.w3.org/TR/css-overflow-3/ * CSSLint: Selectors L4 :is() supersedes :matches() https://drafts.csswg.org/selectors-4/#matches * CSSLint: Text Decoration L3 (CR 2018-06-26) https://drafts.csswg.org/css-text-decor-3/ * CSSLint: fix '&&' in grammarParser consequences: * fixed text-shadow * fixed <display-listitem> * switched to a string in <shadow> * CSSLint: fix definition for 'rotate' * CSSLint: fix applyEmbeddedOverrides * CSSLint: update definition for 'rotate' * CSSLint: reset parserlib cache when inline overrides change * CSSLint: code cosmetics * CSSLint: fixup d5971e9c * CSSLint: code cosmetics * CSSLint: start ignoring from the comment's line number
This commit is contained in:
parent
cdc7f98150
commit
55189f1fdd
|
@ -157,10 +157,18 @@
|
|||
'text-align-all': true,
|
||||
|
||||
'contain': true,
|
||||
'mask-image': true,
|
||||
'mix-blend-mode': true,
|
||||
'rotate': true,
|
||||
'isolation': true,
|
||||
'zoom': true,
|
||||
|
||||
// https://www.w3.org/TR/css-round-display-1/
|
||||
'border-boundary': true,
|
||||
'shape': true,
|
||||
'shape-inside': true,
|
||||
'viewport-fit': true,
|
||||
|
||||
// nonstandard https://compat.spec.whatwg.org/
|
||||
'box-reflect': true,
|
||||
'text-fill-color': true,
|
||||
|
@ -171,6 +179,7 @@
|
|||
});
|
||||
Object.assign(CodeMirror.mimeModes['text/css'].valueKeywords, {
|
||||
'isolate': true,
|
||||
'rect': true,
|
||||
'recto': true,
|
||||
'verso': true,
|
||||
});
|
||||
|
|
|
@ -34,11 +34,11 @@ class Reporter {
|
|||
* 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
|
||||
* @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} explicitly allowed lines
|
||||
* @param {[][]} ingore list of line ranges to be ignored
|
||||
* @param {Object} allow - explicitly allowed lines
|
||||
* @param {[][]} ingore - list of line ranges to be ignored
|
||||
*/
|
||||
constructor(lines, ruleset, allow, ignore) {
|
||||
this.messages = [];
|
||||
|
@ -126,6 +126,9 @@ var CSSLint = (() => {
|
|||
};
|
||||
const rules = [];
|
||||
|
||||
// previous CSSLint overrides are used to decide whether the parserlib's cache should be reset
|
||||
let prevOverrides;
|
||||
|
||||
return Object.assign(new parserlib.util.EventTarget(), {
|
||||
|
||||
addRule(rule) {
|
||||
|
@ -193,8 +196,13 @@ var CSSLint = (() => {
|
|||
rules[id] &&
|
||||
rules[id].init(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: true});
|
||||
parser.parse(text, {reuseCache});
|
||||
} catch (ex) {
|
||||
reporter.error('Fatal error, cannot continue: ' + ex.message, ex.line, ex.col, {});
|
||||
}
|
||||
|
@ -219,17 +227,50 @@ var CSSLint = (() => {
|
|||
},
|
||||
});
|
||||
|
||||
// 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:
|
||||
|
||||
/* csslint allow:rulename1,rulename2,... */
|
||||
// allows to break the specified rules on the next single line of code
|
||||
|
||||
// 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:
|
||||
// "2" or "true" means "error"
|
||||
// "1" or nothing means "warning" - note in this case ":" can also be omitted
|
||||
// "0" or "false" means "ignore"
|
||||
// (the quotes are added here for convenience, don't put them in the actual comments)
|
||||
|
||||
function applyEmbeddedOverrides(text, ruleset, allow, ignore) {
|
||||
let ignoreStart = null;
|
||||
let ignoreEnd = null;
|
||||
let lineno = 0;
|
||||
let eol = -1;
|
||||
let m;
|
||||
|
||||
for (let eol = 0, m; (m = RX_EMBEDDED.exec(text)); lineno++) {
|
||||
eol = (text.indexOf('\n', eol) + 1 || text.length + 1) - 1;
|
||||
if (eol < m.index) continue;
|
||||
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);
|
||||
const cmd = ovr.split(':', 1)[0];
|
||||
const i = cmd.length + 1;
|
||||
|
||||
switch (cmd.trim()) {
|
||||
|
@ -246,15 +287,13 @@ var CSSLint = (() => {
|
|||
}
|
||||
|
||||
case 'ignore':
|
||||
if (ovr.lastIndexOf('start', i) > 0) {
|
||||
if (ignoreStart === null) {
|
||||
ignoreStart = lineno;
|
||||
}
|
||||
if (ovr.includes('start')) {
|
||||
ignoreStart = ignoreStart || lineno;
|
||||
break;
|
||||
}
|
||||
if (ovr.lastIndexOf('end', i) > 0) {
|
||||
if (ovr.includes('end')) {
|
||||
ignoreEnd = lineno;
|
||||
if (ignoreStart !== null && ignoreEnd !== null) {
|
||||
if (ignoreStart && ignoreEnd) {
|
||||
ignore.push([ignoreStart, ignoreEnd]);
|
||||
ignoreStart = ignoreEnd = null;
|
||||
}
|
||||
|
@ -273,7 +312,7 @@ var CSSLint = (() => {
|
|||
}
|
||||
|
||||
// Close remaining ignore block, if any
|
||||
if (ignoreStart !== null) {
|
||||
if (ignoreStart) {
|
||||
ignore.push([ignoreStart, lineno]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -194,6 +194,7 @@ self.parserlib = (() => {
|
|||
'border-bottom-right-radius': '<x-one-radius>',
|
||||
'border-bottom-style': '<border-style>',
|
||||
'border-bottom-width': '<border-width>',
|
||||
'border-boundary': 'none | parent | display',
|
||||
'border-inline-color': '<color>{1,2}',
|
||||
'border-inline-end': '<border-shorthand>',
|
||||
'border-inline-end-color': '<color>',
|
||||
|
@ -467,6 +468,7 @@ self.parserlib = (() => {
|
|||
'marquee-speed': 1,
|
||||
'marquee-style': 1,
|
||||
'mask': 1,
|
||||
'mask-image': '[ none | <image> | <uri> ]#',
|
||||
'max-height': 'none | <width-height>',
|
||||
'max-width': 'none | <width-height>',
|
||||
'min-height': 'auto | <width-height>',
|
||||
|
@ -496,7 +498,9 @@ self.parserlib = (() => {
|
|||
'outline-offset': '<length>',
|
||||
'outline-style': '<border-style> | auto',
|
||||
'outline-width': '<border-width>',
|
||||
'overflow': '<overflow>',
|
||||
'overflow': '<overflow>{1,2}',
|
||||
'overflow-block': '<overflow>',
|
||||
'overflow-inline': '<overflow>',
|
||||
'overflow-style': 1,
|
||||
'overflow-wrap': 'normal | break-word',
|
||||
'overflow-x': '<overflow>',
|
||||
|
@ -549,7 +553,7 @@ self.parserlib = (() => {
|
|||
'rest-before': 1,
|
||||
'richness': 1,
|
||||
'right': '<width>',
|
||||
'rotate': 'none | <number>{3}? <angle>',
|
||||
'rotate': 'none | [ x | y | z | <number>{3} ]? && <angle>',
|
||||
'rotation': 1,
|
||||
'rotation-point': 1,
|
||||
'row-gap': '<row-gap>',
|
||||
|
@ -560,6 +564,7 @@ self.parserlib = (() => {
|
|||
|
||||
// S
|
||||
'scale': 'none | <number>{1,3}',
|
||||
'shape-inside': 'auto | outside-shape | [ <basic-shape> || shape-box ] | <image> | display',
|
||||
'shape-rendering': 'auto | optimizeSpeed | crispEdges | geometricPrecision',
|
||||
'size': 1,
|
||||
'speak': 'normal | none | spell-out',
|
||||
|
@ -598,15 +603,18 @@ self.parserlib = (() => {
|
|||
'text-decoration-skip': 'none | [ objects || [ spaces | [ leading-spaces || trailing-spaces ] ] || ' +
|
||||
'edges || box-decoration ]',
|
||||
'text-decoration-style': '<text-decoration-style>',
|
||||
'text-emphasis': 1,
|
||||
'text-emphasis': '<text-emphasis-style> || <color>',
|
||||
'text-emphasis-style': '<text-emphasis-style>',
|
||||
'text-emphasis-position': '[ over | under ] && [ right | left ]?',
|
||||
'text-height': 1,
|
||||
'text-indent': '<length> | <percentage>',
|
||||
'text-justify': 'auto | none | inter-word | inter-ideograph | inter-cluster | distribute | kashida',
|
||||
'text-outline': 1,
|
||||
'text-overflow': 'clip | ellipsis',
|
||||
'text-rendering': 'auto | optimizeSpeed | optimizeLegibility | geometricPrecision',
|
||||
'text-shadow': 'none | [ [ <color> && <length>{2,3} ] | <length>{2,3} ]#',
|
||||
'text-shadow': 'none | [ <color>? && <length>{2,3} ]#',
|
||||
'text-transform': 'capitalize | uppercase | lowercase | none',
|
||||
'text-underline-position': 'auto | [ under || [ left | right ] ]',
|
||||
'text-wrap': 'normal | none | avoid',
|
||||
'top': '<width>',
|
||||
'touch-action': 'auto | none | pan-x | pan-y | pan-left | pan-right | pan-up | pan-down | manipulation',
|
||||
|
@ -689,7 +697,7 @@ self.parserlib = (() => {
|
|||
|
||||
'<basic-shape>': 'inset() | circle() | ellipse() | polygon()',
|
||||
|
||||
'<bg-image>': '<image> | <gradient> | none',
|
||||
'<bg-image>': '<image> | none',
|
||||
|
||||
'<blend-mode>': 'normal | multiply | screen | overlay | darken | lighten | color-dodge | ' +
|
||||
'color-burn | hard-light | soft-light | difference | exclusion | hue | ' +
|
||||
|
@ -773,7 +781,7 @@ self.parserlib = (() => {
|
|||
return this['<ident>'](part) && !this['<generic-family>'](part);
|
||||
},
|
||||
|
||||
'<image>': '<uri>',
|
||||
'<image>': '<uri> | <gradient> | cross-fade()',
|
||||
|
||||
'<inflexible-breadth>': '<length-percentage> | min-content | max-content | auto',
|
||||
|
||||
|
@ -832,7 +840,7 @@ self.parserlib = (() => {
|
|||
return this['<number>'](part) && part.value >= 0 && part.value <= 1;
|
||||
},
|
||||
|
||||
'<overflow>': 'visible | hidden | scroll | auto',
|
||||
'<overflow>': 'visible | hidden | clip | scroll | auto',
|
||||
|
||||
'<overflow-position>': 'unsafe | safe',
|
||||
|
||||
|
@ -882,12 +890,15 @@ self.parserlib = (() => {
|
|||
if (part.tokenType === Tokens.USO_VAR) return true;
|
||||
if (part.type !== 'function' || !part.expr) return false;
|
||||
const subparts = part.expr.parts;
|
||||
return subparts.length &&
|
||||
lower(part.name) === 'var' &&
|
||||
subparts[0].type === 'custom-property' && (
|
||||
subparts.length === 1 ||
|
||||
subparts[1].text === ','
|
||||
);
|
||||
if (!subparts.length) return false;
|
||||
const name = lower(part.name);
|
||||
return (
|
||||
name === 'var' && subparts[0].type === 'custom-property' ||
|
||||
name === 'env' && subparts[0].type === 'identifier'
|
||||
) && (
|
||||
subparts.length === 1 ||
|
||||
subparts[1].text === ','
|
||||
);
|
||||
},
|
||||
|
||||
'<width>': '<length> | <percentage> | auto',
|
||||
|
@ -1039,18 +1050,16 @@ self.parserlib = (() => {
|
|||
'<hsl-color>': '[ <number> | <angle> ] <percentage>{2} [ / <nonnegative-number-or-percentage> ]? | ' +
|
||||
'[ <number> | <angle> ] , <percentage>#{2} [ , <nonnegative-number-or-percentage> ]?',
|
||||
|
||||
// inset? && [ <length>{2,4} && <color>? ]
|
||||
'<shadow>': Matcher =>
|
||||
Matcher.many(
|
||||
[true],
|
||||
Matcher.cast('<length>').braces(2, 4),
|
||||
'inset',
|
||||
'<color>'),
|
||||
'<shadow>': 'inset? && [ <length>{2,4} && <color>? ]',
|
||||
|
||||
'<single-timing-function>': 'linear | <cubic-bezier-timing-function> | <step-timing-function> | frames()',
|
||||
|
||||
'<text-decoration-line>': 'none | [ underline || overline || line-through || blink ]',
|
||||
|
||||
'<text-emphasis-style>': 'none | ' +
|
||||
'[ [ filled | open ] || [ dot | circle | double-circle | triangle | sesame ] ] | ' +
|
||||
'<string>',
|
||||
|
||||
'<track-list>': '[ <line-names>? [ <track-size> | <track-repeat> ] ]+ <line-names>?',
|
||||
|
||||
'<track-repeat>': 'repeat( [ <positive-integer> ] , [ <line-names>? <track-size> ]+ <line-names>? )',
|
||||
|
@ -1408,6 +1417,7 @@ self.parserlib = (() => {
|
|||
{name: 'NOT'},
|
||||
{name: 'ANY', text: ['any', '-webkit-any', '-moz-any']},
|
||||
{name: 'MATCHES'},
|
||||
{name: 'IS'},
|
||||
|
||||
/*
|
||||
* Defined in CSS3 Paged Media
|
||||
|
@ -1881,7 +1891,8 @@ self.parserlib = (() => {
|
|||
const p = required === false ? Matcher.prec.OROR : Matcher.prec.ANDAND;
|
||||
const s = ms.map((m, i) => {
|
||||
if (required !== false && !required[i]) {
|
||||
return m.toString(Matcher.prec.MOD) + '?';
|
||||
const str = m.toString(Matcher.prec.MOD);
|
||||
return str.endsWith('?') ? str : str + '?';
|
||||
}
|
||||
return m.toString(p);
|
||||
}).join(required === false ? ' || ' : ' && ');
|
||||
|
@ -1919,10 +1930,22 @@ self.parserlib = (() => {
|
|||
function andand() {
|
||||
// andand = seq ( " && " seq)*
|
||||
const m = [seq()];
|
||||
let reqPrev = !isOptional(m[0]);
|
||||
const required = [reqPrev];
|
||||
while (reader.readMatch(' && ')) {
|
||||
m.push(seq());
|
||||
const item = seq();
|
||||
const req = !isOptional(item);
|
||||
// Matcher.many apparently can't handle optional items first
|
||||
if (req && !reqPrev) {
|
||||
m.unshift(item);
|
||||
required.unshift(req);
|
||||
} else {
|
||||
m.push(item);
|
||||
required.push(req);
|
||||
reqPrev = req;
|
||||
}
|
||||
}
|
||||
return m.length === 1 ? m[0] : Matcher.andand.apply(Matcher, m);
|
||||
return m.length === 1 ? m[0] : Matcher.many(required, ...m);
|
||||
}
|
||||
|
||||
function seq() {
|
||||
|
@ -1977,6 +2000,10 @@ self.parserlib = (() => {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isOptional(item) {
|
||||
return !Array.isArray(item.options) && item.toString().endsWith('?');
|
||||
}
|
||||
})();
|
||||
|
||||
//endregion
|
||||
|
@ -2663,7 +2690,7 @@ self.parserlib = (() => {
|
|||
known.add(value.text);
|
||||
|
||||
function throwEndExpected(token, force) {
|
||||
if (force || token.name !== 'var' || token.type !== 'function') {
|
||||
if (force || (token.name !== 'var' && token.name !== 'env') || token.type !== 'function') {
|
||||
throw new ValidationError(`Expected end of value but found '${token.text}'.`, token);
|
||||
}
|
||||
}
|
||||
|
@ -3012,11 +3039,13 @@ self.parserlib = (() => {
|
|||
/*
|
||||
* Potential tokens:
|
||||
* - ANY
|
||||
* - IS
|
||||
* - MATCHES
|
||||
* - NOT
|
||||
* - CHAR
|
||||
*/
|
||||
case ':':
|
||||
return this.notOrAnyOrMatchesToken(c, pos);
|
||||
return this.notOrIsToken(c, pos);
|
||||
|
||||
/*
|
||||
* Potential tokens:
|
||||
|
@ -3224,16 +3253,20 @@ self.parserlib = (() => {
|
|||
}
|
||||
|
||||
// NOT
|
||||
// IS
|
||||
// ANY
|
||||
// MATCHES
|
||||
// CHAR
|
||||
notOrAnyOrMatchesToken(first, pos) {
|
||||
notOrIsToken(first, pos) {
|
||||
// first is always ':'
|
||||
const reader = this._reader;
|
||||
const func = reader.readMatch(/(not|(-(moz|webkit)-)?any|matches)\(/iy);
|
||||
const func = reader.readMatch(/(not|is|(-(moz|webkit)-)?(any|matches))\(/iy);
|
||||
if (func) {
|
||||
const lcase = func[0].toLowerCase();
|
||||
const type =
|
||||
func.startsWith('n') || func.startsWith('N') ? Tokens.NOT :
|
||||
func.startsWith('m') || func.startsWith('M') ? Tokens.MATCHES : Tokens.ANY;
|
||||
lcase === 'n' ? Tokens.NOT :
|
||||
lcase === 'i' ? Tokens.IS :
|
||||
lcase === 'm' ? Tokens.MATCHES : Tokens.ANY;
|
||||
return this.createToken(type, first + func, pos);
|
||||
}
|
||||
return this.charToken(first, pos);
|
||||
|
@ -4239,14 +4272,25 @@ self.parserlib = (() => {
|
|||
|
||||
_viewport() {
|
||||
const stream = this._tokenStream;
|
||||
stream.mustMatch(Tokens.VIEWPORT_SYM);
|
||||
const start = stream.mustMatch(Tokens.VIEWPORT_SYM);
|
||||
|
||||
this.fire('startviewport');
|
||||
// only viewport-fit is allowed but we're reusing MediaQuery syntax unit,
|
||||
// and accept anything for the sake of simplicity since the spec isn't yet final:
|
||||
// https://drafts.csswg.org/css-round-display/#extending-viewport-rule
|
||||
const descriptors = this._mediaQueryList();
|
||||
|
||||
this.fire({
|
||||
type: 'startviewport',
|
||||
descriptors,
|
||||
}, start);
|
||||
|
||||
this._ws();
|
||||
this._readDeclarations();
|
||||
|
||||
this.fire('endviewport');
|
||||
this.fire({
|
||||
type: 'endviewport',
|
||||
descriptors,
|
||||
});
|
||||
}
|
||||
|
||||
_document() {
|
||||
|
@ -4664,13 +4708,13 @@ self.parserlib = (() => {
|
|||
return value.length ? value : null;
|
||||
}
|
||||
|
||||
_anyOrMatches() {
|
||||
_is() {
|
||||
const stream = this._tokenStream;
|
||||
if (!stream.match([Tokens.ANY, Tokens.MATCHES])) return null;
|
||||
if (!stream.match([Tokens.IS, Tokens.ANY, Tokens.MATCHES])) return null;
|
||||
|
||||
let arg;
|
||||
const start = stream._token;
|
||||
const type = start.type === Tokens.ANY ? 'any' : 'matches';
|
||||
const type = lower(Tokens[start.type].name);
|
||||
const value =
|
||||
start.value +
|
||||
this._ws() +
|
||||
|
@ -5380,6 +5424,7 @@ self.parserlib = (() => {
|
|||
[Tokens.MEDIA_SYM, Parser.prototype._media],
|
||||
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
|
||||
[Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced],
|
||||
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
|
||||
]),
|
||||
|
||||
media: new Map([
|
||||
|
@ -5396,8 +5441,9 @@ self.parserlib = (() => {
|
|||
[Tokens.DOT, Parser.prototype._class],
|
||||
[Tokens.LBRACKET, Parser.prototype._attrib],
|
||||
[Tokens.COLON, Parser.prototype._pseudo],
|
||||
[Tokens.ANY, Parser.prototype._anyOrMatches],
|
||||
[Tokens.MATCHES, Parser.prototype._anyOrMatches],
|
||||
[Tokens.IS, Parser.prototype._is],
|
||||
[Tokens.ANY, Parser.prototype._is],
|
||||
[Tokens.MATCHES, Parser.prototype._is],
|
||||
[Tokens.NOT, Parser.prototype._negation],
|
||||
]),
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user