svg-spritemap-webpack-plugin
Version:
Generates symbol-based SVG spritemap from all .svg files in a directory
247 lines (246 loc) • 9.94 kB
JavaScript
import path from 'node:path';
import webpack from 'webpack';
import xmldom from '@xmldom/xmldom';
import { merge } from 'webpack-merge';
import { optimize } from 'svgo';
import { compact, map, sum } from 'lodash-es';
import { svgElementAttributes } from 'svg-element-attributes';
// Helpers
import { addVariablesNamespace, hasVariables } from './variables.js';
// Constants
import { SPRITE_NAME_ATTRIBUTE, SPRITE_LOCATION_ATTRIBUTE, VAR_NAMESPACE, VAR_NAMESPACE_VALUE } from '../constants.js';
export const SVG_PARSER = new xmldom.DOMParser();
export const SVG_SERIALIZER = new xmldom.XMLSerializer();
export const generateSVG = (sources, options, warnings) => {
var _a, _b, _c;
const sizes = {
width: [],
height: [],
gutter: []
};
if (!sources.length) {
return;
}
const document = new xmldom.DOMImplementation().createDocument('http://www.w3.org/2000/svg', '');
const svg = document.createElement('svg');
const items = compact(sources.map((source, index) => {
return {
location: source.location,
id: generateIdentifier(source.location, options, index),
name: generateName(source.location, options, index),
title: generateTitle(source.location),
content: generateSprite(source.content)
};
}));
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
if (options.sprite.generate.use) {
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
}
if (hasVariables(map(items, 'content'))) {
svg.setAttribute(`xmlns:${VAR_NAMESPACE}`, VAR_NAMESPACE_VALUE);
}
Object.entries(options.output.svg.attributes).forEach(([name, value]) => {
svg.setAttribute(name, value);
});
for (const item of items) {
if (!item.content.trim()) {
warnings.push(new webpack.WebpackError(`Sprite '${item.location}' has an empty source file.`));
continue;
}
const documentElement = getDocumentElement(item.content);
if (!documentElement) {
warnings.push(new webpack.WebpackError(`Sprite '${item.location}' does not have a valid document element.`));
continue;
}
const attributes = getDocumentElementAttributes(documentElement, ['viewbox', 'width', 'height', 'id', 'xmlns', SPRITE_LOCATION_ATTRIBUTE]);
for (const [name, value] of Object.entries(attributes)) {
if (!name.toLowerCase().startsWith('xmlns:')) {
continue;
}
svg.setAttribute(name, value);
}
let width = Number.parseFloat((_a = getDocumentElementAttribute(documentElement, 'width')) !== null && _a !== void 0 ? _a : '');
let height = Number.parseFloat((_b = getDocumentElementAttribute(documentElement, 'height')) !== null && _b !== void 0 ? _b : '');
let viewbox = (_c = getDocumentElementAttribute(documentElement, 'viewbox')) === null || _c === void 0 ? void 0 : _c.split(' ').map((value) => {
return Number.parseFloat(value);
});
if ((viewbox === null || viewbox === void 0 ? void 0 : viewbox.length) !== 4 && (Number.isNaN(width) || Number.isNaN(height))) {
warnings.push(new webpack.WebpackError(`Sprite '${item.location}' is invalid, it's lacking both a valid viewbox and width/height attributes.`));
continue;
}
if ((viewbox === null || viewbox === void 0 ? void 0 : viewbox.length) !== 4) {
viewbox = [0, 0, width, height];
}
if (Number.isNaN(width)) {
width = viewbox[2]; // eslint-disable-line unicorn/prefer-at
}
if (Number.isNaN(height)) {
height = viewbox[3]; // eslint-disable-line unicorn/prefer-at
}
const y = sum([...sizes.height, ...sizes.gutter, options.sprite.gutter]);
if (options.sprite.generate.symbol) {
const symbol = generateElement('symbol', document, attributes);
symbol.setAttribute(SPRITE_NAME_ATTRIBUTE, item.name);
symbol.setAttribute(SPRITE_LOCATION_ATTRIBUTE, item.location);
symbol.setAttribute('viewBox', viewbox.join(' '));
symbol.setAttribute('id', [
item.id,
generatePostfix(options.sprite.generate.symbol)
].join(''));
if (options.sprite.generate.dimensions) {
symbol.setAttribute('height', height.toString());
symbol.setAttribute('width', width.toString());
}
if (options.sprite.generate.title) {
const hasTitle = [...documentElement.childNodes].some((node) => {
return node.nodeName === 'title';
});
if (!hasTitle) {
const title = document.createElement('title');
title.appendChild(document.createTextNode(item.title));
symbol.appendChild(title);
}
}
[...documentElement.childNodes].forEach((node) => {
symbol.appendChild(node);
});
svg.appendChild(symbol);
}
if (options.sprite.generate.use) {
const use = generateElement('use', document, attributes);
use.setAttribute('x', '0');
use.setAttribute('y', y.toString());
use.setAttribute('width', width.toString());
use.setAttribute('height', height.toString());
use.setAttribute('xlink:href', [
'#',
item.id,
generatePostfix(options.sprite.generate.symbol)
].join(''));
svg.appendChild(use);
}
if (options.sprite.generate.view) {
const view = generateElement('view', document, attributes);
view.setAttribute('id', [
item.id,
generatePostfix(options.sprite.generate.view)
].join(''));
view.setAttribute('viewBox', [
0,
Math.max(0, y - (options.sprite.gutter / 2)),
width + (options.sprite.gutter / 2),
height + (options.sprite.gutter / 2)
].join(' '));
svg.appendChild(view);
}
sizes.width.push(width);
sizes.height.push(height);
sizes.gutter.push(options.sprite.gutter);
}
if (options.output.svg.sizes) {
svg.setAttribute('width', Math.max(...sizes.width).toString());
svg.setAttribute('height', sum([...sizes.height, ...sizes.gutter]).toString());
}
return SVG_SERIALIZER.serializeToString(svg);
};
export const cleanSVG = (content) => {
return [
SPRITE_NAME_ATTRIBUTE,
SPRITE_LOCATION_ATTRIBUTE
].reduce((content, attribute) => {
return content.replaceAll(new RegExp(`\\s*${attribute}="[^"]*"`, 'g'), '');
}, content);
};
export const optimizeSVG = (content, options) => {
const svg = cleanSVG(content);
if (!options.output.svgo) {
return svg;
}
const configuration = merge({
plugins: [{
name: 'preset-default',
params: {
overrides: {
cleanupIds: false,
removeHiddenElems: false
}
}
}]
}, options.output.svgo === true ? {} : options.output.svgo);
return optimize(svg, configuration).data;
};
const getDocumentElement = (content) => {
var _a;
try {
return (_a = SVG_PARSER.parseFromString(content, 'image/svg+xml').documentElement) !== null && _a !== void 0 ? _a : undefined;
}
catch (_b) {
return;
}
};
const getDocumentElementAttribute = (documentElement, name) => {
var _a;
return (_a = [...documentElement.attributes].find((attribute) => {
return attribute.name.toLowerCase() === name.toLowerCase();
})) === null || _a === void 0 ? void 0 : _a.value;
};
const getDocumentElementAttributes = (documentElement, exclusions = []) => {
return [...documentElement.attributes].reduce((attributes, attribute) => {
if (exclusions.includes(attribute.name.toLowerCase())) {
return attributes;
}
return Object.assign(Object.assign({}, attributes), { [attribute.name]: attribute.value });
}, {});
};
const getValidAttributes = (tagName) => {
return [
...svgElementAttributes['*'],
...svgElementAttributes.svg.filter((attribute) => {
return svgElementAttributes[tagName].includes(attribute);
})
];
};
export const generatePrefix = (location, options) => {
if (typeof options.sprite.prefix === 'function') {
return options.sprite.prefix(location);
}
return options.sprite.prefix;
};
export const generatePostfix = (value) => {
if (typeof value === 'string') {
return value;
}
return '';
};
const generateTitle = (location) => {
return path.basename(location, path.extname(location));
};
const generateName = (location, options, index) => {
const title = generateTitle(location);
if (!options.sprite.idify) {
return title;
}
return options.sprite.idify(title, index);
};
const generateIdentifier = (location, options, index) => {
return compact([
generatePrefix(location, options),
generateName(location, options, index)
]).join('');
};
const generateSprite = (content) => {
if (hasVariables(content)) {
return addVariablesNamespace(content);
}
return content;
};
const generateElement = (tagName, document, attributes) => {
const element = document.createElement(tagName);
for (const [name, value] of Object.entries(attributes)) {
if (!getValidAttributes(tagName).includes(name)) {
continue;
}
element.setAttribute(name, value);
}
return element;
};