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 <rich.harris@vercel.com>
pull/17509/head
Elliott Johnson 6 months ago committed by GitHub
parent 5ce6186889
commit c9ebd6a885
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: export `parseCss` from `svelte/compiler`

@ -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<AST.CSS.StyleSheet, 'attributes' | 'content'>}
*/
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

@ -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;

@ -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, '</style');
const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
const content_end = parser.index;
parser.read(/^<\/style\s*>/);
parser.eat('</style', true);
parser.read(/^\s*>/);
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<AST.CSS.Rule | AST.CSS.Atrule>}
*/
function read_body(parser, close) {
function read_body(parser, finished) {
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
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 `<style>`).
* @param {Parser} parser
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
*/
export function parse_stylesheet(parser) {
return read_body(parser, (p) => p.index >= p.template.length);
}

@ -0,0 +1,138 @@
import { assert, describe, it } from 'vitest';
import { parseCss } from 'svelte/compiler';
describe('parseCss', () => {
it('parses a simple rule', () => {
const ast = parseCss('div { color: red; }');
assert.equal(ast.type, 'StyleSheet');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Rule');
});
it('parses at-rules', () => {
const ast = parseCss('@media (min-width: 800px) { div { color: red; } }');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Atrule');
if (ast.children[0].type === 'Atrule') {
assert.equal(ast.children[0].name, 'media');
}
});
it('parses @import', () => {
const ast = parseCss("@import 'foo.css';");
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Atrule');
if (ast.children[0].type === 'Atrule') {
assert.equal(ast.children[0].name, 'import');
assert.equal(ast.children[0].block, null);
}
});
it('parses multiple rules', () => {
const ast = parseCss('div { color: red; } span { color: blue; }');
assert.equal(ast.children.length, 2);
});
it('has correct start/end positions', () => {
const ast = parseCss('div { color: red; }');
assert.equal(ast.start, 0);
assert.equal(ast.end, 19);
});
it('strips BOM', () => {
const ast = parseCss('\uFEFFdiv { color: red; }');
assert.equal(ast.start, 0);
assert.equal(ast.end, 19);
});
it('parses nested rules', () => {
const ast = parseCss('div { color: red; span { color: blue; } }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
assert.equal(rule.type, 'Rule');
if (rule.type === 'Rule') {
assert.equal(rule.block.children.length, 2); // declaration + nested rule
}
});
it('parses empty stylesheet', () => {
const ast = parseCss('');
assert.equal(ast.type, 'StyleSheet');
assert.equal(ast.children.length, 0);
assert.equal(ast.start, 0);
assert.equal(ast.end, 0);
});
it('parses whitespace-only stylesheet', () => {
const ast = parseCss(' \n\t ');
assert.equal(ast.children.length, 0);
});
it('parses comments', () => {
const ast = parseCss('/* comment */ div { color: red; }');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Rule');
});
it('parses complex selectors', () => {
const ast = parseCss('div > span + p ~ a { color: red; }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
if (rule.type === 'Rule') {
assert.equal(rule.prelude.type, 'SelectorList');
assert.equal(rule.prelude.children.length, 1);
// div > span + p ~ a has 4 relative selectors
assert.equal(rule.prelude.children[0].children.length, 4);
}
});
it('parses pseudo-classes and pseudo-elements', () => {
const ast = parseCss('div:hover::before { color: red; }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
if (rule.type === 'Rule') {
const selectors = rule.prelude.children[0].children[0].selectors;
assert.equal(selectors.length, 3); // div, :hover, ::before
assert.equal(selectors[0].type, 'TypeSelector');
assert.equal(selectors[1].type, 'PseudoClassSelector');
assert.equal(selectors[2].type, 'PseudoElementSelector');
}
});
it('parses @keyframes', () => {
const ast = parseCss('@keyframes fade { from { opacity: 0; } to { opacity: 1; } }');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Atrule');
if (ast.children[0].type === 'Atrule') {
assert.equal(ast.children[0].name, 'keyframes');
assert.notEqual(ast.children[0].block, null);
}
});
it('parses class and id selectors', () => {
const ast = parseCss('.foo#bar { color: red; }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
if (rule.type === 'Rule') {
const selectors = rule.prelude.children[0].children[0].selectors;
assert.equal(selectors.length, 2);
assert.equal(selectors[0].type, 'ClassSelector');
assert.equal(selectors[1].type, 'IdSelector');
}
});
it('parses attribute selectors', () => {
const ast = parseCss('[data-foo="bar"] { color: red; }');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
if (rule.type === 'Rule') {
const selectors = rule.prelude.children[0].children[0].selectors;
assert.equal(selectors.length, 1);
assert.equal(selectors[0].type, 'AttributeSelector');
if (selectors[0].type === 'AttributeSelector') {
assert.equal(selectors[0].name, 'data-foo');
assert.equal(selectors[0].value, 'bar');
}
}
});
});

@ -884,6 +884,12 @@ declare module 'svelte/compiler' {
modern?: false;
loose?: boolean;
} | undefined): Record<string, any>;
/**
* The parseCss function parses a CSS stylesheet, returning its abstract syntax tree.
*
* @param source The CSS source code
* */
export function parseCss(source: string): Omit<AST.CSS.StyleSheet, "attributes" | "content">;
/**
* @deprecated Replace this with `import { walk } from 'estree-walker'`
* */

Loading…
Cancel
Save