From dd8c8d0ffbe9a57e37bf872d4fb2c2e2561327a1 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 4 Mar 2019 01:55:15 +0300 Subject: [PATCH] CSSLint and parserlib (#646) * CSSLint: add mask-image https://drafts.fxtf.org/css-masking-1/#the-mask-image * CSSLint: update 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 * switched to a string in * 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 --- edit/codemirror-default.js | 9 ++ vendor-overwrites/csslint/csslint.js | 71 +++++++++++---- vendor-overwrites/csslint/parserlib.js | 120 +++++++++++++++++-------- 3 files changed, 147 insertions(+), 53 deletions(-) diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js index d0f6b8c2..edc7af28 100644 --- a/edit/codemirror-default.js +++ b/edit/codemirror-default.js @@ -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, }); diff --git a/vendor-overwrites/csslint/csslint.js b/vendor-overwrites/csslint/csslint.js index 95f74c98..b57e98e2 100644 --- a/vendor-overwrites/csslint/csslint.js +++ b/vendor-overwrites/csslint/csslint.js @@ -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]); } } diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index e53c1c7a..da4095ca 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -194,6 +194,7 @@ self.parserlib = (() => { 'border-bottom-right-radius': '', 'border-bottom-style': '', 'border-bottom-width': '', + 'border-boundary': 'none | parent | display', 'border-inline-color': '{1,2}', 'border-inline-end': '', 'border-inline-end-color': '', @@ -467,6 +468,7 @@ self.parserlib = (() => { 'marquee-speed': 1, 'marquee-style': 1, 'mask': 1, + 'mask-image': '[ none | | ]#', 'max-height': 'none | ', 'max-width': 'none | ', 'min-height': 'auto | ', @@ -496,7 +498,9 @@ self.parserlib = (() => { 'outline-offset': '', 'outline-style': ' | auto', 'outline-width': '', - 'overflow': '', + 'overflow': '{1,2}', + 'overflow-block': '', + 'overflow-inline': '', 'overflow-style': 1, 'overflow-wrap': 'normal | break-word', 'overflow-x': '', @@ -549,7 +553,7 @@ self.parserlib = (() => { 'rest-before': 1, 'richness': 1, 'right': '', - 'rotate': 'none | {3}? ', + 'rotate': 'none | [ x | y | z | {3} ]? && ', 'rotation': 1, 'rotation-point': 1, 'row-gap': '', @@ -560,6 +564,7 @@ self.parserlib = (() => { // S 'scale': 'none | {1,3}', + 'shape-inside': 'auto | outside-shape | [ || shape-box ] | | 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-emphasis': 1, + 'text-emphasis': ' || ', + 'text-emphasis-style': '', + 'text-emphasis-position': '[ over | under ] && [ right | left ]?', 'text-height': 1, 'text-indent': ' | ', '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 | [ [ && {2,3} ] | {2,3} ]#', + 'text-shadow': 'none | [ ? && {2,3} ]#', 'text-transform': 'capitalize | uppercase | lowercase | none', + 'text-underline-position': 'auto | [ under || [ left | right ] ]', 'text-wrap': 'normal | none | avoid', 'top': '', 'touch-action': 'auto | none | pan-x | pan-y | pan-left | pan-right | pan-up | pan-down | manipulation', @@ -689,7 +697,7 @@ self.parserlib = (() => { '': 'inset() | circle() | ellipse() | polygon()', - '': ' | | none', + '': ' | none', '': '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[''](part) && !this[''](part); }, - '': '', + '': ' | | cross-fade()', '': ' | min-content | max-content | auto', @@ -832,7 +840,7 @@ self.parserlib = (() => { return this[''](part) && part.value >= 0 && part.value <= 1; }, - '': 'visible | hidden | scroll | auto', + '': 'visible | hidden | clip | scroll | auto', '': '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 === ',' + ); }, '': ' | | auto', @@ -1039,18 +1050,16 @@ self.parserlib = (() => { '': '[ | ] {2} [ / ]? | ' + '[ | ] , #{2} [ , ]?', - // inset? && [ {2,4} && ? ] - '': Matcher => - Matcher.many( - [true], - Matcher.cast('').braces(2, 4), - 'inset', - ''), + '': 'inset? && [ {2,4} && ? ]', '': 'linear | | | frames()', '': 'none | [ underline || overline || line-through || blink ]', + '': 'none | ' + + '[ [ filled | open ] || [ dot | circle | double-circle | triangle | sesame ] ] | ' + + '', + '': '[ ? [ | ] ]+ ?', '': 'repeat( [ ] , [ ? ]+ ? )', @@ -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], ]), };