From 722acdfebff0d240ad755ae8456e9fe05475b1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B5=AD?= Date: Fri, 25 Sep 2026 20:16:58 +0900 Subject: [PATCH] Don't treat `\` as an escape inside CSS comments (#20508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The CSS parser treated `\` inside comments as an escape and skipped the character after it. Because of that, a comment ending in `\*/` was never closed, and the CSS that followed was swallowed or turned into a broken rule: ```css /* C:\temp\*/ .a { color: red } ``` Before this change the `.a` rule disappeared entirely, and `/* \*/ .a { color: red }` produced the selector `* \*/ .a`. The same thing happened to comments inside declaration values, where the comment ran on until the next `*/` it could find, pulling following declarations into the value. Per [CSS Syntax Level 3 ยง4.3.2](https://www.w3.org/TR/css-syntax-3/#consume-comment), a comment ends at the first `*/` and escapes aren't processed inside comments. This removes the backslash handling from both comment-scanning loops in `css-parser.ts` (top-level and inside declaration values). The existing test `/*Hello, \*\/ world!*/` keeps passing, since that input contains no `*/` before the final one. ## Test plan Added two tests to `packages/tailwindcss/src/css-parser.test.ts` (both run with Unix and Windows line endings): - a top-level comment ending in `\*/` followed by a rule - a comment ending in `\*/` inside a custom property value, followed by another declaration Both fail without the change to `css-parser.ts` and pass with it. ```sh pnpm vitest run packages/tailwindcss/src/css-parser.test.ts pnpm vitest run --project tailwindcss pnpm run lint ``` --------- Co-authored-by: Robin Malfait Co-authored-by: Robin Malfait --- CHANGELOG.md | 1 + packages/tailwindcss/src/css-parser.test.ts | 39 +++++++++++++++++++++ packages/tailwindcss/src/css-parser.ts | 27 +++++++------- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2cbe5a7a..479fca86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434)) - Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466)) - Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512)) +- Ensure CSS comments ending with `\*/` are closed correctly instead of swallowing the CSS that follows (e.g. `/* C:\temp\*/`) ([#20508](https://github.com/tailwindlabs/tailwindcss/pull/20508)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/css-parser.test.ts b/packages/tailwindcss/src/css-parser.test.ts index 8df19f96c..0dcbc0a41 100644 --- a/packages/tailwindcss/src/css-parser.test.ts +++ b/packages/tailwindcss/src/css-parser.test.ts @@ -31,6 +31,23 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { ).toEqual([]) }) + it('should end a comment at `*/` even when it is preceded by a `\\`', () => { + expect( + parse(css` + /* C:\temp\*/ + .foo { + color: red; + } + `), + ).toEqual([ + { + kind: 'rule', + selector: '.foo', + nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }], + }, + ]) + }) + it('should parse a comment inside of a selector and ignore it', () => { expect( parse(css` @@ -448,6 +465,28 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { ]) }) + it('should end a comment in a custom property at `*/` even when it is preceded by a `\\`', () => { + expect( + parse(css` + --foo: /* C:\temp\*/ bar; + --bar: /* baz */ qux; + `), + ).toEqual([ + { + kind: 'declaration', + property: '--foo', + value: '/* C:\\temp\\*/ bar', + important: false, + }, + { + kind: 'declaration', + property: '--bar', + value: '/* baz */ qux', + important: false, + }, + ]) + }) + it('should parse empty custom properties', () => { expect( parse(css` diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index 3dd5e6ffe..0896881e6 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -128,19 +128,24 @@ export function parse(input: string, opts?: ParseOptions) { // ^^^^^^^^^^^^^ // } // ``` + // + // The escape character `\` in comments are ignored, this means that a + // the end of a comment preceded by `\` does _not_ mean that the end of the + // comment is escaped and therefore we have to keep parsing. + // + // This is a valid comment: + // ``` + // /*C:\*/ + // ``` + // See: https://www.w3.org/TR/css-syntax-3/#consume-comment else if (currentChar === SLASH && input.charCodeAt(i + 1) === ASTERISK) { let start = i for (let j = i + 2; j < input.length; j++) { peekChar = input.charCodeAt(j) - // Current character is a `\` therefore the next character is escaped. - if (peekChar === BACKSLASH) { - j += 1 - } - - // End of the comment - else if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) { + // End of the comment. + if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) { i = j + 1 break } @@ -224,13 +229,9 @@ export function parse(input: string, opts?: ParseOptions) { else if (peekChar === SLASH && input.charCodeAt(j + 1) === ASTERISK) { for (let k = j + 2; k < input.length; k++) { peekChar = input.charCodeAt(k) - // Current character is a `\` therefore the next character is escaped. - if (peekChar === BACKSLASH) { - k += 1 - } - // End of the comment - else if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) { + // End of the comment. + if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) { j = k + 1 break }