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:
tophf 2019-03-04 01:55:15 +03:00 committed by Rob Garrison
parent 1ff34fc449
commit dd8c8d0ffb
3 changed files with 147 additions and 53 deletions

View File

@ -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,
});

View File

@ -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]);
}
}

View File

@ -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],
]),
};