@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
JavaScript
;
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