From a1d5035d1777be6ab177d3749668f599ccefc445 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:13:47 +0200 Subject: [PATCH] fix: preserve CSS comments in the AST printer (#18637) Adds a new `comments` array to the stylesheet node which CSS comments are added to. Is subsequently used in `print` to see them in the output. Alternative to #18475 --- .changeset/soft-cats-comment.md | 5 + packages/svelte/src/compiler/index.js | 3 +- .../src/compiler/phases/1-parse/index.js | 4 + .../src/compiler/phases/1-parse/read/style.js | 82 ++-- .../phases/2-analyze/css/css-analyze.js | 3 + .../compiler/phases/2-analyze/css/css-warn.js | 3 + .../compiler/phases/3-transform/css/index.js | 3 + packages/svelte/src/compiler/print/index.js | 361 +++++++++++------- packages/svelte/src/compiler/types/css.d.ts | 10 + packages/svelte/tests/css-parse.test.ts | 14 +- .../parser-legacy/samples/css/output.json | 1 + .../whitespace-after-style-tag/output.json | 1 + .../samples/css-nth-syntax/output.json | 11 +- .../samples/css-pseudo-classes/output.json | 173 ++++++++- .../script-style-no-markup/output.json | 19 + .../semicolon-inside-quotes/output.json | 4 +- .../print/samples/style-comments/input.svelte | 26 ++ .../samples/style-comments/output.svelte | 28 ++ packages/svelte/types/index.d.ts | 10 + 19 files changed, 586 insertions(+), 175 deletions(-) create mode 100644 .changeset/soft-cats-comment.md create mode 100644 packages/svelte/tests/print/samples/style-comments/input.svelte create mode 100644 packages/svelte/tests/print/samples/style-comments/output.svelte diff --git a/.changeset/soft-cats-comment.md b/.changeset/soft-cats-comment.md new file mode 100644 index 0000000000..01b07c4d7d --- /dev/null +++ b/.changeset/soft-cats-comment.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve CSS comments in the AST printer diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 1d822514b9..599206ac6a 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -141,7 +141,8 @@ export function parseCss(source) { type: 'StyleSheetFile', start: 0, end: source.length, - children + children, + comments: parser.css_comments }; } diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 02d1629c99..fa7b292640 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -50,6 +50,9 @@ export class Parser { /** */ index = 0; + /** @type {AST.CSS.CSSComment[]} */ + css_comments = []; + /** * Creates a minimal parser instance for CSS-only parsing. * Skips Svelte component parsing setup. @@ -61,6 +64,7 @@ export class Parser { parser.template = source; parser.index = 0; parser.loose = false; + parser.css_comments = []; return parser; } diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 160e5da277..5eb8c2b3e9 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -24,6 +24,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/; */ export default function read_style(parser, start, attributes) { const content_start = parser.index; + parser.css_comments = []; const children = read_body(parser, (p) => p.match('= p.template.length); const content_end = parser.index; @@ -36,6 +37,7 @@ export default function read_style(parser, start, attributes) { end: parser.index, attributes, children, + comments: parser.css_comments, content: { start: content_start, end: content_end, @@ -229,18 +231,22 @@ function read_selector(parser, inside_pseudo_class = false) { end: parser.index }); } else if (parser.eat('::')) { + const name = read_identifier(parser); + /** @type {AST.CSS.SelectorList | null} */ + let args = null; + + if (parser.eat('(')) { + args = read_selector_list(parser, true); + parser.eat(')', true); + } + relative_selector.selectors.push({ type: 'PseudoElementSelector', - name: read_identifier(parser), + name, start, - end: parser.index + end: parser.index, + ...(args && { args }) }); - // We read the inner selectors of a pseudo element to ensure it parses correctly, - // but we don't do anything with the result. - if (parser.eat('(')) { - read_selector_list(parser, true); - parser.eat(')', true); - } } else if (parser.eat(':')) { const name = read_identifier(parser); @@ -323,7 +329,7 @@ function read_selector(parser, inside_pseudo_class = false) { } const index = parser.index; - allow_comment_or_whitespace(parser); + allow_comment_or_whitespace(parser, false); if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) { // rewind, so we know whether to continue building the selector list @@ -449,7 +455,7 @@ function read_block_item(parser) { // read ahead to understand whether we're dealing with a declaration or a nested rule. // this involves some duplicated work, but avoids a try-catch that would disguise errors const start = parser.index; - read_value(parser); + read_value(parser, false); const char = parser.template[parser.index]; parser.index = start; @@ -492,10 +498,13 @@ function read_declaration(parser) { /** * @param {Parser} parser + * @param {boolean} [capture_comments] * @returns {string} */ -function read_value(parser) { +function read_value(parser, capture_comments = true) { let value = ''; + /** @type {AST.CSS.CSSComment[]} */ + const value_comments = []; let escaped = false; let in_url = false; @@ -523,6 +532,13 @@ function read_value(parser) { } else if (char === '(' && value.slice(-3) === 'url') { in_url = true; } else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) { + const leading_whitespace = value.length - value.trimStart().length; + for (const comment of value_comments) { + comment.position = Math.max( + 0, + /** @type {number} */ (comment.position) - leading_whitespace + ); + } return value.trim(); } else if ( char === '/' && @@ -530,13 +546,11 @@ function read_value(parser) { !quote_mark && parser.template[parser.index + 1] === '*' ) { - parser.index += 2; - while (parser.index < parser.template.length) { - if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') { - parser.index += 2; - break; - } - parser.index++; + const comment = read_comment(parser); + if (capture_comments) { + comment.position = value.length; + parser.css_comments.push(comment); + value_comments.push(comment); } continue; } @@ -624,13 +638,16 @@ function read_identifier(parser) { return identifier; } -/** @param {Parser} parser */ -function allow_comment_or_whitespace(parser) { +/** + * @param {Parser} parser + * @param {boolean} [capture_comments] + */ +function allow_comment_or_whitespace(parser, capture_comments = true) { parser.allow_whitespace(); while (parser.match('/*') || parser.match('