refactor: upgrade to csstree v3 (#5455)

The new version is aware of modern colors and other new css features.
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2025-11-23 14:32:12 +03:00
1 parent 1662637d92
commit 02ca084a0c
24 files changed
+1053 -335

No files matched your search

+772
View File
@@ -0,0 +1,772 @@
// Minimal-yet-safe TypeScript typings for css-tree v3.x
// Covers: AST node types, parse/generate, walk/find, List, lexer API,
// value definition syntax helpers, and common utils used in this project.
declare module "css-tree" {
// -------------------------------------------------
// Common
// -------------------------------------------------
export interface SourceLocation {
source: string;
start: { offset: number; line: number; column: number };
end: { offset: number; line: number; column: number };
}
export interface BaseNode {
type: string;
loc: SourceLocation | null;
}
// Doubly-linked list used by csstree
export interface ListItem<T> {
prev: ListItem<T> | null;
next: ListItem<T> | null;
data: T;
}
export class List<T> implements Iterable<T> {
static createItem<T>(data: T): ListItem<T>;
constructor();
// iteration
[Symbol.iterator](): IterableIterator<T>;
// props
readonly size: number;
readonly isEmpty: boolean;
readonly first: T | null;
readonly last: T | null;
// traversal helpers
forEach(
fn: (data: T, item: ListItem<T>, list: List<T>) => void,
thisArg?: any
): void;
forEachRight(
fn: (data: T, item: ListItem<T>, list: List<T>) => void,
thisArg?: any
): void;
// conversion
fromArray(items: T[]): this;
toArray(): T[];
toJSON(): T[];
// mutation (minimal surface used by walkers)
clear(): void;
remove(item: ListItem<T>): ListItem<T>;
insert(item: ListItem<T>, before?: ListItem<T> | null): void;
insertData(data: T, before?: ListItem<T> | null): void;
append(item: ListItem<T>): void;
appendData(data: T): void;
prepend(item: ListItem<T>): void;
prependData(data: T): void;
replace(oldItem: ListItem<T>, newItemOrList: ListItem<T> | List<T>): void;
}
// -------------------------------------------------
// AST Nodes (as per docs/ast.md)
// -------------------------------------------------
export interface AnPlusB extends BaseNode {
type: "AnPlusB";
a: string | null;
b: string | null;
}
export interface Atrule extends BaseNode {
type: "Atrule";
name: string;
prelude: AtrulePrelude | Raw | null;
block: Block | null;
}
export interface AtrulePrelude extends BaseNode {
type: "AtrulePrelude";
children: List<CssNode>;
}
export interface AttributeSelector extends BaseNode {
type: "AttributeSelector";
name: Identifier;
matcher: string | null;
value: StringNode | Identifier | null;
flags: string | null;
}
export interface Block extends BaseNode {
type: "Block";
children: List<Atrule | Rule | Declaration>;
}
export interface Brackets extends BaseNode {
type: "Brackets";
children: List<CssNode>;
}
export interface CDC extends BaseNode {
type: "CDC";
}
export interface CDO extends BaseNode {
type: "CDO";
}
export interface ClassSelector extends BaseNode {
type: "ClassSelector";
name: string;
}
export interface Combinator extends BaseNode {
type: "Combinator";
name: string;
}
export interface Comment extends BaseNode {
type: "Comment";
value: string;
}
export interface Condition extends BaseNode {
type: "Condition";
kind: string;
children: List<
| Identifier
| Feature
| FeatureFunction
| FeatureRange
| SupportsDeclaration
>;
}
export interface Declaration extends BaseNode {
type: "Declaration";
important: boolean | string;
property: string;
value: Value | Raw;
}
export interface DeclarationList extends BaseNode {
type: "DeclarationList";
children: List<Declaration | Atrule | Rule>;
}
export interface Dimension extends BaseNode {
type: "Dimension";
value: string;
unit: string;
}
export interface Feature extends BaseNode {
type: "Feature";
kind: string;
name: string;
value: Identifier | NumberNode | Dimension | Ratio | FunctionNode | null;
}
export interface FeatureFunction extends BaseNode {
type: "FeatureFunction";
kind: string;
feature: string;
value: Declaration | Selector;
}
export interface FeatureRange extends BaseNode {
type: "FeatureRange";
kind: string;
left: Identifier | NumberNode | Dimension | Ratio | FunctionNode;
leftComparison: string;
middle: Identifier | NumberNode | Dimension | Ratio | FunctionNode;
rightComparison: string | null;
right: Identifier | NumberNode | Dimension | Ratio | FunctionNode | null;
}
export interface FunctionNode extends BaseNode {
type: "Function";
name: string;
children: List<CssNode>;
}
export interface GeneralEnclosed extends BaseNode {
type: "GeneralEnclosed";
kind: string;
function: string | null;
children: List<CssNode>;
}
export interface Hash extends BaseNode {
type: "Hash";
value: string;
}
export interface IdSelector extends BaseNode {
type: "IdSelector";
name: string;
}
export interface Identifier extends BaseNode {
type: "Identifier";
name: string;
}
export interface Layer extends BaseNode {
type: "Layer";
name: string;
}
export interface LayerList extends BaseNode {
type: "LayerList";
children: List<Layer>;
}
export interface MediaQuery extends BaseNode {
type: "MediaQuery";
modifier: string | null;
mediaType: string | null;
condition: Condition | null;
}
export interface MediaQueryList extends BaseNode {
type: "MediaQueryList";
children: List<MediaQuery>;
}
export interface NestingSelector extends BaseNode {
type: "NestingSelector";
}
export interface Nth extends BaseNode {
type: "Nth";
nth: AnPlusB | Identifier;
selector: SelectorList | null;
}
export interface NumberNode extends BaseNode {
type: "Number";
value: string;
}
export interface Operator extends BaseNode {
type: "Operator";
value: string;
}
export interface Parentheses extends BaseNode {
type: "Parentheses";
children: List<CssNode>;
}
export interface Percentage extends BaseNode {
type: "Percentage";
value: string;
}
export interface PseudoClassSelector extends BaseNode {
type: "PseudoClassSelector";
name: string;
children: List<Raw> | null;
}
export interface PseudoElementSelector extends BaseNode {
type: "PseudoElementSelector";
name: string;
children: List<Raw> | null;
}
export interface Ratio extends BaseNode {
type: "Ratio";
left: NumberNode | FunctionNode;
right: NumberNode | FunctionNode | null;
}
export interface Raw extends BaseNode {
type: "Raw";
value: string;
}
export interface Rule extends BaseNode {
type: "Rule";
prelude: SelectorList | Raw;
block: Block;
}
export interface Scope extends BaseNode {
type: "Scope";
root: SelectorList | Raw | null;
limit: SelectorList | Raw | null;
}
export interface Selector extends BaseNode {
type: "Selector";
children: List<
| TypeSelector
| IdSelector
| ClassSelector
| AttributeSelector
| PseudoClassSelector
| PseudoElementSelector
| Combinator
| NestingSelector
>;
}
export interface SelectorList extends BaseNode {
type: "SelectorList";
children: List<Selector | Raw>;
}
export interface StringNode extends BaseNode {
type: "String";
value: string;
}
export interface StyleSheet extends BaseNode {
type: "StyleSheet";
children: List<Comment | CDO | CDC | Atrule | Rule | Raw>;
}
export interface SupportsDeclaration extends BaseNode {
type: "SupportsDeclaration";
declaration: Declaration;
}
export interface TypeSelector extends BaseNode {
type: "TypeSelector";
name: string;
}
export interface UnicodeRange extends BaseNode {
type: "UnicodeRange";
value: string;
}
export interface Url extends BaseNode {
type: "Url";
value: string;
}
export interface Value extends BaseNode {
type: "Value";
children: List<CssNode>;
}
export interface WhiteSpace extends BaseNode {
type: "WhiteSpace";
value: string;
}
export type CssNode =
| AnPlusB
| Atrule
| AtrulePrelude
| AttributeSelector
| Block
| Brackets
| CDC
| CDO
| ClassSelector
| Combinator
| Comment
| Condition
| Declaration
| DeclarationList
| Dimension
| Feature
| FeatureFunction
| FeatureRange
| FunctionNode
| GeneralEnclosed
| Hash
| IdSelector
| Identifier
| Layer
| LayerList
| MediaQuery
| MediaQueryList
| NestingSelector
| Nth
| NumberNode
| Operator
| Parentheses
| Percentage
| PseudoClassSelector
| PseudoElementSelector
| Ratio
| Raw
| Rule
| Scope
| Selector
| SelectorList
| StringNode
| StyleSheet
| SupportsDeclaration
| TypeSelector
| UnicodeRange
| Url
| Value
| WhiteSpace;
// A plain-object form where children are arrays (for JSON, etc.)
export type CssNodePlain = Omit<CssNode, "children" | "loc"> & {
children?: CssNodePlain[] | null;
loc?: SourceLocation | null;
};
// -------------------------------------------------
// Parsing / Generation
// -------------------------------------------------
export type ParseContext =
| "stylesheet"
| "atrule"
| "atrulePrelude"
| "mediaQueryList"
| "mediaQuery"
| "rule"
| "selectorList"
| "selector"
| "block"
| "declarationList"
| "declaration"
| "value";
export interface ParseOptions {
context?: ParseContext;
atrule?: string | null;
positions?: boolean;
onParseError?: (error: any, fallbackNode?: Raw) => void;
onComment?: (value: string, loc: SourceLocation | null) => void;
onToken?:
| ((type: number, start: number, end: number, index: number) => void)
| Array<{ type: number; start: number; end: number }>
| null;
filename?: string;
offset?: number;
line?: number;
column?: number;
parseAtrulePrelude?: boolean;
parseRulePrelude?: boolean;
parseValue?: boolean;
parseCustomProperty?: boolean;
}
// Strict overloads by context
export function parse(source: string): StyleSheet;
export function parse(
source: string,
options: ParseOptions & { context?: "stylesheet" }
): StyleSheet;
export function parse(
source: string,
options: ParseOptions & { context: "atrule" }
): Atrule;
export function parse(
source: string,
options: ParseOptions & { context: "atrulePrelude" }
): AtrulePrelude;
export function parse(
source: string,
options: ParseOptions & { context: "mediaQueryList" }
): MediaQueryList;
export function parse(
source: string,
options: ParseOptions & { context: "mediaQuery" }
): MediaQuery;
export function parse(
source: string,
options: ParseOptions & { context: "rule" }
): Rule;
export function parse(
source: string,
options: ParseOptions & { context: "selectorList" }
): SelectorList;
export function parse(
source: string,
options: ParseOptions & { context: "selector" }
): Selector;
export function parse(
source: string,
options: ParseOptions & { context: "block" }
): Block;
export function parse(
source: string,
options: ParseOptions & { context: "declarationList" }
): DeclarationList;
export function parse(
source: string,
options: ParseOptions & { context: "declaration" }
): Declaration;
export function parse(
source: string,
options: ParseOptions & { context: "value" }
): Value;
// Fallback
export function parse(source: string, options: ParseOptions): CssNode;
export function generate(
ast: CssNode,
options: {
sourceMap: true;
decorator?: (node: CssNode) => any;
mode?: "spec" | "safe";
}
): { css: string; map: any };
export function generate(
ast: CssNode,
options?: {
sourceMap?: false;
decorator?: (node: CssNode) => any;
mode?: "spec" | "safe";
}
): string;
// -------------------------------------------------
// Traversal helpers
// -------------------------------------------------
export type WalkHandler = (
node: CssNode,
item?: ListItem<CssNode>,
list?: List<CssNode>
) => any;
export interface WalkOptions {
enter?: WalkHandler;
leave?: WalkHandler;
visit?: CssNode["type"] | null;
reverse?: boolean;
}
export function walk(ast: CssNode, options: WalkOptions | WalkHandler): void;
export function find(ast: CssNode, fn: WalkHandler): CssNode | null;
export function findLast(ast: CssNode, fn: WalkHandler): CssNode | null;
export function findAll(ast: CssNode, fn: WalkHandler): CssNode[];
// -------------------------------------------------
// Utils
// -------------------------------------------------
export function clone<T extends CssNode>(ast: T): T;
export function fromPlainObject<T extends { children?: any }>(obj: T): T;
export function toPlainObject<T extends { children?: any }>(ast: T): T;
export interface PropertyInfo {
basename: string;
name: string;
hack: string;
vendor: string;
prefix: string;
custom: boolean;
}
export function property(name: string): PropertyInfo;
export interface KeywordInfo {
basename: string;
name: string;
vendor: string;
prefix: string;
custom: boolean;
}
export function keyword(name: string): KeywordInfo;
export namespace ident {
function decode(value: string): string;
function encode(value: string): string;
}
export namespace string {
function decode(value: string): string;
function encode(value: string, preferSingleQuotes?: boolean): string;
}
export namespace url {
function decode(value: string): string;
function encode(value: string): string;
}
// -------------------------------------------------
// Definition syntax (values) sub-API
// -------------------------------------------------
export namespace definitionSyntax {
// AST nodes for definition syntax are intentionally minimal here
interface Base {
type: string;
}
interface Group extends Base {
type: "Group";
terms: Base[];
combinator: " " | "|" | "||" | "&&";
disallowEmpty: boolean;
explicit: boolean;
}
interface Keyword extends Base {
type: "Keyword";
name: string;
}
interface Function extends Base {
type: "Function";
name: string;
}
interface String extends Base {
type: "String";
value: string;
}
interface Property extends Base {
type: "Property";
name: string;
}
interface Type extends Base {
type: "Type";
name: string;
opts: Range | null;
}
interface Range extends Base {
type: "Range";
min: number | null;
max: number | null;
}
interface Multiplier extends Base {
type: "Multiplier";
comma: boolean;
min: number;
max: number;
term: Base;
}
type Node =
| Group
| Keyword
| Function
| String
| Property
| Type
| Range
| Multiplier
| Base;
function parse(source: string): Node;
function walk(
node: Node,
options:
| { enter?: (n: Node) => void; leave?: (n: Node) => void }
| ((n: Node) => void),
context?: any
): void;
function generate(
node: Node,
options?: {
forceBraces?: boolean;
compact?: boolean;
decorate?: (content: string, node: Node) => string;
}
): string;
}
// -------------------------------------------------
// Lexer
// -------------------------------------------------
export interface AtruleSyntaxConfig {
prelude?: string | definitionSyntax.Node | ((ref?: any) => any) | null;
descriptors?: Record<
string,
string | definitionSyntax.Node | ((ref?: any) => any)
> | null;
}
export interface LexerConfig {
generic?: boolean;
cssWideKeywords?: string[];
units?: Record<string, string[]>;
types?: Record<
string,
string | definitionSyntax.Node | ((ref?: any) => any)
>;
atrules?: Record<string, AtruleSyntaxConfig>;
properties?: Record<
string,
string | definitionSyntax.Node | ((ref?: any) => any)
>;
}
export interface MatchResult {
matched: any;
error: Error | null;
iterations: number;
isType(node: CssNode | null | undefined, name: string): boolean;
getTrace(
node: CssNode | null | undefined
): Array<{ type: string; name: string }>;
}
export interface FragmentResult {
parent: List<CssNode>;
nodes: List<CssNode>;
}
export class Lexer {
constructor(config?: LexerConfig, syntax?: any, structure?: any);
cssWideKeywords: string[];
units: Record<string, string[]>;
checkStructure(
ast: CssNode
): false | Array<{ node: CssNode; message: string }>;
checkAtruleName(atruleName: string): Error | void;
checkAtrulePrelude(
atruleName: string,
prelude?: string | CssNode | null
): Error | void;
checkAtruleDescriptorName(
atruleName: string,
descriptorName: string
): Error | void;
checkPropertyName(propertyName: string): Error | void;
matchAtrulePrelude(
atruleName: string,
prelude?: string | CssNode | null
): MatchResult;
matchAtruleDescriptor(
atruleName: string,
descriptorName: string,
value: string | CssNode
): MatchResult;
matchDeclaration(node: CssNode): MatchResult;
matchProperty(propertyName: string, value: string | CssNode): MatchResult;
matchType(typeName: string, value: string | CssNode): MatchResult;
match(
syntax: string | definitionSyntax.Node,
value: string | CssNode
): MatchResult;
findValueFragments(
propertyName: string,
value: Value,
type: string,
name: string
): FragmentResult[];
findDeclarationValueFragments(
declaration: Declaration,
type: string,
name: string
): FragmentResult[];
findAllFragments(
ast: CssNode,
type: string,
name: string
): FragmentResult[];
// descriptor getters
getAtrule(atruleName: string, fallbackBasename?: boolean): any | null;
getAtrulePrelude(
atruleName: string,
fallbackBasename?: boolean
): any | null;
getAtruleDescriptor(atruleName: string, name: string): any | null;
getProperty(propertyName: string, fallbackBasename?: boolean): any | null;
getType(name: string): any | null;
validate(): null | {
errors: string[];
types: string[];
properties: string[];
};
dump(syntaxAsAst?: boolean, pretty?: boolean): any;
toString(): string;
}
export const lexer: Lexer;
export function createLexer(config?: LexerConfig): Lexer;
// -------------------------------------------------
// Tokenizer / misc exports (lightly typed)
// -------------------------------------------------
export const tokenTypes: Record<string, number>;
export const tokenNames: string[];
export function tokenize(input: string): IterableIterator<any>;
// Syntax factory and forking
export function createSyntax(config: any): any;
export function fork(ext?: any): any;
}
// Optional subpath module shims to enable tree-shake-style imports
declare module "css-tree/lexer" {
export * from "css-tree";
}
declare module "css-tree/parser" {
export type { ParseOptions, CssNode } from "css-tree";
export { parse as default } from "css-tree";
}
declare module "css-tree/generator" {
export { generate as default } from "css-tree";
}
declare module "css-tree/walker" {
export {
walk,
find,
findAll,
findLast,
WalkOptions,
WalkHandler,
} from "css-tree";
}
declare module "css-tree/definition-syntax" {
export { definitionSyntax } from "css-tree";
}
declare module "css-tree/utils" {
export {
List,
ListItem,
clone,
fromPlainObject,
toPlainObject,
property,
keyword,
ident,
string,
url,
} from "css-tree";
}
@@ -1,4 +1,4 @@
import { generate, walk } from "css-tree";
import { generate, walk, List, type CssNode } from "css-tree";
import type { HtmlTags } from "html-tags";
import {
camelCaseProperty,
@@ -294,13 +294,14 @@ const substituteVars = (
enter(node, item, list) {
if (node.type === "Function" && node.name === "var") {
const varValue = parseCssVar(node);
if (varValue) {
if (varValue && item) {
const newValue = mapper(varValue);
list.replace(
list?.replace(
item,
node.children.createItem({
List.createItem<CssNode>({
type: "Raw",
value: toValue(newValue),
loc: null,
})
);
}
+1 -1
View File
@@ -88,7 +88,7 @@
"change-case": "^5.4.4",
"colord": "^2.9.3",
"cookie": "^1.0.1",
"css-tree": "^2.3.1",
"css-tree": "^3.1.0",
"debug": "^4.3.7",
"downshift": "^6.1.7",
"entrijs": "^0.0.11",
-3
View File
@@ -36,7 +36,6 @@
"@storybook/addon-controls": "^8.6.4",
"@storybook/react": "^8.6.4",
"@storybook/react-vite": "^8.6.4",
"@types/css-tree": "^2.3.1",
"@types/node": "^22.13.10",
"@types/react": "^18.2.70",
"esbuild": "^0.25.3",
@@ -85,8 +84,6 @@
},
"patchedDependencies": {
"@stitches/react@1.3.1-1": "patches/@stitches__react@1.3.1-1.patch",
"css-tree@2.3.1": "patches/css-tree@2.3.1.patch",
"@types/css-tree@2.3.1": "patches/@types__css-tree@2.3.1.patch",
"@radix-ui/react-scroll-area@1.0.5": "patches/@radix-ui__react-scroll-area@1.0.5.patch",
"@remix-run/dev": "patches/@remix-run__dev.patch"
}
+1 -2
View File
@@ -16,7 +16,6 @@
"css-to-ws": "./bin/css-to-ws.ts"
},
"devDependencies": {
"@types/css-tree": "^2.3.1",
"@webstudio-is/tsconfig": "workspace:*",
"html-tags": "^4.0.0",
"mdn-data": "2.23.0",
@@ -36,7 +35,7 @@
"@webstudio-is/css-engine": "workspace:*",
"change-case": "^5.4.4",
"colord": "^2.9.3",
"css-tree": "^2.3.1",
"css-tree": "^3.1.0",
"openai": "^3.2.1",
"p-retry": "^6.2.1",
"warn-once": "^0.1.1"
+4 -1
View File
@@ -219,6 +219,7 @@ export const parseCssVar = (node: FunctionNode): undefined | VarValue => {
const [name, _comma, ...fallback] = node.children;
const fallbackString = generate({
type: "Value",
loc: null,
children: new List<CssNode>().fromArray(fallback),
}).trim();
if (name.type === "Identifier") {
@@ -380,7 +381,7 @@ const parseLiteral = (
type: "function",
args: parseShadow(
node.children.toArray(),
generate({ type: "Value", children: node.children })
generate({ type: "Value", loc: null, children: node.children })
),
name: node.name,
};
@@ -469,6 +470,7 @@ export const parseCssValue = (
value: splitRepeated(nodes).map((nodes) => {
const value = generate({
type: "Value",
loc: null,
children: new List<CssNode>().fromArray(nodes),
});
const parsed = parseCssValue(property, value, false) as LayerValueItem;
@@ -506,6 +508,7 @@ export const parseCssValue = (
}
return generate({
type: "Value",
loc: null,
children: new List<CssNode>().fromArray(nodes),
});
}),
+175 -145
View File
@@ -102,163 +102,193 @@ export const parseCss = (css: string): ParsedStyleDecl[] => {
return [];
}
csstree.walk(ast, function (node) {
// forbid nested at rules
if (node.type === "Atrule" && this.atrule) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore https://github.com/csstree/csstree/blob/v2.3.1/docs/traversal.md
return this.break;
}
if (node.type !== "Declaration" || this.rule?.prelude.type === undefined) {
return;
}
// Track context as we traverse
let currentAtrule: csstree.Atrule | undefined;
let currentRule: csstree.Rule | undefined;
let nestedAtruleDepth = 0;
if (this.atrule && this.atrule.name !== "media") {
return;
}
const supportedMediaFeatures = ["min-width", "max-width"];
const supportedUnits = ["px"];
const supportedMediaFeatures = ["min-width", "max-width"];
const supportedUnits = ["px"];
let breakpoint: undefined | string;
let invalidBreakpoint = false;
if (this.atrule?.prelude?.type === "AtrulePrelude") {
csstree.walk(this.atrule.prelude, {
enter: (node, item, list) => {
if (node.type === "Identifier") {
if (
node.name === "screen" ||
node.name === "all" ||
node.name === "and"
) {
list.remove(item);
csstree.walk(ast, {
enter(node) {
if (node.type === "Atrule") {
if (currentAtrule) {
nestedAtruleDepth++;
}
currentAtrule = node;
} else if (node.type === "Rule") {
currentRule = node;
}
},
leave(node) {
if (node.type === "Atrule") {
if (nestedAtruleDepth > 0) {
nestedAtruleDepth--;
} else {
currentAtrule = undefined;
}
} else if (node.type === "Rule") {
currentRule = undefined;
}
// Process declarations
if (
node.type !== "Declaration" ||
!currentRule ||
currentRule.prelude.type === undefined
) {
return;
}
// forbid nested at rules
if (nestedAtruleDepth > 0) {
return;
}
if (currentAtrule && currentAtrule.name !== "media") {
return;
}
let breakpoint: undefined | string;
let invalidBreakpoint = false;
if (currentAtrule?.prelude?.type === "AtrulePrelude") {
csstree.walk(currentAtrule.prelude, {
enter: (node) => {
if (node.type === "MediaQuery") {
if (node.mediaType === "screen" || node.mediaType === "all") {
node.mediaType = null;
}
// prevent saving print styles
if (node.mediaType === "print") {
invalidBreakpoint = true;
}
}
// prevent saving print styles
if (node.name === "print") {
if (
node.type === "Feature" &&
supportedMediaFeatures.includes(node.name) === false
) {
invalidBreakpoint = true;
}
}
if (
node.type === "MediaFeature" &&
supportedMediaFeatures.includes(node.name) === false
) {
invalidBreakpoint = true;
}
if (
node.type === "Dimension" &&
supportedUnits.includes(node.unit) === false
) {
invalidBreakpoint = true;
}
},
leave: (node) => {
// complex media queries are not supported yet
if (node.type === "MediaQuery" && node.children.size > 1) {
invalidBreakpoint = true;
}
``;
},
});
const generated = csstree.generate(this.atrule.prelude);
if (generated) {
breakpoint = generated;
}
}
if (invalidBreakpoint || this.rule.prelude.type !== "SelectorList") {
return;
}
const selectors: Selector[] = [];
for (const node of this.rule.prelude.children) {
if (node.type !== "Selector") {
continue;
}
let selector: Selector | undefined = undefined;
const children = node.children.toArray();
// @ts-expect-error NestingSelector is not defined in type definitions
const startsWithNesting = children[0]?.type === "NestingSelector";
for (let index = 0; index < children.length; index += 1) {
const childNode = children[index];
let name: string = "";
let state: string | undefined;
switch (childNode.type) {
case "TypeSelector":
name = childNode.name;
break;
case "ClassSelector":
name = `.${childNode.name}`;
break;
case "AttributeSelector":
// for example &[data-state=active]
if (startsWithNesting && index === 1 && children.length === 2) {
state = csstree.generate(childNode);
} else {
name = csstree.generate(childNode);
if (
node.type === "Dimension" &&
supportedUnits.includes(node.unit) === false
) {
invalidBreakpoint = true;
}
break;
case "PseudoClassSelector": {
// First pseudo selector is not a state but an element selector, e.g. :root
if (selector) {
state = `:${childNode.name}`;
} else {
name = `:${childNode.name}`;
},
leave: (node) => {
// complex media queries are not supported yet
if (node.type === "MediaQuery") {
const children = node.condition?.children;
if (children && children.size > 1) {
invalidBreakpoint = true;
}
}
break;
}
case "PseudoElementSelector":
state = `::${childNode.name}`;
break;
case "Combinator":
// " " vs " > "
name =
childNode.name === " " ? childNode.name : ` ${childNode.name} `;
break;
},
});
const generated = csstree.generate(currentAtrule.prelude);
if (generated) {
breakpoint = generated;
}
}
if (invalidBreakpoint || currentRule.prelude.type !== "SelectorList") {
return;
}
const selectors: Selector[] = [];
for (const node of currentRule.prelude.children) {
if (node.type !== "Selector") {
continue;
}
let selector: Selector | undefined = undefined;
const children = node.children.toArray();
const startsWithNesting = children[0]?.type === "NestingSelector";
for (let index = 0; index < children.length; index += 1) {
const childNode = children[index];
let name: string = "";
let state: string | undefined;
switch (childNode.type) {
case "TypeSelector":
name = childNode.name;
break;
case "ClassSelector":
name = `.${childNode.name}`;
break;
case "AttributeSelector":
// for example &[data-state=active]
if (startsWithNesting && index === 1 && children.length === 2) {
state = csstree.generate(childNode);
} else {
name = csstree.generate(childNode);
}
break;
case "PseudoClassSelector": {
// First pseudo selector is not a state but an element selector, e.g. :root
if (selector) {
state = `:${childNode.name}`;
} else {
name = `:${childNode.name}`;
}
break;
}
case "PseudoElementSelector":
state = `::${childNode.name}`;
break;
case "Combinator":
// " " vs " > "
name =
childNode.name === " " ? childNode.name : ` ${childNode.name} `;
break;
}
if (selector) {
selector.name += name;
if (state) {
selector.state = state;
}
} else {
selector = { name, state };
}
}
if (selector) {
selector.name += name;
if (state) {
selector.state = state;
selectors.push(selector);
selector = undefined;
}
}
const stringValue = csstree.generate(node.value);
const parsedCss = parseCssValue(
normalizeProperty(node.property),
stringValue
);
for (const { name: selector, state } of selectors) {
for (const [property, value] of parsedCss) {
const normalizedProperty = normalizeProperty(property);
const styleDecl: ParsedStyleDecl = {
selector,
property: normalizedProperty,
value,
};
if (breakpoint) {
styleDecl.breakpoint = breakpoint;
}
} else {
selector = { name, state };
if (state) {
styleDecl.state = state;
}
// deduplicate styles within selector and state by using map
styles.set(
`${breakpoint}:${selector}:${state}:${normalizedProperty}`,
styleDecl
);
}
}
if (selector) {
selectors.push(selector);
selector = undefined;
}
}
const stringValue = csstree.generate(node.value);
const parsedCss = parseCssValue(
normalizeProperty(node.property),
stringValue
);
for (const { name: selector, state } of selectors) {
for (const [property, value] of parsedCss) {
const normalizedProperty = normalizeProperty(property);
const styleDecl: ParsedStyleDecl = {
selector,
property: normalizedProperty,
value,
};
if (breakpoint) {
styleDecl.breakpoint = breakpoint;
}
if (state) {
styleDecl.state = state;
}
// deduplicate styles within selector and state by using map
styles.set(
`${breakpoint}:${selector}:${state}:${normalizedProperty}`,
styleDecl
);
}
}
},
});
return Array.from(styles.values());
@@ -276,7 +306,7 @@ export const parseMediaQuery = (
let property: undefined | "minWidth" | "maxWidth";
let value: undefined | number;
csstree.walk(ast, (node) => {
if (node.type === "MediaFeature") {
if (node.type === "Feature") {
if (node.name === "min-width") {
property = "minWidth";
}
+49 -26
View File
@@ -13,33 +13,52 @@ const cssWideKeywordsSyntax = Array.from(cssWideKeywords).join(" | ");
const createValueNode = (data?: CssNode[]): Value => ({
type: "Value",
loc: null,
children: new List<CssNode>().fromArray(data ?? []),
});
const createNumber = (value: string): Value => ({
type: "Value",
children: new List<CssNode>().appendData({
const createNumber = (value: string): Value => {
const list = new List<CssNode>();
list.appendData({
type: "Number",
value,
}),
});
loc: null,
});
return {
type: "Value",
loc: null,
children: list,
};
};
const createDimension = (value: string, unit: string): Value => ({
type: "Value",
children: new List<CssNode>().appendData({
const createDimension = (value: string, unit: string): Value => {
const list = new List<CssNode>();
list.appendData({
type: "Dimension",
value,
unit,
}),
});
loc: null,
});
return {
type: "Value",
loc: null,
children: list,
};
};
const createIdentifier = (name: string): Value => ({
type: "Value",
children: new List<CssNode>().appendData({
const createIdentifier = (name: string): Value => {
const list = new List<CssNode>();
list.appendData({
type: "Identifier",
name,
}),
});
loc: null,
});
return {
type: "Value",
loc: null,
children: list,
};
};
const createInitialNode = () => createIdentifier("initial");
@@ -67,7 +86,7 @@ const joinByOperator = (list: List<CssNode> | CssNode[], operator: string) => {
const joined: CssNode[] = [];
for (const node of list) {
if (joined.length > 0) {
joined.push({ type: "Operator", value: operator });
joined.push({ type: "Operator", value: operator, loc: null });
}
joined.push(...getValueList(node));
}
@@ -298,7 +317,7 @@ const expandFont = function* (value: CssNode) {
let fontFamily: CssNode = createInitialNode();
if (config) {
if (
lexer.match("<'font-size'> / <'line-height'> <'font-family'>", config)
lexer.match("<'font-size'> / <'line-height'> <'font-family'>#", config)
.matched
) {
const [fontSizeNode, _slashNode, lineHeightNode, ...fontFamilyNodes] =
@@ -595,8 +614,8 @@ const expandMask = function* (value: CssNode) {
reference ?? createIdentifier("none"),
position ??
createValueNode([
{ type: "Dimension", value: "0", unit: "%" },
{ type: "Dimension", value: "0", unit: "%" },
{ type: "Dimension", value: "0", unit: "%", loc: null },
{ type: "Dimension", value: "0", unit: "%", loc: null },
]),
bgSize ?? createIdentifier("auto"),
repeatStyle ?? createIdentifier("repeat"),
@@ -802,7 +821,9 @@ const expandGrid = function* (value: CssNode) {
if (autoFlowKeyword) {
autoFlow = createIdentifier("row");
if (denseKeyword) {
autoFlow.children.appendList(denseKeyword.children);
denseKeyword.children.forEach((item) => {
autoFlow.children.appendData(item);
});
}
}
autoRows = config ?? createIdentifier("auto");
@@ -819,7 +840,9 @@ const expandGrid = function* (value: CssNode) {
if (autoFlowKeyword) {
autoFlow = createIdentifier("column");
if (denseKeyword) {
autoFlow.children.appendList(denseKeyword.children);
denseKeyword.children.forEach((item) => {
autoFlow.children.appendData(item);
});
}
}
autoColumns = config ?? createIdentifier("auto");
@@ -973,13 +996,13 @@ const expandBackground = function* (value: CssNode) {
image ?? createIdentifier("none"),
position ??
createValueNode([
{ type: "Dimension", value: "0", unit: "%" },
{ type: "Dimension", value: "0", unit: "%" },
{ type: "Dimension", value: "0", unit: "%", loc: null },
{ type: "Dimension", value: "0", unit: "%", loc: null },
]),
size ??
createValueNode([
{ type: "Identifier", name: "auto" },
{ type: "Identifier", name: "auto" },
{ type: "Identifier", name: "auto", loc: null },
{ type: "Identifier", name: "auto", loc: null },
]),
repeatStyle ?? createIdentifier("repeat"),
attachment ?? createIdentifier("scroll"),
@@ -1418,7 +1441,7 @@ const parseValue = function* (property: string, value: string) {
ast.type === "Value" &&
ast.children.isEmpty
) {
ast.children.appendData({ type: "Identifier", name: "unset" });
ast.children.appendData({ type: "Identifier", name: "unset", loc: null });
}
yield [property, ast] as const;
} catch {
+1
View File
@@ -1,5 +1,6 @@
{
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"],
"compilerOptions": {
"isolatedDeclarations": true
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
@@ -2,10 +2,9 @@
"extends": "@webstudio-is/tsconfig/base.json",
"include": [
"src",
"../../@types/**/scroll-timeline.d.ts",
"private-src",
"../sdk/src/schema/animation-schema.ts",
"../../@types/navigator.d.ts"
"../../@types/**/*.d.ts"
],
"exclude": ["private-src/perf/**/*"],
"compilerOptions": {
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"extends": "@webstudio-is/tsconfig/base.json"
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"]
}
+1
View File
@@ -1,5 +1,6 @@
{
"extends": "@webstudio-is/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"],
"compilerOptions": {
"isolatedDeclarations": true
}
-97
View File
@@ -1,97 +0,0 @@
diff --git a/index.d.ts b/index.d.ts
index e6a09d2756a7d942d7dbf2d6b9437deb06e8eb31..46120815179b6176100a045a599b92ecaa6be89c 100755
--- a/index.d.ts
+++ b/index.d.ts
@@ -546,48 +546,7 @@ export interface WalkOptionsVisit<NodeType extends CssNode = CssNode> {
reverse?: boolean | undefined;
}
-export type WalkOptions =
- WalkOptionsVisit<AnPlusB>
- | WalkOptionsVisit<Atrule>
- | WalkOptionsVisit<AtrulePrelude>
- | WalkOptionsVisit<AttributeSelector>
- | WalkOptionsVisit<Block>
- | WalkOptionsVisit<Brackets>
- | WalkOptionsVisit<CDC>
- | WalkOptionsVisit<CDO>
- | WalkOptionsVisit<ClassSelector>
- | WalkOptionsVisit<Combinator>
- | WalkOptionsVisit<Comment>
- | WalkOptionsVisit<Declaration>
- | WalkOptionsVisit<DeclarationList>
- | WalkOptionsVisit<Dimension>
- | WalkOptionsVisit<FunctionNode>
- | WalkOptionsVisit<Hash>
- | WalkOptionsVisit<IdSelector>
- | WalkOptionsVisit<Identifier>
- | WalkOptionsVisit<MediaFeature>
- | WalkOptionsVisit<MediaQuery>
- | WalkOptionsVisit<MediaQueryList>
- | WalkOptionsVisit<Nth>
- | WalkOptionsVisit<NumberNode>
- | WalkOptionsVisit<Operator>
- | WalkOptionsVisit<Parentheses>
- | WalkOptionsVisit<Percentage>
- | WalkOptionsVisit<PseudoClassSelector>
- | WalkOptionsVisit<PseudoElementSelector>
- | WalkOptionsVisit<Ratio>
- | WalkOptionsVisit<Raw>
- | WalkOptionsVisit<Rule>
- | WalkOptionsVisit<Selector>
- | WalkOptionsVisit<SelectorList>
- | WalkOptionsVisit<StringNode>
- | WalkOptionsVisit<StyleSheet>
- | WalkOptionsVisit<TypeSelector>
- | WalkOptionsVisit<UnicodeRange>
- | WalkOptionsVisit<Url>
- | WalkOptionsVisit<Value>
- | WalkOptionsVisit<WhiteSpace>
- | WalkOptionsNoVisit;
+export type WalkOptions = WalkOptionsNoVisit;
export function walk(ast: CssNode, options: EnterOrLeaveFn | WalkOptions): void;
@@ -851,9 +810,30 @@ export class SyntaxReferenceError extends SyntaxError {
reference: string;
}
-export interface LexerMatchResult {
- error: Error | SyntaxMatchError | SyntaxReferenceError | null;
-}
+type Match =
+ | {
+ syntax: DSNodeProperty;
+ match: Match[];
+ }
+ | {
+ syntax: DSNodeType;
+ match: Match[];
+ }
+ | {
+ syntax: DSNodeKeyword;
+ token: string;
+ }
+ | { syntax: null };
+
+export type LexerMatchResult =
+ | {
+ matched: null;
+ error: Error | SyntaxMatchError | SyntaxReferenceError;
+ }
+ | {
+ matched: Match;
+ error: null;
+ };
export class Lexer {
matchAtruleDescriptor(atruleName: string, descriptorName: string, value: CssNode | string): LexerMatchResult;
@@ -864,6 +844,8 @@ export class Lexer {
match(syntax: DSNode | string, value: CssNode | string): LexerMatchResult;
}
+export declare const lexer: Lexer;
+
export function fork(extension: {
atrules?: Record<string, string> | undefined,
properties?: Record<string, string> | undefined,
-18
View File
@@ -1,18 +0,0 @@
diff --git a/package.json b/package.json
index 934e699f19c6f1a265c1ef29121c6dde92cd25ed..a54e28a44de8e1adcf775be9564c2ad8b15b5c98 100644
--- a/package.json
+++ b/package.json
@@ -18,11 +18,11 @@
"validation"
],
"type": "module",
- "module": "./lib/index.js",
+ "module": "./dist/csstree.esm.js",
"main": "./cjs/index.cjs",
"exports": {
".": {
- "import": "./lib/index.js",
+ "import": "./dist/csstree.esm.js",
"require": "./cjs/index.cjs"
},
"./dist/*": "./dist/*.js",
+23 -26
View File
@@ -20,12 +20,6 @@ patchedDependencies:
'@stitches/react@1.3.1-1':
hash: knml42mpr6mlzseo3d6gjljiq4
path: patches/@stitches__react@1.3.1-1.patch
'@types/css-tree@2.3.1':
hash: ac22lciodhaj6xmoeignv36gyq
path: patches/@types__css-tree@2.3.1.patch
css-tree@2.3.1:
hash: epgcmebti7rfrc2ej4odb3t4jy
path: patches/css-tree@2.3.1.patch
importers:
@@ -61,9 +55,6 @@ importers:
'@storybook/react-vite':
specifier: ^8.6.4
version: 8.6.4(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318)(rollup@4.40.0)(storybook@8.6.4(prettier@3.5.3))(typescript@5.8.2)(vite@6.3.4(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3))
'@types/css-tree':
specifier: ^2.3.1
version: 2.3.1(patch_hash=ac22lciodhaj6xmoeignv36gyq)
'@types/node':
specifier: ^22.13.10
version: 22.13.10
@@ -326,8 +317,8 @@ importers:
specifier: ^1.0.1
version: 1.0.1
css-tree:
specifier: ^2.3.1
version: 2.3.1(patch_hash=epgcmebti7rfrc2ej4odb3t4jy)
specifier: ^3.1.0
version: 3.1.0
debug:
specifier: ^4.3.7
version: 4.3.7
@@ -1241,8 +1232,8 @@ importers:
specifier: ^2.9.3
version: 2.9.3
css-tree:
specifier: ^2.3.1
version: 2.3.1(patch_hash=epgcmebti7rfrc2ej4odb3t4jy)
specifier: ^3.1.0
version: 3.1.0
openai:
specifier: ^3.2.1
version: 3.2.1
@@ -1253,9 +1244,6 @@ importers:
specifier: ^0.1.1
version: 0.1.1
devDependencies:
'@types/css-tree':
specifier: ^2.3.1
version: 2.3.1(patch_hash=ac22lciodhaj6xmoeignv36gyq)
'@webstudio-is/tsconfig':
specifier: workspace:*
version: link:../tsconfig
@@ -5375,9 +5363,6 @@ packages:
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/css-tree@2.3.1':
resolution: {integrity: sha512-3m636Jz4d9d+lHVMp6FNLsUWQrfOx1xpm1SBxPbQYSNNgXMe+XswcsDeo1ldyULiuzYyWKk1kmvkLTgNq+215Q==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -6101,6 +6086,10 @@ packages:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-tree@3.1.0:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@@ -7313,6 +7302,9 @@ packages:
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
mdn-data@2.23.0:
resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==}
@@ -12591,7 +12583,7 @@ snapshots:
'@svgo/jsx@0.4.2':
dependencies:
css-tree: 2.3.1(patch_hash=epgcmebti7rfrc2ej4odb3t4jy)
css-tree: 2.3.1
svgo: 3.0.2
'@swc/helpers@0.5.15':
@@ -12678,8 +12670,6 @@ snapshots:
'@types/cookie@0.6.0': {}
'@types/css-tree@2.3.1(patch_hash=ac22lciodhaj6xmoeignv36gyq)': {}
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.31
@@ -13527,13 +13517,18 @@ snapshots:
css-tree@2.2.1:
dependencies:
mdn-data: 2.0.28
source-map-js: 1.2.0
source-map-js: 1.2.1
css-tree@2.3.1(patch_hash=epgcmebti7rfrc2ej4odb3t4jy):
css-tree@2.3.1:
dependencies:
mdn-data: 2.0.30
source-map-js: 1.2.0
css-tree@3.1.0:
dependencies:
mdn-data: 2.12.2
source-map-js: 1.2.1
css-what@6.1.0: {}
cssesc@3.0.0: {}
@@ -14995,6 +14990,8 @@ snapshots:
mdn-data@2.0.30: {}
mdn-data@2.12.2: {}
mdn-data@2.23.0: {}
media-query-parser@2.0.2:
@@ -16644,7 +16641,7 @@ snapshots:
'@trysound/sax': 0.2.0
commander: 7.2.0
css-select: 5.1.0
css-tree: 2.3.1(patch_hash=epgcmebti7rfrc2ej4odb3t4jy)
css-tree: 2.3.1
csso: 5.0.5
picocolors: 1.1.1
@@ -16653,7 +16650,7 @@ snapshots:
'@trysound/sax': 0.2.0
commander: 7.2.0
css-select: 5.1.0
css-tree: 2.3.1(patch_hash=epgcmebti7rfrc2ej4odb3t4jy)
css-tree: 2.3.1
css-what: 6.1.0
csso: 5.0.5
picocolors: 1.1.1