0
# AST Manipulation
1
2
XML Abstract Syntax Tree manipulation utilities for querying, modifying, and traversing SVG document structures with CSS selector support.
3
4
**Note**: The main AST functions (`querySelector`, `querySelectorAll`, `mapNodesToParents`) are exported from the main SVGO entry point. Additional functions like `matches` and `detachNodeFromParent` are available from the internal xast module and are primarily used within custom plugins.
5
6
## Capabilities
7
8
### CSS Selector Queries
9
10
Query SVG elements using CSS selectors.
11
12
```javascript { .api }
13
/**
14
* Query single element using CSS selector
15
* @param node - Parent element to query within
16
* @param selector - CSS selector string
17
* @param parents - Optional parent mapping for context
18
* @returns First matching child element or null
19
*/
20
function querySelector(
21
node: XastParent,
22
selector: string,
23
parents?: Map<XastNode, XastParent>
24
): XastChild | null;
25
26
/**
27
* Query multiple elements using CSS selector
28
* @param node - Parent element to query within
29
* @param selector - CSS selector string
30
* @param parents - Optional parent mapping for context
31
* @returns Array of all matching child elements
32
*/
33
function querySelectorAll(
34
node: XastParent,
35
selector: string,
36
parents?: Map<XastNode, XastParent>
37
): XastChild[];
38
39
/**
40
* Check if element matches CSS selector
41
* @param node - Element to test
42
* @param selector - CSS selector string
43
* @param parents - Optional parent mapping for context
44
* @returns True if element matches selector
45
*/
46
function matches(
47
node: XastElement,
48
selector: string,
49
parents?: Map<XastNode, XastParent>
50
): boolean;
51
```
52
53
**Usage Examples:**
54
55
```javascript
56
import { optimize, querySelector, querySelectorAll, matches } from "svgo";
57
58
// First, get the AST by parsing SVG (typically done inside plugins)
59
// This example shows how you might use these functions in a custom plugin
60
61
const customPlugin = {
62
name: 'examplePlugin',
63
fn: (root) => {
64
return {
65
element: {
66
enter(node, parent) {
67
// Find first rect element
68
const firstRect = querySelector(root, 'rect');
69
if (firstRect) {
70
console.log('Found rect:', firstRect.attributes);
71
}
72
73
// Find all circle elements
74
const circles = querySelectorAll(root, 'circle');
75
console.log('Found circles:', circles.length);
76
77
// Find elements with specific attributes
78
const redElements = querySelectorAll(root, '[fill="red"]');
79
const classElements = querySelectorAll(root, '.my-class');
80
const idElements = querySelectorAll(root, '#my-id');
81
82
// Complex selectors
83
const pathsInGroups = querySelectorAll(root, 'g path');
84
const directChildren = querySelectorAll(root, 'svg > rect');
85
86
// Check if current element matches selector
87
if (matches(node, 'rect[width="100"]')) {
88
console.log('Found 100-width rectangle');
89
}
90
91
// Use parent mapping for context-aware queries
92
const parents = new Map();
93
// ... populate parents map
94
const contextualQuery = querySelector(root, 'use', parents);
95
}
96
}
97
};
98
}
99
};
100
```
101
102
### CSS Selector Support
103
104
Supported CSS selector syntax for SVG element queries.
105
106
**Basic Selectors:**
107
108
```javascript
109
// Element selectors
110
querySelector(root, 'rect') // All <rect> elements
111
querySelector(root, 'g') // All <g> elements
112
querySelector(root, 'path') // All <path> elements
113
114
// ID selectors
115
querySelector(root, '#my-id') // Element with id="my-id"
116
117
// Class selectors
118
querySelector(root, '.my-class') // Elements with class="my-class"
119
120
// Attribute selectors
121
querySelector(root, '[fill]') // Elements with fill attribute
122
querySelector(root, '[fill="red"]') // Elements with fill="red"
123
querySelector(root, '[width="100"]') // Elements with width="100"
124
querySelector(root, '[data-icon]') // Elements with data-icon attribute
125
```
126
127
**Combinators:**
128
129
```javascript
130
// Descendant combinator (space)
131
querySelectorAll(root, 'g rect') // <rect> inside any <g>
132
querySelectorAll(root, 'svg path') // <path> inside any <svg>
133
134
// Child combinator (>)
135
querySelectorAll(root, 'svg > g') // <g> directly inside <svg>
136
querySelectorAll(root, 'g > rect') // <rect> directly inside <g>
137
138
// Multiple selectors (comma)
139
querySelectorAll(root, 'rect, circle') // All <rect> and <circle> elements
140
```
141
142
**Attribute Selector Variants:**
143
144
```javascript
145
// Exact match
146
querySelector(root, '[fill="blue"]') // fill exactly equals "blue"
147
148
// Contains substring
149
querySelector(root, '[class*="icon"]') // class contains "icon"
150
151
// Starts with
152
querySelector(root, '[id^="prefix"]') // id starts with "prefix"
153
154
// Ends with
155
querySelector(root, '[class$="suffix"]') // class ends with "suffix"
156
157
// Space-separated list contains
158
querySelector(root, '[class~="active"]') // class list contains "active"
159
```
160
161
### Node Manipulation
162
163
Utilities for manipulating AST nodes.
164
165
```javascript { .api }
166
/**
167
* Remove node from its parent
168
* @param node - Child node to remove
169
* @param parentNode - Parent node containing the child
170
*/
171
function detachNodeFromParent(node: XastChild, parentNode: XastParent): void;
172
```
173
174
### AST Traversal
175
176
Advanced traversal utilities for walking through AST nodes with visitor patterns.
177
178
```javascript { .api }
179
/**
180
* Traverse AST nodes with visitor pattern
181
* @param node - Starting node for traversal
182
* @param visitor - Visitor object with enter/exit callbacks
183
* @param parentNode - Parent of the starting node
184
*/
185
function visit(node: XastNode, visitor: Visitor, parentNode?: XastParent | null): void;
186
187
/**
188
* Symbol to skip traversing children of current node
189
* Return this from visitor enter callback to skip subtree
190
*/
191
const visitSkip: symbol;
192
193
interface Visitor {
194
doctype?: VisitorNode<XastDoctype>;
195
instruction?: VisitorNode<XastInstruction>;
196
comment?: VisitorNode<XastComment>;
197
cdata?: VisitorNode<XastCdata>;
198
text?: VisitorNode<XastText>;
199
element?: VisitorNode<XastElement>;
200
root?: VisitorRoot;
201
}
202
203
interface VisitorNode<Node> {
204
enter?: (node: Node, parentNode: XastParent) => void | symbol;
205
exit?: (node: Node, parentNode: XastParent) => void;
206
}
207
208
interface VisitorRoot {
209
enter?: (node: XastRoot, parentNode: null) => void;
210
exit?: (node: XastRoot, parentNode: null) => void;
211
}
212
```
213
214
**Usage Examples:**
215
216
```javascript
217
import { detachNodeFromParent } from "svgo";
218
219
// Custom plugin that removes elements
220
const removeElementsPlugin = {
221
name: 'removeElements',
222
fn: (root, params) => {
223
return {
224
element: {
225
enter(node, parent) {
226
// Remove elements matching criteria
227
if (params.selectors && params.selectors.some(sel => matches(node, sel))) {
228
detachNodeFromParent(node, parent);
229
return; // Skip processing children of removed node
230
}
231
232
// Remove elements by tag name
233
if (params.tagNames && params.tagNames.includes(node.name)) {
234
detachNodeFromParent(node, parent);
235
return;
236
}
237
238
// Remove empty text nodes
239
if (node.type === 'text' && !node.value.trim()) {
240
detachNodeFromParent(node, parent);
241
}
242
}
243
}
244
};
245
},
246
params: {
247
selectors: ['.remove-me', '[data-temp]'],
248
tagNames: ['title', 'desc']
249
}
250
};
251
```
252
253
**Traversal Examples:**
254
255
```javascript
256
import { visit, visitSkip } from "svgo";
257
258
// Custom plugin using AST traversal
259
const customTraversalPlugin = {
260
name: 'customTraversal',
261
fn: (root) => {
262
let elementCount = 0;
263
let pathCount = 0;
264
265
// Use visitor pattern to traverse the AST
266
visit(root, {
267
element: {
268
enter(node, parent) {
269
elementCount++;
270
271
if (node.name === 'path') {
272
pathCount++;
273
274
// Example: Skip processing children of path elements
275
if (node.children.length > 0) {
276
console.log('Skipping path children');
277
return visitSkip;
278
}
279
}
280
281
// Example: Remove elements with specific attributes
282
if (node.attributes['data-remove']) {
283
detachNodeFromParent(node, parent);
284
return visitSkip; // Skip children of removed node
285
}
286
},
287
exit(node, parent) {
288
// Called after visiting all children
289
if (node.name === 'g' && node.children.length === 0) {
290
console.log('Found empty group');
291
}
292
}
293
},
294
text: {
295
enter(node, parent) {
296
// Process text nodes
297
if (node.value.trim() === '') {
298
detachNodeFromParent(node, parent);
299
}
300
}
301
},
302
root: {
303
exit() {
304
console.log(`Processed ${elementCount} elements, ${pathCount} paths`);
305
}
306
}
307
});
308
309
// Return null since we handled traversal manually
310
return null;
311
}
312
};
313
314
// Advanced traversal example with conditional processing
315
const conditionalTraversalPlugin = {
316
name: 'conditionalTraversal',
317
fn: (root) => {
318
const processedNodes = new Set();
319
320
visit(root, {
321
element: {
322
enter(node, parent) {
323
// Skip already processed nodes
324
if (processedNodes.has(node)) {
325
return visitSkip;
326
}
327
328
processedNodes.add(node);
329
330
// Example: Only process elements with transforms
331
if (!node.attributes.transform) {
332
return visitSkip;
333
}
334
335
console.log(`Processing transformed ${node.name}:`, node.attributes.transform);
336
337
// Complex conditional logic
338
if (node.name === 'use' && node.attributes.href) {
339
// Skip use elements with external references
340
return visitSkip;
341
}
342
}
343
}
344
});
345
346
return null;
347
}
348
};
349
```
350
351
### Parent Mapping
352
353
Create and use parent mappings for context-aware operations.
354
355
```javascript { .api }
356
/**
357
* Create mapping of nodes to their parent nodes
358
* @param node - Root node to start mapping from
359
* @returns Map of each node to its parent
360
*/
361
function mapNodesToParents(node: XastNode): Map<XastNode, XastParent>;
362
```
363
364
**Usage Examples:**
365
366
```javascript
367
import { mapNodesToParents, querySelector } from "svgo";
368
369
const parentMappingPlugin = {
370
name: 'parentMapping',
371
fn: (root) => {
372
// Create parent mapping for entire tree
373
const parents = mapNodesToParents(root);
374
375
return {
376
element: {
377
enter(node) {
378
// Get parent of current node
379
const parent = parents.get(node);
380
if (parent && parent.type === 'element') {
381
console.log(`${node.name} is inside ${parent.name}`);
382
}
383
384
// Use parent mapping with selectors for better context
385
const contextualQuery = querySelector(node, 'use', parents);
386
387
// Find all ancestors of current node
388
let ancestor = parents.get(node);
389
const ancestorChain = [];
390
while (ancestor && ancestor.type !== 'root') {
391
ancestorChain.push(ancestor);
392
ancestor = parents.get(ancestor);
393
}
394
395
if (ancestorChain.length > 0) {
396
console.log('Ancestor chain:', ancestorChain.map(a => a.name || a.type));
397
}
398
}
399
}
400
};
401
}
402
};
403
```
404
405
### AST Node Types
406
407
Understanding the structure of AST nodes for manipulation.
408
409
```javascript { .api }
410
// Root node (document root)
411
interface XastRoot {
412
type: 'root';
413
children: XastChild[];
414
}
415
416
// Element nodes (SVG tags)
417
interface XastElement {
418
type: 'element';
419
name: string; // Tag name (e.g., 'rect', 'g', 'path')
420
attributes: Record<string, string>; // All attributes as key-value pairs
421
children: XastChild[]; // Child nodes
422
}
423
424
// Text content
425
interface XastText {
426
type: 'text';
427
value: string; // Text content
428
}
429
430
// XML comments
431
interface XastComment {
432
type: 'comment';
433
value: string; // Comment content
434
}
435
436
// CDATA sections
437
interface XastCdata {
438
type: 'cdata';
439
value: string; // CDATA content
440
}
441
442
// Processing instructions
443
interface XastInstruction {
444
type: 'instruction';
445
name: string; // Instruction name
446
value: string; // Instruction value
447
}
448
449
// DOCTYPE declarations
450
interface XastDoctype {
451
type: 'doctype';
452
name: string; // Doctype name
453
data: {
454
doctype: string; // Doctype content
455
};
456
}
457
458
// Union types
459
type XastChild = XastElement | XastText | XastComment | XastCdata | XastInstruction | XastDoctype;
460
type XastParent = XastRoot | XastElement;
461
type XastNode = XastRoot | XastChild;
462
```
463
464
### Advanced AST Manipulation
465
466
Complex node manipulation examples.
467
468
```javascript
469
// Plugin that restructures SVG elements
470
const restructurePlugin = {
471
name: 'restructure',
472
fn: (root) => {
473
const parents = mapNodesToParents(root);
474
475
return {
476
element: {
477
enter(node, parent) {
478
// Move all paths to a dedicated group
479
if (node.name === 'path' && parent.type === 'element' && parent.name !== 'g') {
480
// Create new group if it doesn't exist
481
let pathGroup = querySelector(root, 'g[data-paths="true"]');
482
if (!pathGroup) {
483
pathGroup = {
484
type: 'element',
485
name: 'g',
486
attributes: { 'data-paths': 'true' },
487
children: []
488
};
489
// Add to root SVG
490
const svgElement = querySelector(root, 'svg');
491
if (svgElement) {
492
svgElement.children.push(pathGroup);
493
}
494
}
495
496
// Move path to group
497
detachNodeFromParent(node, parent);
498
pathGroup.children.push(node);
499
}
500
501
// Flatten unnecessary groups
502
if (node.name === 'g' && Object.keys(node.attributes).length === 0) {
503
// Move all children up one level
504
const children = [...node.children];
505
children.forEach(child => {
506
parent.children.push(child);
507
});
508
509
// Remove empty group
510
detachNodeFromParent(node, parent);
511
}
512
}
513
}
514
};
515
}
516
};
517
518
// Plugin that analyzes SVG structure
519
const analyzePlugin = {
520
name: 'analyze',
521
fn: (root) => {
522
const stats = {
523
elements: 0,
524
paths: 0,
525
groups: 0,
526
transforms: 0,
527
ids: new Set(),
528
classes: new Set()
529
};
530
531
return {
532
element: {
533
enter(node) {
534
stats.elements++;
535
536
if (node.name === 'path') stats.paths++;
537
if (node.name === 'g') stats.groups++;
538
if (node.attributes.transform) stats.transforms++;
539
if (node.attributes.id) stats.ids.add(node.attributes.id);
540
if (node.attributes.class) {
541
node.attributes.class.split(/\s+/).forEach(cls => stats.classes.add(cls));
542
}
543
}
544
},
545
root: {
546
exit() {
547
console.log('SVG Analysis:', {
548
...stats,
549
ids: Array.from(stats.ids),
550
classes: Array.from(stats.classes)
551
});
552
}
553
}
554
};
555
}
556
};
557
```