Skip to content

Commit 8c0d76b

Browse files
committed
feat: Add AST traversal functionality with visitor pattern
Implements a comprehensive traverse() function that enables walking through the CSS selector AST using the visitor pattern. Supports both simple visitor functions and objects with enter/exit hooks, context information (parent, parents, key, index), and ability to skip subtrees. Added: - src/traverse.ts: Core traversal implementation with visitor pattern - test/traverse.test.ts: 30+ comprehensive test cases - Updated README.md with usage examples and documentation - Exported traverse types from index.ts
1 parent 899dd64 commit 8c0d76b

File tree

5 files changed

+806
-13
lines changed

5 files changed

+806
-13
lines changed

README.md

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A high-performance CSS selector parser with advanced features for modern web dev
1111

1212
- 🚀 **Fast and memory-efficient** parsing for all CSS selectors
1313
- 🌳 **AST-based** object model for programmatic manipulation
14+
- 🚶 **AST traversal** with visitor pattern for analyzing and transforming selectors
1415
- 📊 **Full compliance** with all CSS selector specifications
1516
- 🧪 **Comprehensive test coverage**
1617
- 📚 **Well-documented API** with TypeScript support
@@ -20,18 +21,9 @@ A high-performance CSS selector parser with advanced features for modern web dev
2021

2122
## Playground
2223

