UNPKG

@svgd/core

Version:

An SVG optimization tool that converts SVG files into a single path 'd' attribute string for efficient storage and rendering.

608 lines (598 loc) 16.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { defaultConfig: () => defaultConfig, getPaths: () => getPaths, getSvg: () => getSvg, getSvgoConfig: () => getSvgoConfig }); module.exports = __toCommonJS(index_exports); // src/commands.ts var commands = [ { code: "o", attribute: "opacity", regexp: "[\\d.]+", toAttribute: (codeValue) => codeValue, toCommand: (attributeValue) => attributeValue }, { code: "of", attribute: "fill-opacity", regexp: "[\\d.]+", toAttribute: (codeValue) => codeValue, toCommand: (attributeValue) => attributeValue }, { code: "os", attribute: "stroke-opacity", regexp: "[\\d.]+", toAttribute: (codeValue) => codeValue, toCommand: (attributeValue) => attributeValue }, { code: "f", attribute: "stroke", regexp: "[#0-9a-zA-Z]+", toAttribute: (codeValue) => { switch (codeValue) { case "c": return "currentColor"; case "n": return "none"; default: return codeValue; } }, toCommand: (attributeValue) => { switch (attributeValue) { case "currentColor": return "c"; case "none": return "n"; default: return attributeValue; } } }, { code: "F", attribute: "fill", regexp: "[#0-9a-zA-Z]+", toAttribute: (codeValue) => { switch (codeValue) { case "c": return "currentColor"; case "n": return "none"; default: return codeValue; } }, toCommand: (attributeValue) => { switch (attributeValue) { case "currentColor": return null; case "none": return "n"; default: return attributeValue; } } }, { code: "w", attribute: "stroke-width", regexp: "[\\d.]+", toAttribute: (codeValue) => codeValue, toCommand: (attributeValue) => attributeValue }, { code: "e", attribute: "fill-rule", regexp: "", toAttribute: () => "evenodd", toCommand: (attributeValue) => attributeValue === "evenodd" ? "" : null } ]; // src/getPaths.ts function getPaths(d) { const paths = []; let attributes = {}; const pathCommands = d.split(new RegExp( `(${commands.map((cmd) => `${cmd.code}${cmd.regexp}`).join("|")})` )); pathCommands.forEach((text, i) => { const isCommand = i % 2 === 1; if (isCommand) { commands.forEach(({ code, attribute, regexp, toAttribute }) => { const match = text.match(new RegExp(`^${code}(${regexp})$`)); if (match) { attributes[attribute] = toAttribute(match[1]); } }); return; } const d2 = text.trim(); if (d2) { paths.push({ ...attributes, d: d2 }); attributes = {}; } }); return paths; } // src/getSvg.ts function getSvg(d, viewbox) { const svgParts = getPaths(d).map((attributes) => `<path ${attributes ? Object.entries(attributes).map(([k, v]) => `${k}="${v}"`).join(" ") : ""} />`); const { minX = 0, minY = 0, width = 24, height = 24 } = viewbox ?? {}; const content = svgParts.length ? ` ${svgParts.join(` `)} ` : ""; return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${minX} ${minY} ${width} ${height}" width="${width}" height="${height}">${content}</svg>`; } // src/convertRoundedRectToPath.ts var convertRoundedRectToPath = { name: "convertRoundedRectToPath", description: "Convert only rounded <rect> elements to <path>.", fn: () => { return { element: { enter: (node) => { if (node.name !== "rect" || node.attributes == null) { return; } const attrs = node.attributes; const x = toNumber(attrs.x, 0); const y = toNumber(attrs.y, 0); const width = toNumber(attrs.width, null); const height = toNumber(attrs.height, null); if (width == null || height == null || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { return; } const hasRx = attrs.rx != null; const hasRy = attrs.ry != null; if (!hasRx && !hasRy) { return; } let rx = hasRx ? toNumber(attrs.rx, 0) : null; let ry = hasRy ? toNumber(attrs.ry, 0) : null; if (rx != null && ry == null) { ry = rx; } else if (rx == null && ry != null) { rx = ry; } rx = clamp(rx ?? 0, 0, width / 2); ry = clamp(ry ?? 0, 0, height / 2); if (rx <= 0 && ry <= 0) { return; } const d = buildRoundedRectPath(x, y, width, height, rx, ry); node.name = "path"; node.attributes = { ...attrs, d }; delete node.attributes.x; delete node.attributes.y; delete node.attributes.width; delete node.attributes.height; delete node.attributes.rx; delete node.attributes.ry; } } }; } }; function toNumber(value, fallback) { if (value == null) { return fallback; } const num = Number.parseFloat(String(value).trim()); return Number.isFinite(num) ? num : fallback; } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function buildRoundedRectPath(x, y, width, height, rx, ry) { const x2 = x + width; const y2 = y + height; return [ "M", fmt(x + rx), fmt(y), "H", fmt(x2 - rx), "A", fmt(rx), fmt(ry), "0", "0", "1", fmt(x2), fmt(y + ry), "V", fmt(y2 - ry), "A", fmt(rx), fmt(ry), "0", "0", "1", fmt(x2 - rx), fmt(y2), "H", fmt(x + rx), "A", fmt(rx), fmt(ry), "0", "0", "1", fmt(x), fmt(y2 - ry), "V", fmt(y + ry), "A", fmt(rx), fmt(ry), "0", "0", "1", fmt(x + rx), fmt(y), "Z" ].join(" "); } function fmt(value) { return Number.parseFloat(value.toFixed(6)).toString(); } // src/defaultConfig.ts var defaultConfig = { resize: { targetViewBox: { minX: 0, minY: 0, width: 24, height: 24 } }, colors: false, svgo: { plugins: [ { name: "removeAttrs", params: { attrs: [ "overflow", "filter" ] } }, { name: "preset-default", params: { overrides: { convertShapeToPath: false, convertColors: false, mergePaths: false, moveElemsAttrsToGroup: false, moveGroupAttrsToElems: false } } }, { name: "inlineStyles", params: { onlyMatchedOnce: false } }, { name: "convertStyleToAttrs" }, { name: "removeUselessStrokeAndFill", params: { stroke: true, fill: true, removeNone: true } }, { name: "convertColors", params: { currentColor: false, names2hex: true, rgb2hex: true, shorthex: true, shortname: false } }, { name: "convertShapeToPath", params: { convertArcs: true } }, convertRoundedRectToPath, { name: "mergePaths", params: { force: true } }, { name: "moveGroupAttrsToElems" }, { name: "collapseGroups" }, { name: "convertPathData" }, { name: "removeHiddenElems" }, { name: "removeUselessDefs" } ] } }; // src/resizePlugin.ts function resizePlugin(params) { return { name: "resizePlugin", fn: (ast) => { const svgNode = getSvgNode(ast); if (!svgNode) return null; const originalDims = getOriginalDimensions(svgNode); const transform = computeTransformations(originalDims, params); wrapChildrenInGroup(svgNode, transform); overrideSvgAttributesIfNeeded(svgNode, params); return null; } }; } function getSvgNode(ast) { return ast.children.find( (node) => node.type === "element" && node.name === "svg" ); } function getOriginalDimensions(svgNode) { const viewBox = svgNode.attributes.viewBox; if (viewBox) { const [minX, minY, width, height] = viewBox.split(/[\s,]+/).map(parseFloat); return { minX, minY, width, height }; } return { minX: 0, minY: 0, width: parseFloat(svgNode.attributes.width ?? "100"), height: parseFloat(svgNode.attributes.height ?? "100") }; } function computeTransformations(originalDims, params) { const { targetViewBox, preserveAspectRatio = true } = params; const { minX: origMinX, minY: origMinY, width: origWidth, height: origHeight } = originalDims; const { minX, minY, width, height } = targetViewBox; const scaleX = width / origWidth; const scaleY = height / origHeight; const scale = preserveAspectRatio ? Math.min(scaleX, scaleY) : NaN; const translateX = minX - origMinX * (preserveAspectRatio ? scale : scaleX) + (preserveAspectRatio ? (width - origWidth * scale) / 2 : 0); const translateY = minY - origMinY * (preserveAspectRatio ? scale : scaleY) + (preserveAspectRatio ? (height - origHeight * scale) / 2 : 0); if (preserveAspectRatio) { return `translate(${translateX}, ${translateY}) scale(${scale}, ${scale})`; } return `translate(${translateX}, ${translateY}) scale(${scaleX}, ${scaleY})`; } function wrapChildrenInGroup(svgNode, transform) { const groupNode = { type: "element", name: "g", attributes: { transform }, children: [] }; groupNode.children = svgNode.children.splice(0, svgNode.children.length); svgNode.children.push(groupNode); } function overrideSvgAttributesIfNeeded(svgNode, params) { const { overrideSvgAttributes = true, targetViewBox } = params; if (!overrideSvgAttributes) return; const { minX, minY, width, height } = targetViewBox; svgNode.attributes.viewBox = `${minX} ${minY} ${width} ${height}`; delete svgNode.attributes.width; delete svgNode.attributes.height; } // src/inlineUsePlugin.ts var inlineUsePlugin = { name: "inlineUse", fn: () => { const defsMap = /* @__PURE__ */ new Map(); function collectDefs(node) { if (node.type === "element" || node.type === "root") { if (node.type === "element" && node.name === "defs" && Array.isArray(node.children)) { node.children = node.children.filter((defEl) => { if (defEl.type !== "element" || defEl.name !== "path") { return true; } const { id, ...attributes } = defEl?.attributes ?? {}; if (id) { defsMap.set(id, { ...defEl, attributes }); return false; } return true; }); } else if (Array.isArray(node.children)) { for (const child of node.children) { collectDefs(child); } } } } return { root: { enter(rootNode) { collectDefs(rootNode); } }, element: { enter(node, parentNode) { if (node.name !== "use") return; const href = node.attributes.href || node.attributes["xlink:href"]; if (!href || !href.startsWith("#")) return; const id = href.slice(1); const defEl = defsMap.get(id); if (!defEl) return; const clone = { name: defEl.name, type: defEl.type, attributes: { ...defEl.attributes, ...node.attributes }, children: defEl.children }; const idx = parentNode.children.indexOf(node); if (idx >= 0) { parentNode.children.splice(idx, 1, clone); } } } }; } }; // src/moveGroupOpacityToElementsPlugin.ts var opacityAttibutes = ["opacity", "fill-opacity", "stroke-opacity"]; var moveGroupOpacityToElementsPlugin = { name: "inlineUse", fn: () => { return { element: { enter: (node) => { if (node.name === "g" && node.children.length !== 0) { const mergers = opacityAttibutes.map((opacityAttibute) => getMergeOpacity(node, opacityAttibute)).filter(Boolean); for (const child of node.children) { if (child.type === "element") { mergers.forEach((merge) => merge(child)); } } opacityAttibutes.forEach((opacityAttibute) => { delete node.attributes[opacityAttibute]; }); } } } }; } }; function getMergeOpacity(parent, attributeName) { if (!(attributeName in parent.attributes)) return null; const parentValue = parent.attributes[attributeName]; const parsedParentValue = Number.parseFloat(parentValue); return (node) => { if (node.type === "element") { const value = node.attributes[attributeName]; node.attributes[attributeName] = value !== null && value !== void 0 ? String(Number.parseFloat(node.attributes[attributeName]) * parsedParentValue) : parentValue; } }; } // src/getSvgoConfig.ts var getSvgoConfig = (config = defaultConfig) => { const plugins = config.svgo.plugins ?? []; const pluginsByColor = config.colors ? plugins : plugins.map((plugin) => typeof plugin === "object" && plugin.name === "convertColors" ? { ...plugin, params: { currentColor: true } } : plugin); return { ...config.svgo, plugins: [ inlineUsePlugin, moveGroupOpacityToElementsPlugin, resizePlugin(config.resize), ...pluginsByColor, extractPathDPlugin() ] }; }; var extractPathDPlugin = () => ({ name: "extractPathD", fn: (ast) => { const collectPathsContext = { paths: [], wasCommand: false }; collectPaths(ast, collectPathsContext); ast.children = [{ type: "text", value: collectPathsContext.paths.join(" ") }]; return null; } }); var pickCollectableAttributes = (attributes) => { const pickedAttributes = {}; commands.forEach(({ attribute }) => { if (attributes[attribute] !== void 0) { pickedAttributes[attribute] = attributes[attribute]; } }); return pickedAttributes; }; var collectPaths = (node, context, inheritedAttributes = {}) => { if (node.type === "element" && !["path", "g", "svg", "title"].includes(node.name)) { throw new Error(`[SVGD ERROR] svg has other tag "${node.name}"`); } if (node.type === "element" && node.name === "path" && node.attributes.d) { const effectiveAttributes = { ...inheritedAttributes, ...node.attributes }; const d = node.attributes.d; const commandsArray = []; commands.forEach(({ code, toCommand, attribute }) => { if (attribute in effectiveAttributes) { const commandValue = toCommand(effectiveAttributes[attribute]); if (commandValue !== null) { commandsArray.push(`${code}${commandValue}`); } } }); if (commandsArray.length) { context.wasCommand = true; context.paths.push(...commandsArray); } else if (context.wasCommand) { context.paths.push("o1"); } context.paths.push(d); } const childrenInheritedAttributes = node.type === "element" && ["g", "svg"].includes(node.name) ? { ...inheritedAttributes, ...pickCollectableAttributes(node.attributes) } : inheritedAttributes; if ("children" in node) { node.children.forEach((node2) => collectPaths(node2, context, childrenInheritedAttributes)); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { defaultConfig, getPaths, getSvg, getSvgoConfig }); //# sourceMappingURL=index.cjs.map