From 4cc5680c364cc1a24c3e9ef676548958dd573276 Mon Sep 17 00:00:00 2001 From: dchaudhari7177 <111210939+dchaudhari7177@users.noreply.github.com> Date: Sun, 20 Sep 2026 12:16:16 +0530 Subject: [PATCH 1/2] fix: strip `//` comments from unknown at-rule preludes An unknown at-rule's prelude is scanned as raw text by $parseUntil, which handled /* */ but had no case for //. A line comment inside `@supports selector(...)` was therefore copied into the CSS, taking the rest of the line with it -- and any quote inside the comment was picked up as a real string, mangling the output further. Skip from // to the end of the line, but only where whitespace precedes it. That is what separates a comment from a URL: the scanner absorbs comments while skipping whitespace, which is why `http://host` and a protocol-relative `url(//host/x.png)` are not comments, and both must keep working. Gate it on a new stripLineComments flag rather than applying it to every permissive value. A custom property's value is preserved verbatim, so a // inside `--this: () => { ... }` is content, not a comment. Closes #3527 --- packages/less/lib/less/parser/parser-input.js | 21 ++++++++- packages/less/lib/less/parser/parser.js | 6 +-- .../at-rules-unknown-line-comments.css | 31 +++++++++++++ .../at-rules-unknown-line-comments.less | 45 +++++++++++++++++++ 4 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.css create mode 100644 packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.less diff --git a/packages/less/lib/less/parser/parser-input.js b/packages/less/lib/less/parser/parser-input.js index 9a9dc3924..2f05e64b7 100644 --- a/packages/less/lib/less/parser/parser-input.js +++ b/packages/less/lib/less/parser/parser-input.js @@ -214,7 +214,7 @@ 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; @@ -278,6 +278,25 @@ export default () => { i++; inComment = true; blockDepth++; + } else if (stripLineComments && + input.charAt(i + 1) === '/' && + /\s/.test(input.charAt(i - 1))) { + // A `//` comment, which is not CSS and must not + // reach the output. Only when whitespace precedes + // it: everywhere else the scanner absorbs comments + // while skipping whitespace, which is exactly why + // `http://host` and a protocol-relative + // `url(//host/x.png)` are not comments. + 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 '\'': diff --git a/packages/less/lib/less/parser/parser.js b/packages/less/lib/less/parser/parser.js index d21096cd2..fa7efa04e 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 000000000..888dd3df6 --- /dev/null +++ b/packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.css @@ -0,0 +1,31 @@ +@supports selector( + :focus-visible ) { + a { + color: red; + } +} +@supports (display: grid) { + .grid { + display: grid; + } +} +@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 (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 000000000..763d50dae --- /dev/null +++ b/packages/test-data/tests-unit/at-rules-unknown-line-comments/at-rules-unknown-line-comments.less @@ -0,0 +1,45 @@ +// 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; + } +} + +// A `//` that is not preceded by whitespace is not a comment: it is part of a +// URL. 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); + } +} + +// Block comments are valid CSS and stay. +@supports (a: b) /* keep me */ { + .block-comment { + color: teal; + } +} From 3c7bf86ff29cb4fed6eb3f539dfac2da4b5c127b Mon Sep 17 00:00:00 2001 From: dchaudhari7177 <111210939+dchaudhari7177@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:23:31 +0530 Subject: [PATCH 2/2] fix: tell a `//` comment from an address by URL context, not whitespace Requiring whitespace before `//` kept `url( //host/x.png)` wrong (the URL was stripped) and let `(display: flex)// comment` through. Track whether the scanner is inside `url(`, `url-prefix(` or `domain(`, where the value parser also stops absorbing comments, and treat a `//` after a `:` as a scheme. Every other `//` in an unknown at-rule prelude is a comment. --- packages/less/lib/less/parser/parser-input.js | 25 +++++++++++++------ .../at-rules-unknown-line-comments.css | 10 ++++++++ .../at-rules-unknown-line-comments.less | 17 +++++++++++-- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/less/lib/less/parser/parser-input.js b/packages/less/lib/less/parser/parser-input.js index 2f05e64b7..ff3390492 100644 --- a/packages/less/lib/less/parser/parser-input.js +++ b/packages/less/lib/less/parser/parser-input.js @@ -221,6 +221,9 @@ export default () => { 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; @@ -280,13 +283,15 @@ export default () => { blockDepth++; } else if (stripLineComments && input.charAt(i + 1) === '/' && - /\s/.test(input.charAt(i - 1))) { + urlDepth === 0 && + input.charAt(i - 1) !== ':') { // A `//` comment, which is not CSS and must not - // reach the output. Only when whitespace precedes - // it: everywhere else the scanner absorbs comments - // while skipping whitespace, which is exactly why - // `http://host` and a protocol-relative - // `url(//host/x.png)` are not comments. + // 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); @@ -317,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++; @@ -333,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/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 index 888dd3df6..215eb752d 100644 --- 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 @@ -9,6 +9,11 @@ display: grid; } } +@supports (display: flex) { + .flex { + display: flex; + } +} @supports (background: url(http://example.com/a.png)) { .absolute-url { color: blue; @@ -24,6 +29,11 @@ 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 index 763d50dae..f72f3c010 100644 --- 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 @@ -17,8 +17,15 @@ } } -// A `//` that is not preceded by whitespace is not a comment: it is part of a -// URL. These must be left alone. +@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; @@ -37,6 +44,12 @@ } } +@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 {