From c9ebd6a88553a3041d68c6063ec095fdd601f4b7 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 21 Jan 2026 14:09:30 -0700 Subject: [PATCH] feat: export `parseCss` from `svelte/compiler` (#17496) * feat: export from * changeset * fix: Errors * set_source, so state is correct if an error occurs * simplify --------- Co-authored-by: Rich Harris --- .changeset/loud-buses-guess.md | 5 + packages/svelte/src/compiler/index.js | 26 +++- .../src/compiler/phases/1-parse/index.js | 14 ++ .../src/compiler/phases/1-parse/read/style.js | 30 ++-- packages/svelte/tests/css-parse.test.ts | 138 ++++++++++++++++++ packages/svelte/types/index.d.ts | 6 + 6 files changed, 205 insertions(+), 14 deletions(-) create mode 100644 .changeset/loud-buses-guess.md create mode 100644 packages/svelte/tests/css-parse.test.ts diff --git a/.changeset/loud-buses-guess.md b/.changeset/loud-buses-guess.md new file mode 100644 index 0000000000..d6c7c3d5a4 --- /dev/null +++ b/.changeset/loud-buses-guess.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: export `parseCss` from `svelte/compiler` diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 9095a2a2f2..acfef6a320 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -3,8 +3,9 @@ /** @import { AST } from './public.js' */ import { walk as zimmerframe_walk } from 'zimmerframe'; import { convert } from './legacy.js'; -import { parse as _parse } from './phases/1-parse/index.js'; +import { parse as _parse, Parser } from './phases/1-parse/index.js'; import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js'; +import { parse_stylesheet } from './phases/1-parse/read/style.js'; import { analyze_component, analyze_module } from './phases/2-analyze/index.js'; import { transform_component, transform_module } from './phases/3-transform/index.js'; import { validate_component_options, validate_module_options } from './validate-options.js'; @@ -118,6 +119,29 @@ export function parse(source, { modern, loose } = {}) { return to_public_ast(source, ast, modern); } +/** + * The parseCss function parses a CSS stylesheet, returning its abstract syntax tree. + * + * @param {string} source The CSS source code + * @returns {Omit} + */ +export function parseCss(source) { + source = remove_bom(source); + state.reset({ warning: () => false, filename: undefined }); + + state.set_source(source); + + const parser = Parser.forCss(source); + const children = parse_stylesheet(parser); + + return { + type: 'StyleSheet', + start: 0, + end: source.length, + children + }; +} + /** * @param {string} source * @param {AST.Root} ast diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 3b5f83d58f..88b4352e8a 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -34,6 +34,20 @@ export class Parser { /** */ index = 0; + /** + * Creates a minimal parser instance for CSS-only parsing. + * Skips Svelte component parsing setup. + * @param {string} source + * @returns {Parser} + */ + static forCss(source) { + const parser = Object.create(Parser.prototype); + parser.template = source; + parser.index = 0; + parser.loose = false; + return parser; + } + /** Whether we're parsing in TypeScript mode */ ts = false; 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 80ab234d92..c88a3707c1 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -24,10 +24,11 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/; */ export default function read_style(parser, start, attributes) { const content_start = parser.index; - const children = read_body(parser, ' p.match('= p.template.length); const content_end = parser.index; - parser.read(/^<\/style\s*>/); + parser.eat('/); return { type: 'StyleSheet', @@ -46,20 +47,14 @@ export default function read_style(parser, start, attributes) { /** * @param {Parser} parser - * @param {string} close - * @returns {any[]} + * @param {(parser: Parser) => boolean} finished + * @returns {Array} */ -function read_body(parser, close) { +function read_body(parser, finished) { /** @type {Array} */ const children = []; - while (parser.index < parser.template.length) { - allow_comment_or_whitespace(parser); - - if (parser.match(close)) { - return children; - } - + while ((allow_comment_or_whitespace(parser), !finished(parser))) { if (parser.match('@')) { children.push(read_at_rule(parser)); } else { @@ -67,7 +62,7 @@ function read_body(parser, close) { } } - e.expected_token(parser.template.length, close); + return children; } /** @@ -627,3 +622,12 @@ function allow_comment_or_whitespace(parser) { parser.allow_whitespace(); } } + +/** + * Parse standalone CSS content (not wrapped in `