CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-svgo

Node.js library and command-line application for optimizing SVG vector graphics files

Pending
Overview
Eval results
Files

ast-manipulation.mddocs/

AST Manipulation

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.

Capabilities

CSS Selector Queries

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

CSS Selector Support

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 attribute

Combinators:

// 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> elements

Attribute 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"

Node Manipulation

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;

AST Traversal

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

Parent Mapping

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

AST Node Types

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;

Advanced AST Manipulation

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

docs

ast-manipulation.md

cli.md

configuration.md

core-optimization.md

data-uri.md

index.md

plugins.md

utility-functions.md

tile.json