Node.js library and command-line application for optimizing SVG vector graphics files
—
Advanced utility functions for security checking, reference detection, and numeric precision handling. These functions are commonly used in custom plugins and advanced SVG processing workflows.
Functions for detecting potentially unsafe SVG content.
/**
* Check if element contains scripts or event handlers
* @param node - Element to check for script content
* @returns True if element contains scripts or event handlers
*/
function hasScripts(node: XastElement): boolean;Usage Examples:
import { hasScripts } from "svgo";
// Custom plugin that avoids processing elements with scripts
const safeOptimizationPlugin = {
name: 'safeOptimization',
fn: (root) => {
return {
element: {
enter(node, parent) {
// Skip optimization for elements with scripts or event handlers
if (hasScripts(node)) {
console.log(`Skipping ${node.name} - contains scripts`);
return visitSkip;
}
// Example: Safe to optimize this element
if (node.name === 'g' && Object.keys(node.attributes).length === 0) {
// Can safely flatten empty groups without scripts
node.children.forEach(child => {
parent.children.push(child);
});
detachNodeFromParent(node, parent);
}
}
}
};
}
};
// Example usage in element analysis
const analyzeScripts = (node) => {
if (hasScripts(node)) {
console.log('Found unsafe element:', {
name: node.name,
hasScriptTag: node.name === 'script',
hasJavascriptLinks: node.name === 'a' &&
Object.entries(node.attributes).some(([key, val]) =>
key.includes('href') && val?.startsWith('javascript:')),
hasEventHandlers: Object.keys(node.attributes).some(attr =>
attr.startsWith('on') || attr.includes('event'))
});
}
};Functions for detecting and extracting SVG element references.
/**
* Check if string contains URL references like url(#id)
* @param body - String content to check
* @returns True if string contains URL references
*/
function includesUrlReference(body: string): boolean;
/**
* Extract reference IDs from attribute values
* @param attribute - Attribute name to check
* @param value - Attribute value to search
* @returns Array of found reference IDs
*/
function findReferences(attribute: string, value: string): string[];Usage Examples:
import { includesUrlReference, findReferences } from "svgo";
// Custom plugin that tracks SVG references
const referenceTrackerPlugin = {
name: 'referenceTracker',
fn: (root) => {
const references = new Set();
const definitions = new Set();
return {
element: {
enter(node) {
// Track defined IDs
if (node.attributes.id) {
definitions.add(node.attributes.id);
}
// Find references in attributes
Object.entries(node.attributes).forEach(([attr, value]) => {
// Quick check for URL references
if (includesUrlReference(value)) {
console.log(`Found URL reference in ${attr}:`, value);
}
// Extract specific reference IDs
const refs = findReferences(attr, value);
refs.forEach(ref => references.add(ref));
});
}
},
root: {
exit() {
const unusedDefs = [...definitions].filter(id => !references.has(id));
const brokenRefs = [...references].filter(id => !definitions.has(id));
console.log('Reference Analysis:', {
definitions: definitions.size,
references: references.size,
unusedDefinitions: unusedDefs,
brokenReferences: brokenRefs
});
}
}
};
}
};
// Example: Check various attribute types
const checkElementReferences = (node) => {
Object.entries(node.attributes).forEach(([attr, value]) => {
const refs = findReferences(attr, value);
if (refs.length > 0) {
console.log(`${attr}="${value}" references:`, refs);
}
// Examples of what gets detected:
// fill="url(#gradient1)" → ['gradient1']
// href="#symbol1" → ['symbol1']
// begin="rect1.click" → ['rect1']
// style="fill: url('#pattern1')" → ['pattern1']
});
};
// Example: Style processing with reference awareness
const processStyles = (styleValue) => {
if (includesUrlReference(styleValue)) {
console.log('Style contains references, preserving:', styleValue);
return styleValue; // Don't modify styles with references
}
// Safe to process style without references
return optimizeStyleValue(styleValue);
};Utility for consistent numeric precision handling.
/**
* Round number to specified precision without string conversion
* @param num - Number to round
* @param precision - Number of decimal places
* @returns Rounded number (not string like Number.prototype.toFixed)
*/
function toFixed(num: number, precision: number): number;Usage Examples:
import { toFixed } from "svgo";
// Custom plugin that normalizes numeric values
const numericNormalizationPlugin = {
name: 'numericNormalization',
fn: (root, params) => {
const precision = params.precision || 3;
return {
element: {
enter(node) {
// Normalize coordinate attributes
['x', 'y', 'width', 'height', 'cx', 'cy', 'r', 'rx', 'ry'].forEach(attr => {
if (node.attributes[attr]) {
const num = parseFloat(node.attributes[attr]);
if (!isNaN(num)) {
node.attributes[attr] = toFixed(num, precision).toString();
}
}
});
// Normalize transform values
if (node.attributes.transform) {
node.attributes.transform = normalizeTransform(
node.attributes.transform,
precision
);
}
// Normalize path data
if (node.name === 'path' && node.attributes.d) {
node.attributes.d = normalizePathData(
node.attributes.d,
precision
);
}
}
}
};
}
};
// Example: Transform matrix normalization
const normalizeTransform = (transform, precision) => {
return transform.replace(/[\d.]+/g, (match) => {
const num = parseFloat(match);
return toFixed(num, precision).toString();
});
};
// Example: Path data normalization
const normalizePathData = (pathData, precision) => {
return pathData.replace(/[\d.-]+/g, (match) => {
const num = parseFloat(match);
return toFixed(num, precision).toString();
});
};
// Example: Consistent numeric operations
const calculateBounds = (elements, precision = 2) => {
let minX = Infinity, minY = Infinity;
let maxX = -Infinity, maxY = -Infinity;
elements.forEach(el => {
const x = parseFloat(el.attributes.x || 0);
const y = parseFloat(el.attributes.y || 0);
const width = parseFloat(el.attributes.width || 0);
const height = parseFloat(el.attributes.height || 0);
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x + width);
maxY = Math.max(maxY, y + height);
});
return {
x: toFixed(minX, precision),
y: toFixed(minY, precision),
width: toFixed(maxX - minX, precision),
height: toFixed(maxY - minY, precision)
};
};
// Example: Comparing with Number.prototype.toFixed
const compareToFixed = (num, precision) => {
const svgoResult = toFixed(num, precision); // Returns number
const nativeResult = Number(num.toFixed(precision)); // String → number
console.log('Input:', num);
console.log('SVGO toFixed:', svgoResult, typeof svgoResult);
console.log('Native toFixed:', nativeResult, typeof nativeResult);
console.log('Equal:', svgoResult === nativeResult);
};
// Usage in plugin parameter processing
const processPluginParams = (params, precision) => {
const processed = {};
Object.entries(params).forEach(([key, value]) => {
if (typeof value === 'number') {
processed[key] = toFixed(value, precision);
} else {
processed[key] = value;
}
});
return processed;
};Examples combining multiple utility functions for complex processing.
// Complex plugin using all utility functions
const comprehensivePlugin = {
name: 'comprehensive',
fn: (root, params) => {
const precision = params.precision || 2;
const preserveReferences = params.preserveReferences !== false;
const skipScripts = params.skipScripts !== false;
const referenceIds = new Set();
const definedIds = new Set();
return {
element: {
enter(node, parent) {
// Security check
if (skipScripts && hasScripts(node)) {
console.log(`Skipping ${node.name} - contains scripts`);
return visitSkip;
}
// Track ID definitions
if (node.attributes.id) {
definedIds.add(node.attributes.id);
}
// Process attributes
Object.entries(node.attributes).forEach(([attr, value]) => {
// Track references if preservation is enabled
if (preserveReferences) {
const refs = findReferences(attr, value);
refs.forEach(ref => referenceIds.add(ref));
if (includesUrlReference(value)) {
return; // Skip processing attributes with references
}
}
// Normalize numeric attributes
if (['x', 'y', 'width', 'height', 'r', 'cx', 'cy'].includes(attr)) {
const num = parseFloat(value);
if (!isNaN(num)) {
node.attributes[attr] = toFixed(num, precision).toString();
}
}
});
}
},
root: {
exit() {
if (preserveReferences) {
const unusedIds = [...definedIds].filter(id => !referenceIds.has(id));
if (unusedIds.length > 0) {
console.log('Unused IDs found:', unusedIds);
}
}
}
}
};
}
};Install with Tessl CLI
npx tessl i tessl/npm-svgo