UNPKG

svg-spritemap-webpack-plugin

Version:

Generates symbol-based SVG spritemap from all .svg files in a directory

247 lines (246 loc) 9.94 kB
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; };