23-
Try the interactive playground to explore the parser's capabilities:
24+
**[🎮 Launch Interactive Playground](https://mdevils.github.io/css-selector-parser/)**
2425

25-
**[🎮 Launch Playground](https://mdevils.github.io/css-selector-parser/)**
26-
27-
The playground allows you to:
28-
- ✍️ Write CSS selectors with syntax highlighting
29-
- ⚙️ Configure parser options in real-time
30-
- 🌳 View the parsed AST structure
31-
- 🧪 Test different CSS syntax levels and modules
32-
- ✅ See rendered output for validation
33-
34-
Perfect for learning, debugging, and exploring CSS selector syntax!
26+
Test CSS selectors in your browser with syntax highlighting, real-time AST visualization, and configurable parser options.
3527

3628
## Supported CSS Selector Standards
3729

@@ -173,6 +165,70 @@ const selector = ast.selector({
173165
console.log(render(selector)); // a[href^="/"], .container:has(nav) > a[href]:nth-child(2)::before
174166
```
175167

168+
### Traversing the AST
169+
170+
The `traverse` function allows you to walk through the AST and visit each node, making it easy to analyze or transform selectors.
171+
172+
```javascript
173+
import { createParser, traverse } from 'css-selector-parser';
174+
175+
const parse = createParser();
176+
const selector = parse('div.foo > span#bar:hover::before');
177+
178+
// Simple visitor function - called for each node
179+
traverse(selector, (node, context) => {
180+
console.log(node.type, context.parents.length);
181+
});
182+
183+
// Visitor with enter/exit hooks
184+
traverse(selector, {
185+
enter(node, context) {
186+
console.log('Entering:', node.type);
187+
if (node.type === 'ClassName') {
188+
console.log('Found class:', node.name);
189+
}
190+
},
191+
exit(node, context) {
192+
console.log('Leaving:', node.type);
193+
}
194+
});
195+
196+
// Skip visiting children of specific nodes
197+
traverse(selector, (node) => {
198+
if (node.type === 'PseudoClass') {
199+
// Don't visit children of pseudo-classes
200+
return false;
201+
}
202+
});
203+
204+
// Practical example: collect all class names
205+
const classNames = [];
206+
traverse(selector, (node) => {
207+
if (node.type === 'ClassName') {
208+
classNames.push(node.name);
209+
}
210+
});
211+
console.log(classNames); // ['foo']
212+
213+
// Access parent information
214+
traverse(selector, (node, context) => {
215+
console.log({
216+
type: node.type,
217+
parent: context.parent?.type,
218+
depth: context.parents.length,
219+
key: context.key,
220+
index: context.index
221+
});
222+
});
223+
```
224+
225+
The traversal context provides:
226+
- `node`: The current AST node being visited
227+
- `parent`: The parent node (undefined for root)
228+
- `parents`: Array of all ancestor nodes from root to current
229+
- `key`: Property name in parent that references this node
230+
- `index`: Array index if this node is in an array
231+
176232
## CSS Modules Support
177233

178234
CSS Modules are specifications that add new selectors or modify existing ones. This parser supports various CSS modules that can be included in your syntax definition:
@@ -205,6 +261,7 @@ The `latest` syntax automatically includes all modules marked as current specifi
205261
- [Parsing CSS Selectors](docs/modules.md#createParser)
206262
- [Constructing CSS AST](docs/modules.md#ast)
207263
- [Rendering CSS AST](docs/modules.md#render)
264+
- [Traversing CSS AST](docs/modules.md#traverse)
208265

209266
## Contributing
210267

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ export {
2323
AstWildcardTag
2424
} from './ast.js';
2525
export {CssLevel, CssModule, SyntaxDefinition} from './syntax-definitions.js';
26+
export {traverse, Visitor, VisitorFunction, TraversalContext, TraverseOptions} from './traverse.js';

src/traverse.ts

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import {
2+
AstEntity,
3+
AstSelector,
4+
AstRule,
5+
AstTagName,
6+
AstWildcardTag,
7+
AstAttribute,
8+
AstPseudoClass,
9+
AstPseudoElement,
10+
AstFormulaOfSelector
11+
} from './ast.js';
12+
13+
/**
14+
* Visitor function that is called for each node during traversal.
15+
* Return `false` to skip visiting children of this node.
16+
* Return `true` or `undefined` to continue traversal normally.
17+
*/
18+
export type VisitorFunction = (node: AstEntity, context: TraversalContext) => void | boolean | undefined;
19+
20+
/**
21+
* Visitor object with optional enter and exit hooks.
22+
* - `enter`: Called when first visiting a node (before its children)
23+
* - `exit`: Called when leaving a node (after its children)
24+
*/
25+
export interface Visitor {
26+
/**
27+
* Called when entering a node (before visiting its children).
28+
* Return `false` to skip visiting children of this node.
29+
*/
30+
enter?: VisitorFunction;
31+
/**
32+
* Called when exiting a node (after visiting its children).
33+
*/
34+
exit?: VisitorFunction;
35+
}
36+
37+
/**
38+
* Context information provided to visitor functions during traversal.
39+
*/
40+
export interface TraversalContext {
41+
/** The current node being visited */
42+
node: AstEntity;
43+
/** Parent node (undefined for root) */
44+
parent?: AstEntity;
45+
/** Path of parent nodes from root to current node */
46+
parents: AstEntity[];
47+
/** Property name in parent that references this node */
48+
key?: string;
49+
/** Array index if this node is in an array */
50+
index?: number;
51+
}
52+
53+
/**
54+
* Options for controlling traversal behavior.
55+
*/
56+
export interface TraverseOptions {
57+
/** Custom visitor implementation */
58+
visitor?: Visitor | VisitorFunction;
59+
}
60+
61+
/**
62+
* Internal state for tracking traversal.
63+
*/
64+
interface TraversalState {
65+
visitor: Visitor;
66+
parents: AstEntity[];
67+
}
68+
69+
/**
70+
* Traverses a CSS selector AST, calling visitor functions for each node.
71+
*
72+
* @param node - The root AST node to start traversal from
73+
* @param visitor - Visitor function or object with enter/exit hooks
74+
*
75+
* @example
76+
* ```typescript
77+
* import { createParser, traverse } from 'css-selector-parser';
78+
*
79+
* const parse = createParser();
80+
* const selector = parse('div.foo > span#bar');
81+
*
82+
* // Simple visitor function
83+
* traverse(selector, (node, context) => {
84+
* console.log(node.type, context.parents.length);
85+
* });
86+
*
87+
* // Visitor with enter/exit hooks
88+
* traverse(selector, {
89+
* enter(node, context) {
90+
* if (node.type === 'ClassName') {
91+
* console.log('Found class:', node.name);
92+
* }
93+
* },
94+
* exit(node, context) {
95+
* console.log('Leaving:', node.type);
96+
* }
97+
* });
98+
*
99+
* // Skip subtrees
100+
* traverse(selector, {
101+
* enter(node, context) {
102+
* if (node.type === 'PseudoClass') {
103+
* // Don't visit children of pseudo-classes
104+
* return false;
105+
* }
106+
* }
107+
* });
108+
* ```
109+
*/
110+
export function traverse(node: AstEntity, visitor: Visitor | VisitorFunction): void {
111+
const visitorObj: Visitor = typeof visitor === 'function' ? {enter: visitor} : visitor;
112+
113+
const state: TraversalState = {
114+
visitor: visitorObj,
115+
parents: []
116+
};
117+
118+
visitNode(node, state, undefined, undefined, undefined);
119+
}
120+
121+
/**
122+
* Visits a single node and its children.
123+
*/
124+
function visitNode(
125+
node: AstEntity,
126+
state: TraversalState,
127+
parent: AstEntity | undefined,
128+
key: string | undefined,
129+
index: number | undefined
130+
): void {
131+
const context: TraversalContext = {
132+
node,
133+
parent,
134+
parents: [...state.parents],
135+
key,
136+
index
137+
};
138+
139+
// Call enter hook
140+
let skipChildren = false;
141+
if (state.visitor.enter) {
142+
const result = state.visitor.enter(node, context);
143+
if (result === false) {
144+
skipChildren = true;
145+
}
146+
}
147+
148+
// Visit children unless skipped
149+
if (!skipChildren) {
150+
state.parents.push(node);
151+
visitChildren(node, state);
152+
state.parents.pop();
153+
}
154+
155+
// Call exit hook
156+
if (state.visitor.exit) {
157+
state.visitor.exit(node, context);
158+
}
159+
}
160+
161+
/**
162+
* Visits all children of a node based on its type.
163+
*/
164+
function visitChildren(node: AstEntity, state: TraversalState): void {
165+
switch (node.type) {
166+
case 'Selector':
167+
visitSelector(node, state);
168+
break;
169+
case 'Rule':
170+
visitRule(node, state);
171+
break;
172+
case 'TagName':
173+
visitTagName(node, state);
174+
break;
175+
case 'WildcardTag':
176+
visitWildcardTag(node, state);
177+
break;
178+
case 'Attribute':
179+
visitAttribute(node, state);
180+
break;
181+
case 'PseudoClass':
182+
visitPseudoClass(node, state);
183+
break;
184+
case 'PseudoElement':
185+
visitPseudoElement(node, state);
186+
break;
187+
case 'FormulaOfSelector':
188+
visitFormulaOfSelector(node, state);
189+
break;
190+
// Leaf nodes with no children
191+
case 'Id':
192+
case 'ClassName':
193+
case 'NamespaceName':
194+
case 'WildcardNamespace':
195+
case 'NoNamespace':
196+
case 'NestingSelector':
197+
case 'String':
198+
case 'Formula':
199+
case 'Substitution':
200+
// No children to visit
201+
break;
202+
}
203+
}
204+
205+
function visitSelector(node: AstSelector, state: TraversalState): void {
206+
node.rules.forEach((rule, index) => {
207+
visitNode(rule, state, node, 'rules', index);
208+
});
209+
}
210+
211+
function visitRule(node: AstRule, state: TraversalState): void {
212+
node.items.forEach((item, index) => {
213+
visitNode(item, state, node, 'items', index);
214+
});
215+
216+
if (node.nestedRule) {
217+
visitNode(node.nestedRule, state, node, 'nestedRule', undefined);
218+
}
219+
}
220+
221+
function visitTagName(node: AstTagName, state: TraversalState): void {
222+
if (node.namespace) {
223+
visitNode(node.namespace, state, node, 'namespace', undefined);
224+
}
225+
}
226+
227+
function visitWildcardTag(node: AstWildcardTag, state: TraversalState): void {
228+
if (node.namespace) {
229+
visitNode(node.namespace, state, node, 'namespace', undefined);
230+
}
231+
}
232+
233+
function visitAttribute(node: AstAttribute, state: TraversalState): void {
234+
if (node.namespace) {
235+
visitNode(node.namespace, state, node, 'namespace', undefined);
236+
}
237+
if (node.value) {
238+
visitNode(node.value, state, node, 'value', undefined);
239+
}
240+
}
241+
242+
function visitPseudoClass(node: AstPseudoClass, state: TraversalState): void {
243+
if (node.argument) {
244+
visitNode(node.argument, state, node, 'argument', undefined);
245+
}
246+
}
247+
248+
function visitPseudoElement(node: AstPseudoElement, state: TraversalState): void {
249+
if (node.argument) {
250+
visitNode(node.argument, state, node, 'argument', undefined);
251+
}
252+
}
253+
254+
function visitFormulaOfSelector(node: AstFormulaOfSelector, state: TraversalState): void {
255+
visitNode(node.selector, state, node, 'selector', undefined);
256+
}

test/import.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as Lib from '../src/index.js';
22

33
// eslint-disable-next-line @typescript-eslint/no-var-requires
4-
const {ast, render, createParser} = require(
4+
const {ast, render, createParser, traverse} = require(
55
process.env.TEST_DIST ? `../dist/${process.env.TEST_DIST}/index.js` : '../src/index.js'
66
) as typeof Lib;
77

8-
export {ast, render, createParser};
8+
export {ast, render, createParser, traverse};
9+
export type {AstEntity, AstSelector, TraversalContext} from '../src/index.js';

0 commit comments

Comments
 (0)