diff --git a/packages/less/lib/less/parser/parser-input.js b/packages/less/lib/less/parser/parser-input.js index 9a9dc39241..ff33904929 100644 --- a/packages/less/lib/less/parser/parser-input.js +++ b/packages/less/lib/less/parser/parser-input.js @@ -214,13 +214,16 @@ export default () => { * group array (or null). `[...]`/`{...}` do NOT shield a reference — only * `(...)` (a declaration-value group) does. */ - parserInput.$parseUntil = (tok, detectBareVar) => { + parserInput.$parseUntil = (tok, detectBareVar, stripLineComments) => { let quote = ''; let returnVal = null; let inComment = false; let blockDepth = 0; let parenDepth = 0; let bareVarIndex = null; + // Open `(` groups, true for those that hold a URL: `url(`, `url-prefix(`, `domain(`. + const urlParens = []; + let urlDepth = 0; const blockStack = []; const parseGroups = []; const length = input.length; @@ -278,6 +281,27 @@ export default () => { i++; inComment = true; blockDepth++; + } else if (stripLineComments && + input.charAt(i + 1) === '/' && + urlDepth === 0 && + input.charAt(i - 1) !== ':') { + // A `//` comment, which is not CSS and must not + // reach the output. Two `//` are not comments: one + // inside a URL function, where the value parser + // turns comment absorption off too, so + // `url(//host/x.png)` and `url( //host/x.png)` keep + // their address; and one after a `:`, which is a + // scheme, as in a bare `http://host`. + const before = input.slice(lastPos, i); + if (before) { + parseGroups.push(before); + } + let nextNewLine = input.indexOf('\n', i + 2); + if (nextNewLine < 0) { + nextNewLine = length; + } + lastPos = nextNewLine; + i = nextNewLine - 1; } break; case '\'': @@ -298,11 +322,16 @@ export default () => { blockStack.push('}'); blockDepth++; break; - case '(': + case '(': { blockStack.push(')'); blockDepth++; parenDepth++; + const isUrl = /(?:^|[^\w-])(?:url|url-prefix|domain)$/i.test( + input.slice(Math.max(0, i - 11), i)); + urlParens.push(isUrl); + if (isUrl) { urlDepth++; } break; + } case '[': blockStack.push(']'); blockDepth++; @@ -314,6 +343,7 @@ export default () => { if (nextChar === expected) { blockDepth--; if (nextChar === ')' && parenDepth > 0) { parenDepth--; } + if (nextChar === ')' && urlParens.pop()) { urlDepth--; } } else { // move the parser to the error and return expected skipWhitespace(i - startPos); diff --git a/packages/less/lib/less/parser/parser.js b/packages/less/lib/less/parser/parser.js index d21096cd22..fa7efa04e6 100644 --- a/packages/less/lib/less/parser/parser.js +++ b/packages/less/lib/less/parser/parser.js @@ -1874,7 +1874,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { * prelude (non-value position); accept `@{var}` interpolation and warn * on a bare `@var` reference (which resolves today but is deprecated). */ - permissiveValue: function (untilTokens, deprecateVariables) { + permissiveValue: function (untilTokens, deprecateVariables, stripLineComments) { const entities = this.entities; let i; let e; @@ -1942,7 +1942,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { } parserInput.save(); - value = parserInput.$parseUntil(tok, deprecateVariables); + value = parserInput.$parseUntil(tok, deprecateVariables, stripLineComments); if (value) { if (typeof value === 'string') { @@ -2350,7 +2350,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { return e; }, atruleUnknown: function (value, name, hasBlock) { - value = this.permissiveValue(/^[{;]/, true); + value = this.permissiveValue(/^[{;]/, true, true); hasBlock = (parserInput.currentChar() === '{'); if (!value) { if (!hasBlock && parserInput.currentChar() !== ';') { diff --git a/packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.css b/packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.css new file mode 100644 index 0000000000..215eb752df --- /dev/null +++ b/packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.css @@ -0,0 +1,41 @@ +@supports selector( + :focus-visible ) { + a { + color: red; + } +} +@supports (display: grid) { + .grid { + display: grid; + } +} +@supports (display: flex) { + .flex { + display: flex; + } +} +@supports (background: url(http://example.com/a.png)) { + .absolute-url { + color: blue; + } +} +@document url-prefix(http://example.com/) { + .doc { + color: green; + } +} +@supports (background: url(//cdn.example.com/x.png)) { + .protocol-relative { + background: url(//cdn.example.com/x.png); + } +} +@supports (background: url( //cdn.example.com/y.png)) { + .protocol-relative-spaced { + color: navy; + } +} +@supports (a: b) /* keep me */ { + .block-comment { + color: teal; + } +} diff --git a/packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.less b/packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.less new file mode 100644 index 0000000000..f72f3c0102 --- /dev/null +++ b/packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.less @@ -0,0 +1,58 @@ +// A `//` comment inside an unknown at-rule prelude is scanned as raw text by +// $parseUntil, so it used to survive into the CSS and swallow the rest of the +// line with it. + +@supports selector( + :focus-visible // a line comment +) { + a { + color: red; + } +} + +@supports (display: grid) // trailing comment +{ + .grid { + display: grid; + } +} + +@supports (display: flex)// no space before it +{ + .flex { + display: flex; + } +} + +// A `//` inside a URL function, or after a scheme's `:`, is part of an +// address. These must be left alone. +@supports (background: url(http://example.com/a.png)) { + .absolute-url { + color: blue; + } +} + +@document url-prefix(http://example.com/) { + .doc { + color: green; + } +} + +@supports (background: url(//cdn.example.com/x.png)) { + .protocol-relative { + background: url(//cdn.example.com/x.png); + } +} + +@supports (background: url( //cdn.example.com/y.png)) { + .protocol-relative-spaced { + color: navy; + } +} + +// Block comments are valid CSS and stay. +@supports (a: b) /* keep me */ { + .block-comment { + color: teal; + } +}