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);
}
}
});
}