Node.js library and command-line application for optimizing SVG vector graphics files
—
XML Abstract Syntax Tree manipulation utilities for querying, modifying, and traversing SVG document structures with CSS selector support.
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.
Query SVG elements using CSS selectors.
/**
* Query single element using CSS selector
* @param node - Parent element to query within
* @param selector - CSS selector string
* @param parents - Optional parent mapping for context
* @returns First matching child element or null
*/
function querySelector(
node: XastParent,
selector: string,
parents?: Map<XastNode, XastParent>
): XastChild | null;
/**
* Query multiple elements using CSS selector
* @param node - Parent element to query within
* @param selector - CSS selector string
* @param parents - Optional parent mapping for context
* @returns Array of all matching child elements
*/
function querySelectorAll(
node: XastParent,
selector: string,
parents?: Map<XastNode, XastParent>
): XastChild[];
/**
* Check if element matches CSS selector
* @param node - Element to test
* @param selector - CSS selector string
* @param parents - Optional parent mapping for context
* @returns True if element matches selector
*/
function matches(
node: XastElement,
selector: string,
parents?: Map<XastNode, XastParent>
): boolean;Usage Examples:
import { optimize, querySelector, querySelectorAll, matches } from "svgo";
// First, get the AST by parsing SVG (typically done inside plugins)
// This example shows how you might use these functions in a custom plugin
const customPlugin = {
name: 'examplePlugin',
fn: (root) => {
return {
element: {
enter(node, parent) {
// Find first rect element
const firstRect = querySelector(root, 'rect');
if (firstRect) {
console.log('Found rect:', firstRect.attributes);
}
// Find all circle elements
const circles = querySelectorAll(root, 'circle');
console.log('Found circles:', circles.length);
// Find elements with specific attributes
const redElements = querySelectorAll(root, '[fill="red"]');
const classElements = querySelectorAll(root, '.my-class');
const idElements = querySelectorAll(root, '#my-id');
// Complex selectors
const pathsInGroups = querySelectorAll(root, 'g path');
const directChildren = querySelectorAll(root, 'svg > rect');
// Check if current element matches selector
if (matches(node, 'rect[width="100"]')) {
console.log('Found 100-width rectangle');
}
// Use parent mapping for context-aware queries
const parents = new Map();
// ... populate parents map
const contextualQuery = querySelector(root, 'use', parents);
}
}
};
}
};Supported CSS selector syntax for SVG element queries.
Basic Selectors:
// Element selectors
querySelector(root, 'rect') // All <rect> elements
querySelector(root, 'g') // All <g> elements
querySelector(root, 'path') // All <path> elements
// ID selectors
querySelector(root, '#my-id') // Element with id="my-id"
// Class selectors
querySelector(root, '.my-class') // Elements with class="my-class"
// Attribute selectors
querySelector(root, '[fill]') // Elements with fill attribute
querySelector(root, '[fill="red"]') // Elements with fill="red"
querySelector(root, '[width="100"]') // Elements with width="100"
querySelector(root, '[data-icon]') // Elements with data-icon attributeCombinators:
// Descendant combinator (space)
querySelectorAll(root, 'g rect') // <rect> inside any <g>
querySelectorAll(root, 'svg path') // <path> inside any <svg>
// Child combinator (>)
querySelectorAll(root, 'svg > g') // <g> directly inside <svg>
querySelectorAll(root, 'g > rect') // <rect> directly inside <g>
// Multiple selectors (comma)
querySelectorAll(root, 'rect, circle') // All <rect> and <circle> elementsAttribute Selector Variants:
// Exact match
querySelector(root, '[fill="blue"]') // fill exactly equals "blue"
// Contains substring
querySelector(root, '[class*="icon"]') // class contains "icon"
// Starts with
querySelector(root, '[id^="prefix"]') // id starts with "prefix"
// Ends with
querySelector(root, '[class$="suffix"]') // class ends with "suffix"
// Space-separated list contains
querySelector(root, '[class~="active"]') // class list contains "active"Utilities for manipulating AST nodes.
/**
* Remove node from its parent
* @param node - Child node to remove
* @param parentNode - Parent node containing the child
*/
function detachNodeFromParent(node: XastChild, parentNode: XastParent): void;Advanced traversal utilities for walking through AST nodes with visitor patterns.
/**
* Traverse AST nodes with visitor pattern
* @param node - Starting node for traversal
* @param visitor - Visitor object with enter/exit callbacks
* @param parentNode - Parent of the starting node
*/
function visit(node: XastNode, visitor: Visitor, parentNode?: XastParent | null): void;
/**
* Symbol to skip traversing children of current node
* Return this from visitor enter callback to skip subtree
*/
const visitSkip: symbol;
interface Visitor {
doctype?: VisitorNode<XastDoctype>;
instruction?: VisitorNode<XastInstruction>;
comment?: VisitorNode<XastComment>;
cdata?: VisitorNode<XastCdata>;
text?: VisitorNode<XastText>;
element?: VisitorNode<XastElement>;
root?: VisitorRoot;
}
interface VisitorNode<Node> {
enter?: (node: Node, parentNode: XastParent) => void | symbol;
exit?: (node: Node, parentNode: XastParent) => void;
}
interface VisitorRoot {
enter?: (node: XastRoot, parentNode: null) => void;
exit?: (node: XastRoot, parentNode: null) => void;
}Usage Examples:
import { detachNodeFromParent } from "svgo";
// Custom plugin that removes elements
const removeElementsPlugin = {
name: 'removeElements',
fn: (root, params) => {
return {
element: {
enter(node, parent) {
// Remove elements matching criteria
if (params.selectors && params.selectors.some(sel => matches(node, sel))) {
detachNodeFromParent(node, parent);
return; // Skip processing children of removed node
}
// Remove elements by tag name
if (params.tagNames && params.tagNames.includes(node.name)) {
detachNodeFromParent(node, parent);
return;
}
// Remove empty text nodes
if (node.type === 'text' && !node.value.trim()) {
detachNodeFromParent(node, parent);
}
}
}
};
},
params: {
selectors: ['.remove-me', '[data-temp]'],
tagNames: ['title', 'desc']
}
};Traversal Examples:
import { visit, visitSkip } from "svgo";
// Custom plugin using AST traversal
const customTraversalPlugin = {
name: 'customTraversal',
fn: (root) => {
let elementCount = 0;
let pathCount = 0;
// Use visitor pattern to traverse the AST
visit(root, {
element: {
enter(node, parent) {
elementCount++;
if (node.name === 'path') {
pathCount++;
// Example: Skip processing children of path elements
if (node.children.length > 0) {
console.log('Skipping path children');
return visitSkip;
}
}
// Example: Remove elements with specific attributes
if (node.attributes['data-remove']) {
detachNodeFromParent(node, parent);
return visitSkip; // Skip children of removed node
}
},
exit(node, parent) {
// Called after visiting all children
if (node.name === 'g' && node.children.length === 0) {
console.log('Found empty group');
}
}
},
text: {
enter(node, parent) {
// Process text nodes
if (node.value.trim() === '') {
detachNodeFromParent(node, parent);
}
}
},
root: {
exit() {
console.log(`Processed ${elementCount} elements, ${pathCount} paths`);
}
}
});
// Return null since we handled traversal manually
return null;
}
};
// Advanced traversal example with conditional processing
const conditionalTraversalPlugin = {
name: 'conditionalTraversal',
fn: (root) => {
const processedNodes = new Set();
visit(root, {
element: {
enter(node, parent) {
// Skip already processed nodes
if (processedNodes.has(node)) {
return visitSkip;
}
processedNodes.add(node);
// Example: Only process elements with transforms
if (!node.attributes.transform) {
return visitSkip;
}
console.log(`Processing transformed ${node.name}:`, node.attributes.transform);
// Complex conditional logic
if (node.name === 'use' && node.attributes.href) {
// Skip use elements with external references
return visitSkip;
}
}
}
});
return null;
}
};Create and use parent mappings for context-aware operations.
/**
* Create mapping of nodes to their parent nodes
* @param node - Root node to start mapping from
* @returns Map of each node to its parent
*/
function mapNodesToParents(node: XastNode): Map<XastNode, XastParent>;Usage Examples:
import { mapNodesToParents, querySelector } from "svgo";
const parentMappingPlugin = {
name: 'parentMapping',
fn: (root) => {
// Create parent mapping for entire tree
const parents = mapNodesToParents(root);
return {
element: {
enter(node) {
// Get parent of current node
const parent = parents.get(node);
if (parent && parent.type === 'element') {
console.log(`${node.name} is inside ${parent.name}`);
}
// Use parent mapping with selectors for better context
const contextualQuery = querySelector(node, 'use', parents);
// Find all ancestors of current node
let ancestor = parents.get(node);
const ancestorChain = [];
while (ancestor && ancestor.type !== 'root') {
ancestorChain.push(ancestor);
ancestor = parents.get(ancestor);
}
if (ancestorChain.length > 0) {
console.log('Ancestor chain:', ancestorChain.map(a => a.name || a.type));
}
}
}
};
}
};Understanding the structure of AST nodes for manipulation.
// Root node (document root)
interface XastRoot {
type: 'root';
children: XastChild[];
}
// Element nodes (SVG tags)
interface XastElement {
type: 'element';
name: string; // Tag name (e.g., 'rect', 'g', 'path')
attributes: Record<string, string>; // All attributes as key-value pairs
children: XastChild[]; // Child nodes
}
// Text content
interface XastText {
type: 'text';
value: string; // Text content
}
// XML comments
interface XastComment {
type: 'comment';
value: string; // Comment content
}
// CDATA sections
interface XastCdata {
type: 'cdata';
value: string; // CDATA content
}
// Processing instructions
interface XastInstruction {
type: 'instruction';
name: string; // Instruction name
value: string; // Instruction value
}
// DOCTYPE declarations
interface XastDoctype {
type: 'doctype';
name: string; // Doctype name
data: {
doctype: string; // Doctype content
};
}
// Union types
type XastChild = XastElement | XastText | XastComment | XastCdata | XastInstruction | XastDoctype;
type XastParent = XastRoot | XastElement;
type XastNode = XastRoot | XastChild;Complex node manipulation examples.
// Plugin that restructures SVG elements
const restructurePlugin = {
name: 'restructure',
fn: (root) => {
const parents = mapNodesToParents(root);
return {
element: {
enter(node, parent) {
// Move all paths to a dedicated group
if (node.name === 'path' && parent.type === 'element' && parent.name !== 'g') {
// Create new group if it doesn't exist
let pathGroup = querySelector(root, 'g[data-paths="true"]');
if (!pathGroup) {
pathGroup = {
type: 'element',
name: 'g',
attributes: { 'data-paths': 'true' },
children: []
};
// Add to root SVG
const svgElement = querySelector(root, 'svg');
if (svgElement) {
svgElement.children.push(pathGroup);
}
}
// Move path to group
detachNodeFromParent(node, parent);
pathGroup.children.push(node);
}
// Flatten unnecessary groups
if (node.name === 'g' && Object.keys(node.attributes).length === 0) {
// Move all children up one level
const children = [...node.children];
children.forEach(child => {
parent.children.push(child);
});
// Remove empty group
detachNodeFromParent(node, parent);
}
}
}
};
}
};
// Plugin that analyzes SVG structure
const analyzePlugin = {
name: 'analyze',
fn: (root) => {
const stats = {
elements: 0,
paths: 0,
groups: 0,
transforms: 0,
ids: new Set(),
classes: new Set()
};
return {
element: {
enter(node) {
stats.elements++;
if (node.name === 'path') stats.paths++;
if (node.name === 'g') stats.groups++;
if (node.attributes.transform) stats.transforms++;
if (node.attributes.id) stats.ids.add(node.attributes.id);
if (node.attributes.class) {
node.attributes.class.split(/\s+/).forEach(cls => stats.classes.add(cls));
}
}
},
root: {
exit() {
console.log('SVG Analysis:', {
...stats,
ids: Array.from(stats.ids),
classes: Array.from(stats.classes)
});
}
}
};
}
};Install with Tessl CLI
npx tessl i tessl/npm-svgo