A tool set for CSS: fast detailed parser (CSS → AST), walker (AST traversal), generator (AST → CSS) and lexer (validation and matching) based on specs and browser implementations
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Comprehensive AST traversal utilities for walking, searching, and manipulating CSS Abstract Syntax Trees.
Traverses an AST using the visitor pattern, calling visitor functions for each node.
/**
* Traverses AST nodes using visitor pattern
* @param ast - AST node to traverse
* @param visitor - Visitor function or visitor object with enter/leave methods
*/
function walk(ast: CssNode, visitor: WalkVisitor | VisitorObject): void;
type WalkVisitor = (node: CssNode, item: ListItem<CssNode>, list: List<CssNode>) => void;
interface VisitorObject {
/** Called when entering a node */
enter?: (node: CssNode, item: ListItem<CssNode>, list: List<CssNode>) => void;
/** Called when leaving a node */
leave?: (node: CssNode, item: ListItem<CssNode>, list: List<CssNode>) => void;
/** Node-type specific visitors */
[nodeType: string]: WalkVisitor;
}
interface ListItem<T> {
prev: ListItem<T> | null;
next: ListItem<T> | null;
data: T;
}Usage Examples:
import { parse, walk } from 'css-tree';
const ast = parse('.example { color: red; margin: 10px; }');
// Simple visitor function
walk(ast, (node) => {
console.log(`Node type: ${node.type}`);
});
// Visitor with enter/leave
walk(ast, {
enter: (node) => {
console.log(`Entering: ${node.type}`);
},
leave: (node) => {
console.log(`Leaving: ${node.type}`);
}
});
// Node-type specific visitors
walk(ast, {
Declaration: (node) => {
console.log(`Property: ${node.property.name}`);
},
ClassSelector: (node) => {
console.log(`Class: ${node.name}`);
}
});Search functions for locating specific nodes within an AST.
/**
* Finds the first node matching the predicate
* @param ast - AST node to search
* @param predicate - Function that returns true for matching nodes
* @returns First matching node or null
*/
function find(ast: CssNode, predicate: (node: CssNode) => boolean): CssNode | null;
/**
* Finds the last node matching the predicate
* @param ast - AST node to search
* @param predicate - Function that returns true for matching nodes
* @returns Last matching node or null
*/
function findLast(ast: CssNode, predicate: (node: CssNode) => boolean): CssNode | null;
/**
* Finds all nodes matching the predicate
* @param ast - AST node to search
* @param predicate - Function that returns true for matching nodes
* @returns Array of all matching nodes
*/
function findAll(ast: CssNode, predicate: (node: CssNode) => boolean): CssNode[];Usage Examples:
import { parse, find, findLast, findAll } from 'css-tree';
const ast = parse(`
.example { color: red; }
.another { color: blue; margin: 10px; }
`);
// Find first color declaration
const firstColor = find(ast, (node) =>
node.type === 'Declaration' && node.property.name === 'color'
);
// Find last declaration
const lastDeclaration = findLast(ast, (node) =>
node.type === 'Declaration'
);
// Find all class selectors
const classSelectors = findAll(ast, (node) =>
node.type === 'ClassSelector'
);
// Find declarations with specific values
const redDeclarations = findAll(ast, (node) =>
node.type === 'Declaration' &&
find(node.value, (valueNode) =>
valueNode.type === 'Identifier' && valueNode.name === 'red'
)
);Complex traversal patterns for AST manipulation and analysis:
// Modify AST during traversal
walk(ast, (node, item, list) => {
if (node.type === 'Declaration' && node.property.name === 'color') {
// Replace color value
walk(node.value, (valueNode, valueItem, valueList) => {
if (valueNode.type === 'Identifier' && valueNode.name === 'red') {
valueList.replace(valueItem, {
type: 'Identifier',
name: 'blue'
});
}
});
}
});
// Collect statistics
const stats = {
rules: 0,
declarations: 0,
selectors: 0,
functions: 0
};
walk(ast, {
Rule: () => stats.rules++,
Declaration: () => stats.declarations++,
Selector: () => stats.selectors++,
Function: () => stats.functions++
});
// Build selector specificity map
const specificityMap = new Map();
walk(ast, {
Rule: (node) => {
const selector = generate(node.prelude);
const declarations = [];
walk(node.block, {
Declaration: (declNode) => {
declarations.push({
property: declNode.property.name,
value: generate(declNode.value)
});
}
});
specificityMap.set(selector, declarations);
}
});Safe AST modification patterns using List methods:
interface List<T> {
/** Replace an item with new item or list */
replace(item: ListItem<T>, newItemOrList: T | List<T>): void;
/** Remove an item from the list */
remove(item: ListItem<T>): void;
/** Insert item before specified item */
insert(item: ListItem<T>, beforeItem: ListItem<T>): void;
/** Append item to end of list */
append(item: T): ListItem<T>;
/** Prepend item to beginning of list */
prepend(item: T): ListItem<T>;
}Tree Manipulation Examples:
// Remove declarations
walk(ast, (node, item, list) => {
if (node.type === 'Declaration' && node.property.name === 'margin') {
list.remove(item);
}
});
// Add declarations
walk(ast, {
Rule: (node) => {
walk(node.block, {
DeclarationList: (declList) => {
// Add new declaration
declList.children.append({
type: 'Declaration',
important: false,
property: { type: 'Identifier', name: 'display' },
value: { type: 'Identifier', name: 'block' }
});
}
});
}
});
// Replace selectors
walk(ast, (node, item, list) => {
if (node.type === 'ClassSelector' && node.name === 'old-class') {
list.replace(item, {
type: 'ClassSelector',
name: 'new-class'
});
}
});Track traversal context for complex transformations:
function walkWithContext(ast, visitors) {
const context = {
rule: null,
atrule: null,
declaration: null,
depth: 0
};
walk(ast, {
enter: (node) => {
context.depth++;
if (node.type === 'Rule') {
context.rule = node;
} else if (node.type === 'Atrule') {
context.atrule = node;
} else if (node.type === 'Declaration') {
context.declaration = node;
}
if (visitors[node.type]) {
visitors[node.type](node, context);
}
},
leave: (node) => {
context.depth--;
if (node.type === 'Rule') {
context.rule = null;
} else if (node.type === 'Atrule') {
context.atrule = null;
} else if (node.type === 'Declaration') {
context.declaration = null;
}
}
});
}
// Usage
walkWithContext(ast, {
Identifier: (node, context) => {
if (context.declaration && context.declaration.property.name === 'color') {
console.log(`Color value: ${node.name} in rule: ${generate(context.rule.prelude)}`);
}
}
});Optimize traversal for large ASTs:
// Early termination
function findFirstMatch(ast, predicate) {
let result = null;
walk(ast, (node) => {
if (result === null && predicate(node)) {
result = node;
return false; // Stop traversal
}
});
return result;
}
// Selective traversal
function walkDeclarationsOnly(ast, visitor) {
walk(ast, {
Declaration: visitor,
// Skip unnecessary node types for performance
Comment: () => {},
WhiteSpace: () => {}
});
}
// Parallel processing for independent operations
function processRulesInParallel(ast, processor) {
const rules = findAll(ast, (node) => node.type === 'Rule');
return Promise.all(rules.map(processor));
}Helper functions for common AST transformations:
// Clone subtree
function cloneSubtree(node) {
const cloned = { ...node };
if (node.children) {
cloned.children = new List();
node.children.forEach((child) => {
cloned.children.append(cloneSubtree(child));
});
}
return cloned;
}
// Merge rules with same selector
function mergeIdenticalRules(ast) {
const ruleMap = new Map();
walk(ast, (node, item, list) => {
if (node.type === 'Rule') {
const selector = generate(node.prelude);
if (ruleMap.has(selector)) {
// Merge declarations
const existingRule = ruleMap.get(selector);
walk(node.block, {
Declaration: (decl) => {
existingRule.block.children.append(cloneSubtree(decl));
}
});
// Remove duplicate rule
list.remove(item);
} else {
ruleMap.set(selector, node);
}
}
});
}Install with Tessl CLI
npx tessl i tessl/npm-css-tree