Fix code quality violations and exclude Manifest from checks

Document application modes (development/debug/production)
Add global file drop handler, order column normalization, SPA hash fix
Serve CDN assets via /_vendor/ URLs instead of merging into bundles
Add production minification with license preservation
Improve JSON formatting for debugging and production optimization
Add CDN asset caching with CSS URL inlining for production builds
Add three-mode system (development, debug, production)
Update Manifest CLAUDE.md to reflect helper class architecture
Refactor Manifest.php into helper classes for better organization
Pre-manifest-refactor checkpoint: Add app_mode documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-14 10:38:22 +00:00
parent bb9046af1b
commit d523f0f600
2355 changed files with 231384 additions and 32223 deletions

File diff suppressed because it is too large Load Diff

386
node_modules/svgo/plugins/_path.js generated vendored
View File

@@ -1,28 +1,41 @@
'use strict';
import { parsePathData, stringifyPathData } from '../lib/path.js';
/**
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').PathDataItem} PathDataItem
* @typedef Js2PathParams
* @property {number=} floatPrecision
* @property {boolean=} noSpaceAfterFlags
*
* @typedef Point
* @property {number[][]} list
* @property {number} minX
* @property {number} minY
* @property {number} maxX
* @property {number} maxY
*
* @typedef Points
* @property {Point[]} list
* @property {number} minX
* @property {number} minY
* @property {number} maxX
* @property {number} maxY
*/
const { parsePathData, stringifyPathData } = require('../lib/path.js');
/**
* @type {[number, number]}
*/
var prevCtrlPoint;
/** @type {[number, number]} */
let prevCtrlPoint;
/**
* Convert path string to JS representation.
*
* @type {(path: XastElement) => Array<PathDataItem>}
* @param {import('../lib/types.js').XastElement} path
* @returns {import('../lib/types.js').PathDataItem[]}
*/
const path2js = (path) => {
// @ts-ignore legacy
if (path.pathJS) return path.pathJS;
/**
* @type {Array<PathDataItem>}
*/
export const path2js = (path) => {
// @ts-expect-error legacy
if (path.pathJS) {
// @ts-expect-error legacy
return path.pathJS;
}
/** @type {import('../lib/types.js').PathDataItem[]} */
const pathData = []; // JS representation of the path data
const newPathData = parsePathData(path.attributes.d);
for (const { command, args } of newPathData) {
@@ -32,25 +45,22 @@ const path2js = (path) => {
if (pathData.length && pathData[0].command == 'm') {
pathData[0].command = 'M';
}
// @ts-ignore legacy
// @ts-expect-error legacy
path.pathJS = pathData;
return pathData;
};
exports.path2js = path2js;
/**
* Convert relative Path data to absolute.
*
* @type {(data: Array<PathDataItem>) => Array<PathDataItem>}
*
* @param {ReadonlyArray<import('../lib/types.js').PathDataItem>} data
* @returns {import('../lib/types.js').PathDataItem[]}
*/
const convertRelativeToAbsolute = (data) => {
/**
* @type {Array<PathDataItem>}
*/
/** @type {import('../lib/types.js').PathDataItem[]} */
const newData = [];
let start = [0, 0];
let cursor = [0, 0];
const start = [0, 0];
const cursor = [0, 0];
for (let { command, args } of data) {
args = args.slice();
@@ -172,17 +182,15 @@ const convertRelativeToAbsolute = (data) => {
return newData;
};
/**
* @typedef {{ floatPrecision?: number, noSpaceAfterFlags?: boolean }} Js2PathParams
*/
/**
* Convert path array to string.
*
* @type {(path: XastElement, data: Array<PathDataItem>, params: Js2PathParams) => void}
* @param {import('../lib/types.js').XastElement} path
* @param {ReadonlyArray<import('../lib/types.js').PathDataItem>} data
* @param {Js2PathParams} params
*/
exports.js2path = function (path, data, params) {
// @ts-ignore legacy
export const js2path = function (path, data, params) {
// @ts-expect-error legacy
path.pathJS = data;
const pathData = [];
@@ -211,7 +219,9 @@ exports.js2path = function (path, data, params) {
};
/**
* @type {(dest: Array<number>, source: Array<number>) => Array<number>}
* @param {number[]} dest
* @param {ReadonlyArray<number>} source
* @returns {number[]}
*/
function set(dest, source) {
dest[0] = source[source.length - 2];
@@ -224,9 +234,11 @@ function set(dest, source) {
* collision using Gilbert-Johnson-Keerthi distance algorithm
* https://web.archive.org/web/20180822200027/http://entropyinteractive.com/2011/04/gjk-algorithm/
*
* @type {(path1: Array<PathDataItem>, path2: Array<PathDataItem>) => boolean}
* @param {ReadonlyArray<import('../lib/types.js').PathDataItem>} path1
* @param {ReadonlyArray<import('../lib/types.js').PathDataItem>} path2
* @returns {boolean}
*/
exports.intersects = function (path1, path2) {
export const intersects = function (path1, path2) {
// Collect points of every subpath.
const points1 = gatherPoints(convertRelativeToAbsolute(path1));
const points2 = gatherPoints(convertRelativeToAbsolute(path2));
@@ -247,8 +259,9 @@ exports.intersects = function (path1, path2) {
);
});
})
)
) {
return false;
}
// Get a convex hull from points of each subpath. Has the most complexity O(n·log n).
const hullNest1 = points1.list.map(convexHull);
@@ -256,58 +269,72 @@ exports.intersects = function (path1, path2) {
// Check intersection of every subpath of the first path with every subpath of the second.
return hullNest1.some(function (hull1) {
if (hull1.list.length < 3) return false;
if (hull1.list.length < 3) {
return false;
}
return hullNest2.some(function (hull2) {
if (hull2.list.length < 3) return false;
if (hull2.list.length < 3) {
return false;
}
var simplex = [getSupport(hull1, hull2, [1, 0])], // create the initial simplex
direction = minus(simplex[0]); // set the direction to point towards the origin
const simplex = [getSupport(hull1, hull2, [1, 0])]; // create the initial simplex
const direction = minus(simplex[0]); // set the direction to point towards the origin
let iterations = 1e4; // infinite loop protection, 10 000 iterations is more than enough
var iterations = 1e4; // infinite loop protection, 10 000 iterations is more than enough
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-constant-condition
if (iterations-- == 0) {
console.error(
'Error: infinite loop while processing mergePaths plugin.'
'Error: infinite loop while processing mergePaths plugin.',
);
return true; // true is the safe value that means “do nothing with paths”
}
// add a new point
simplex.push(getSupport(hull1, hull2, direction));
// see if the new point was on the correct side of the origin
if (dot(direction, simplex[simplex.length - 1]) <= 0) return false;
if (dot(direction, simplex[simplex.length - 1]) <= 0) {
return false;
}
// process the simplex
if (processSimplex(simplex, direction)) return true;
if (processSimplex(simplex, direction)) {
return true;
}
}
});
});
/**
* @type {(a: Point, b: Point, direction: Array<number>) => Array<number>}
* @param {Point} a
* @param {Point} b
* @param {ReadonlyArray<number>} direction
* @returns {number[]}
*/
function getSupport(a, b, direction) {
return sub(supportPoint(a, direction), supportPoint(b, minus(direction)));
}
// Computes farthest polygon point in particular direction.
// Thanks to knowledge of min/max x and y coordinates we can choose a quadrant to search in.
// Since we're working on convex hull, the dot product is increasing until we find the farthest point.
/**
* @type {(polygon: Point, direction: Array<number>) => Array<number>}
* Computes farthest polygon point in particular direction. Thanks to
* knowledge of min/max x and y coordinates we can choose a quadrant to search
* in. Since we're working on convex hull, the dot product is increasing until
* we find the farthest point.
*
* @param {Point} polygon
* @param {ReadonlyArray<number>} direction
* @returns {number[]}
*/
function supportPoint(polygon, direction) {
var index =
direction[1] >= 0
? direction[0] < 0
? polygon.maxY
: polygon.maxX
: direction[0] < 0
let index =
direction[1] >= 0
? direction[0] < 0
? polygon.maxY
: polygon.maxX
: direction[0] < 0
? polygon.minX
: polygon.minY,
max = -Infinity,
value;
: polygon.minY;
let max = -Infinity;
let value;
while ((value = dot(polygon.list[index], direction)) > max) {
max = value;
index = ++index % polygon.list.length;
@@ -317,16 +344,18 @@ exports.intersects = function (path1, path2) {
};
/**
* @type {(simplex: Array<Array<number>>, direction: Array<number>) => boolean}
* @param {number[][]} simplex
* @param {number[]} direction
* @returns {boolean}
*/
function processSimplex(simplex, direction) {
// we only need to handle to 1-simplex and 2-simplex
if (simplex.length == 2) {
// 1-simplex
let a = simplex[1],
b = simplex[0],
AO = minus(simplex[1]),
AB = sub(b, a);
const a = simplex[1];
const b = simplex[0];
const AO = minus(simplex[1]);
const AB = sub(b, a);
// AO is in the same direction as AB
if (dot(AO, AB) > 0) {
// get the vector perpendicular to AB facing O
@@ -338,14 +367,14 @@ function processSimplex(simplex, direction) {
}
} else {
// 2-simplex
let a = simplex[2], // [a, b, c] = simplex
b = simplex[1],
c = simplex[0],
AB = sub(b, a),
AC = sub(c, a),
AO = minus(a),
ACB = orth(AB, AC), // the vector perpendicular to AB facing away from C
ABC = orth(AC, AB); // the vector perpendicular to AC facing away from B
const a = simplex[2]; // [a, b, c] = simplex
const b = simplex[1];
const c = simplex[0];
const AB = sub(b, a);
const AC = sub(c, a);
const AO = minus(a);
const ACB = orth(AB, AC); // the vector perpendicular to AB facing away from C
const ABC = orth(AC, AB); // the vector perpendicular to AC facing away from B
if (dot(ACB, AO) > 0) {
if (dot(AB, AO) > 0) {
@@ -368,72 +397,62 @@ function processSimplex(simplex, direction) {
simplex.splice(0, 2); // simplex = [a]
}
} // region 7
else return true;
else {
return true;
}
}
return false;
}
/**
* @type {(v: Array<number>) => Array<number>}
* @param {ReadonlyArray<number>} v
* @returns {number[]}
*/
function minus(v) {
return [-v[0], -v[1]];
}
/**
* @type {(v1: Array<number>, v2: Array<number>) => Array<number>}
* @param {ReadonlyArray<number>} v1
* @param {ReadonlyArray<number>} v2
* @returns {number[]}
*/
function sub(v1, v2) {
return [v1[0] - v2[0], v1[1] - v2[1]];
}
/**
* @type {(v1: Array<number>, v2: Array<number>) => number}
* @param {ReadonlyArray<number>} v1
* @param {ReadonlyArray<number>} v2
* @returns {number}
*/
function dot(v1, v2) {
return v1[0] * v2[0] + v1[1] * v2[1];
}
/**
* @type {(v1: Array<number>, v2: Array<number>) => Array<number>}
* @param {ReadonlyArray<number>} v
* @param {ReadonlyArray<number>} from
* @returns {number[]}
*/
function orth(v, from) {
var o = [-v[1], v[0]];
const o = [-v[1], v[0]];
return dot(o, minus(from)) < 0 ? minus(o) : o;
}
/**
* @typedef {{
* list: Array<Array<number>>,
* minX: number,
* minY: number,
* maxX: number,
* maxY: number
* }} Point
*/
/**
* @typedef {{
* list: Array<Point>,
* minX: number,
* minY: number,
* maxX: number,
* maxY: number
* }} Points
*/
/**
* @type {(pathData: Array<PathDataItem>) => Points}
* @param {ReadonlyArray<import('../lib/types.js').PathDataItem>} pathData
* @returns {Points}
*/
function gatherPoints(pathData) {
/**
* @type {Points}
*/
/** @type {Points} */
const points = { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 };
// Writes data about the extreme points on each axle
/**
* @type {(path: Point, point: Array<number>) => void}
* Writes data about the extreme points on each axle.
*
* @param {Point} path
* @param {number[]} point
*/
const addPoint = (path, point) => {
if (!path.list.length || point[1] > path.list[path.maxY][1]) {
@@ -469,15 +488,17 @@ function gatherPoints(pathData) {
points.list.length === 0
? { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 }
: points.list[points.list.length - 1];
let prev = i === 0 ? null : pathData[i - 1];
const prev = i === 0 ? null : pathData[i - 1];
let basePoint =
subPath.list.length === 0 ? null : subPath.list[subPath.list.length - 1];
let data = pathDataItem.args;
const data = pathDataItem.args;
let ctrlPoint = basePoint;
// TODO fix null hack
/**
* @type {(n: number, i: number) => number}
* TODO fix null hack
* @param {number} n
* @param {number} i
* @returns {number}
*/
const toAbsolute = (n, i) => n + (basePoint == null ? 0 : basePoint[i % 2]);
@@ -521,7 +542,7 @@ function gatherPoints(pathData) {
case 'C':
if (basePoint != null) {
// Approximate quibic Bezier curve with middle points between control points
// Approximate cubic Bezier curve with middle points between control points
addPoint(subPath, [
0.5 * (basePoint[0] + data[0]),
0.5 * (basePoint[1] + data[1]),
@@ -568,9 +589,9 @@ function gatherPoints(pathData) {
case 'A':
if (basePoint != null) {
// Convert the arc to bezier curves and use the same approximation
// @ts-ignore no idea what's going on here
var curves = a2c.apply(0, basePoint.concat(data));
// Convert the arc to zier curves and use the same approximation
// @ts-expect-error no idea what's going on here
const curves = a2c.apply(0, basePoint.concat(data));
for (
var cData;
(cData = curves.splice(0, 6).map(toAbsolute)).length;
@@ -590,33 +611,39 @@ function gatherPoints(pathData) {
0.5 * (cData[2] + cData[4]),
0.5 * (cData[3] + cData[5]),
]);
if (curves.length) addPoint(subPath, (basePoint = cData.slice(-2)));
if (curves.length) {
addPoint(subPath, (basePoint = cData.slice(-2)));
}
}
}
break;
}
// Save final command coordinates
if (data.length >= 2) addPoint(subPath, data.slice(-2));
if (data.length >= 2) {
addPoint(subPath, data.slice(-2));
}
}
return points;
}
/**
* Forms a convex hull from set of points of every subpath using monotone chain convex hull algorithm.
* https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
* Forms a convex hull from set of points of every subpath using monotone chain
* convex hull algorithm.
*
* @type {(points: Point) => Point}
* @see https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
* @param {Point} points
* @returns {Point}
*/
function convexHull(points) {
points.list.sort(function (a, b) {
return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0];
});
var lower = [],
minY = 0,
bottom = 0;
const lower = [];
let minY = 0;
let bottom = 0;
for (let i = 0; i < points.list.length; i++) {
while (
lower.length >= 2 &&
@@ -632,9 +659,9 @@ function convexHull(points) {
lower.push(points.list[i]);
}
var upper = [],
maxY = points.list.length - 1,
top = 0;
const upper = [];
let maxY = points.list.length - 1;
let top = 0;
for (let i = points.list.length; i--; ) {
while (
upper.length >= 2 &&
@@ -656,9 +683,7 @@ function convexHull(points) {
const hullList = lower.concat(upper);
/**
* @type {Point}
*/
/** @type {Point} */
const hull = {
list: hullList,
minX: 0, // by sorting
@@ -671,28 +696,30 @@ function convexHull(points) {
}
/**
* @type {(o: Array<number>, a: Array<number>, b: Array<number>) => number}
* @param {ReadonlyArray<number>} o
* @param {ReadonlyArray<number>} a
* @param {ReadonlyArray<number>} b
* @returns {number}
*/
function cross(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
/**
* Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/
* Thanks to Dmitry Baranovskiy for his great work!
* Based on code from [Snap.svg](http://snapsvg.io/) (Apache 2 license). Thanks
* to Dmitry Baranovskiy for his great work!
*
* @type {(
* x1: number,
* y1: number,
* rx: number,
* ry: number,
* angle: number,
* large_arc_flag: number,
* sweep_flag: number,
* x2: number,
* y2: number,
* recursive: Array<number>
* ) => Array<number>}
* @param {number} x1
* @param {number} y1
* @param {number} rx
* @param {number} ry
* @param {number} angle
* @param {number} large_arc_flag
* @param {number} sweep_flag
* @param {number} x2
* @param {number} y2
* @param {ReadonlyArray<number>} recursive
* @returns {number[]}
*/
const a2c = (
x1,
@@ -704,24 +731,29 @@ const a2c = (
sweep_flag,
x2,
y2,
recursive
recursive,
) => {
// for more information of where this Math came from visit:
// https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
const _120 = (Math.PI * 120) / 180;
const rad = (Math.PI / 180) * (+angle || 0);
/**
* @type {Array<number>}
*/
/** @type {number[]} */
let res = [];
/**
* @type {(x: number, y: number, rad: number) => number}
* @param {number} x
* @param {number} y
* @param {number} rad
* @returns {number}
*/
const rotateX = (x, y, rad) => {
return x * Math.cos(rad) - y * Math.sin(rad);
};
/**
* @type {(x: number, y: number, rad: number) => number}
* @param {number} x
* @param {number} y
* @param {number} rad
* @returns {number}
*/
const rotateY = (x, y, rad) => {
return x * Math.sin(rad) + y * Math.cos(rad);
@@ -731,22 +763,22 @@ const a2c = (
y1 = rotateY(x1, y1, -rad);
x2 = rotateX(x2, y2, -rad);
y2 = rotateY(x2, y2, -rad);
var x = (x1 - x2) / 2,
y = (y1 - y2) / 2;
var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
const x = (x1 - x2) / 2;
const y = (y1 - y2) / 2;
let h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
if (h > 1) {
h = Math.sqrt(h);
rx = h * rx;
ry = h * ry;
}
var rx2 = rx * rx;
var ry2 = ry * ry;
var k =
const rx2 = rx * rx;
const ry2 = ry * ry;
const k =
(large_arc_flag == sweep_flag ? -1 : 1) *
Math.sqrt(
Math.abs(
(rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)
)
(rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x),
),
);
var cx = (k * rx * y) / ry + (x1 + x2) / 2;
var cy = (k * -ry * x) / rx + (y1 + y2) / 2;
@@ -769,11 +801,11 @@ const a2c = (
cx = recursive[2];
cy = recursive[3];
}
var df = f2 - f1;
let df = f2 - f1;
if (Math.abs(df) > _120) {
var f2old = f2,
x2old = x2,
y2old = y2;
const f2old = f2;
const x2old = x2;
const y2old = y2;
f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
x2 = cx + rx * Math.cos(f2);
y2 = cy + ry * Math.sin(f2);
@@ -785,27 +817,27 @@ const a2c = (
]);
}
df = f2 - f1;
var c1 = Math.cos(f1),
s1 = Math.sin(f1),
c2 = Math.cos(f2),
s2 = Math.sin(f2),
t = Math.tan(df / 4),
hx = (4 / 3) * rx * t,
hy = (4 / 3) * ry * t,
m = [
-hx * s1,
hy * c1,
x2 + hx * s2 - x1,
y2 - hy * c2 - y1,
x2 - x1,
y2 - y1,
];
const c1 = Math.cos(f1);
const s1 = Math.sin(f1);
const c2 = Math.cos(f2);
const s2 = Math.sin(f2);
const t = Math.tan(df / 4);
const hx = (4 / 3) * rx * t;
const hy = (4 / 3) * ry * t;
const m = [
-hx * s1,
hy * c1,
x2 + hx * s2 - x1,
y2 - hy * c2 - y1,
x2 - x1,
y2 - y1,
];
if (recursive) {
return m.concat(res);
} else {
res = m.concat(res);
var newres = [];
for (var i = 0, n = res.length; i < n; i++) {
const newres = [];
for (let i = 0, n = res.length; i < n; i++) {
newres[i] =
i % 2
? rotateY(res[i - 1], res[i], rad)

View File

@@ -1,255 +1,498 @@
'use strict';
import { cleanupOutData, toFixed } from '../lib/svgo/tools.js';
/**
* @typedef TransformItem
* @property {string} name
* @property {number[]} data
*
* @typedef TransformParams
* @property {boolean} convertToShorts
* @property {number=} degPrecision
* @property {number} floatPrecision
* @property {number} transformPrecision
* @property {boolean} matrixToTransform
* @property {boolean} shortTranslate
* @property {boolean} shortScale
* @property {boolean} shortRotate
* @property {boolean} removeUseless
* @property {boolean} collapseIntoOne
* @property {boolean} leadingZero
* @property {boolean} negativeExtraSpace
*
*/
const transformTypes = new Set([
'matrix',
'rotate',
'scale',
'skewX',
'skewY',
'translate',
]);
const regTransformTypes = /matrix|translate|scale|rotate|skewX|skewY/;
const regTransformSplit =
/\s*(matrix|translate|scale|rotate|skewX|skewY)\s*\(\s*(.+?)\s*\)[\s,]*/;
const regNumericValues = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
/**
* @typedef {{ name: string, data: Array<number> }} TransformItem
*/
/**
* Convert transform string to JS representation.
*
* @type {(transformString: string) => Array<TransformItem>}
* @param {string} transformString
* @returns {TransformItem[]} Object representation of transform, or an empty array if it was malformed.
*/
exports.transform2js = (transformString) => {
// JS representation of the transform data
/**
* @type {Array<TransformItem>}
*/
export const transform2js = (transformString) => {
/** @type {TransformItem[]} */
const transforms = [];
// current transform context
/**
* @type {null | TransformItem}
*/
let current = null;
/** @type {?TransformItem} */
let currentTransform = null;
// split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate', '-45', '']
for (const item of transformString.split(regTransformSplit)) {
var num;
if (item) {
// if item is a translate function
if (regTransformTypes.test(item)) {
// then collect it and change current context
current = { name: item, data: [] };
transforms.push(current);
// else if item is data
} else {
// then split it into [10, 50] and collect as context.data
// eslint-disable-next-line no-cond-assign
while ((num = regNumericValues.exec(item))) {
num = Number(num);
if (current != null) {
current.data.push(num);
}
if (!item) {
continue;
}
if (transformTypes.has(item)) {
currentTransform = { name: item, data: [] };
transforms.push(currentTransform);
} else {
let num;
// then split it into [10, 50] and collect as context.data
while ((num = regNumericValues.exec(item))) {
num = Number(num);
if (currentTransform != null) {
currentTransform.data.push(num);
}
}
}
}
// return empty array if broken transform (no data)
return current == null || current.data.length == 0 ? [] : transforms;
};
return currentTransform == null || currentTransform.data.length == 0
? []
: transforms;
};
/**
* Multiply transforms into one.
*
* @type {(transforms: Array<TransformItem>) => TransformItem}
* @param {ReadonlyArray<TransformItem>} transforms
* @returns {TransformItem}
*/
exports.transformsMultiply = (transforms) => {
// convert transforms objects to the matrices
export const transformsMultiply = (transforms) => {
const matrixData = transforms.map((transform) => {
if (transform.name === 'matrix') {
return transform.data;
}
return transformToMatrix(transform);
});
// multiply all matrices into one
const matrixTransform = {
name: 'matrix',
data:
matrixData.length > 0 ? matrixData.reduce(multiplyTransformMatrices) : [],
};
return matrixTransform;
};
/**
* math utilities in radians.
* Math utilities in radians.
*/
const mth = {
/**
* @type {(deg: number) => number}
* @param {number} deg
* @returns {number}
*/
rad: (deg) => {
return (deg * Math.PI) / 180;
},
/**
* @type {(rad: number) => number}
* @param {number} rad
* @returns {number}
*/
deg: (rad) => {
return (rad * 180) / Math.PI;
},
/**
* @type {(deg: number) => number}
* @param {number} deg
* @returns {number}
*/
cos: (deg) => {
return Math.cos(mth.rad(deg));
},
/**
* @type {(val: number, floatPrecision: number) => number}
* @param {number} val
* @param {number} floatPrecision
* @returns {number}
*/
acos: (val, floatPrecision) => {
return Number(mth.deg(Math.acos(val)).toFixed(floatPrecision));
return toFixed(mth.deg(Math.acos(val)), floatPrecision);
},
/**
* @type {(deg: number) => number}
* @param {number} deg
* @returns {number}
*/
sin: (deg) => {
return Math.sin(mth.rad(deg));
},
/**
* @type {(val: number, floatPrecision: number) => number}
* @param {number} val
* @param {number} floatPrecision
* @returns {number}
*/
asin: (val, floatPrecision) => {
return Number(mth.deg(Math.asin(val)).toFixed(floatPrecision));
return toFixed(mth.deg(Math.asin(val)), floatPrecision);
},
/**
* @type {(deg: number) => number}
* @param {number} deg
* @returns {number}
*/
tan: (deg) => {
return Math.tan(mth.rad(deg));
},
/**
* @type {(val: number, floatPrecision: number) => number}
* @param {number} val
* @param {number} floatPrecision
* @returns {number}
*/
atan: (val, floatPrecision) => {
return Number(mth.deg(Math.atan(val)).toFixed(floatPrecision));
return toFixed(mth.deg(Math.atan(val)), floatPrecision);
},
};
/**
* @typedef {{
* convertToShorts: boolean,
* floatPrecision: number,
* transformPrecision: number,
* matrixToTransform: boolean,
* shortTranslate: boolean,
* shortScale: boolean,
* shortRotate: boolean,
* removeUseless: boolean,
* collapseIntoOne: boolean,
* leadingZero: boolean,
* negativeExtraSpace: boolean,
* }} TransformParams
* @param {TransformItem} matrix
* @returns {TransformItem[][]}
*/
const getDecompositions = (matrix) => {
const decompositions = [];
const qrab = decomposeQRAB(matrix);
const qrcd = decomposeQRCD(matrix);
if (qrab) {
decompositions.push(qrab);
}
if (qrcd) {
decompositions.push(qrcd);
}
return decompositions;
};
/**
* Decompose matrix into simple transforms. See
* https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html
*
* @type {(transform: TransformItem, params: TransformParams) => Array<TransformItem>}
* @param {TransformItem} matrix
* @returns {TransformItem[] | undefined}
* @see {@link https://frederic-wang.fr/2013/12/01/decomposition-of-2d-transform-matrices/} Where applicable, variables are named in accordance with this document.
*/
exports.matrixToTransform = (transform, params) => {
let floatPrecision = params.floatPrecision;
let data = transform.data;
let transforms = [];
let sx = Number(
Math.hypot(data[0], data[1]).toFixed(params.transformPrecision)
);
let sy = Number(
((data[0] * data[3] - data[1] * data[2]) / sx).toFixed(
params.transformPrecision
)
);
let colsSum = data[0] * data[2] + data[1] * data[3];
let rowsSum = data[0] * data[1] + data[2] * data[3];
let scaleBefore = rowsSum != 0 || sx == sy;
const decomposeQRAB = (matrix) => {
const data = matrix.data;
const [a, b, c, d, e, f] = data;
const delta = a * d - b * c;
if (delta === 0) {
return;
}
const r = Math.hypot(a, b);
if (r === 0) {
return;
}
const decomposition = [];
const cosOfRotationAngle = a / r;
// [..., ..., ..., ..., tx, ty] → translate(tx, ty)
if (data[4] || data[5]) {
transforms.push({
if (e || f) {
decomposition.push({
name: 'translate',
data: data.slice(4, data[5] ? 6 : 5),
data: [e, f],
});
}
// [sx, 0, tan(a)·sy, sy, 0, 0] → skewX(a)·scale(sx, sy)
if (!data[1] && data[2]) {
transforms.push({
if (cosOfRotationAngle !== 1) {
const rotationAngleRads = Math.acos(cosOfRotationAngle);
decomposition.push({
name: 'rotate',
data: [mth.deg(b < 0 ? -rotationAngleRads : rotationAngleRads), 0, 0],
});
}
const sx = r;
const sy = delta / sx;
if (sx !== 1 || sy !== 1) {
decomposition.push({ name: 'scale', data: [sx, sy] });
}
const ac_plus_bd = a * c + b * d;
if (ac_plus_bd) {
decomposition.push({
name: 'skewX',
data: [mth.atan(data[2] / sy, floatPrecision)],
data: [mth.deg(Math.atan(ac_plus_bd / (a * a + b * b)))],
});
// [sx, sx·tan(a), 0, sy, 0, 0] → skewY(a)·scale(sx, sy)
} else if (data[1] && !data[2]) {
transforms.push({
name: 'skewY',
data: [mth.atan(data[1] / data[0], floatPrecision)],
});
sx = data[0];
sy = data[3];
// [sx·cos(a), sx·sin(a), sy·-sin(a), sy·cos(a), x, y] → rotate(a[, cx, cy])·(scale or skewX) or
// [sx·cos(a), sy·sin(a), sx·-sin(a), sy·cos(a), x, y] → scale(sx, sy)·rotate(a[, cx, cy]) (if !scaleBefore)
} else if (!colsSum || (sx == 1 && sy == 1) || !scaleBefore) {
if (!scaleBefore) {
sx = (data[0] < 0 ? -1 : 1) * Math.hypot(data[0], data[2]);
sy = (data[3] < 0 ? -1 : 1) * Math.hypot(data[1], data[3]);
transforms.push({ name: 'scale', data: [sx, sy] });
}
var angle = Math.min(Math.max(-1, data[0] / sx), 1),
rotate = [
mth.acos(angle, floatPrecision) *
((scaleBefore ? 1 : sy) * data[1] < 0 ? -1 : 1),
];
if (rotate[0]) transforms.push({ name: 'rotate', data: rotate });
if (rowsSum && colsSum)
transforms.push({
name: 'skewX',
data: [mth.atan(colsSum / (sx * sx), floatPrecision)],
});
// rotate(a, cx, cy) can consume translate() within optional arguments cx, cy (rotation point)
if (rotate[0] && (data[4] || data[5])) {
transforms.shift();
var cos = data[0] / sx,
sin = data[1] / (scaleBefore ? sx : sy),
x = data[4] * (scaleBefore ? 1 : sy),
y = data[5] * (scaleBefore ? 1 : sx),
denom =
(Math.pow(1 - cos, 2) + Math.pow(sin, 2)) *
(scaleBefore ? 1 : sx * sy);
rotate.push(((1 - cos) * x - sin * y) / denom);
rotate.push(((1 - cos) * y + sin * x) / denom);
}
// Too many transformations, return original matrix if it isn't just a scale/translate
} else if (data[1] || data[2]) {
return [transform];
}
if ((scaleBefore && (sx != 1 || sy != 1)) || !transforms.length)
transforms.push({
name: 'scale',
data: sx == sy ? [sx] : [sx, sy],
return decomposition;
};
/**
* @param {TransformItem} matrix
* @returns {TransformItem[] | undefined}
* @see {@link https://frederic-wang.fr/2013/12/01/decomposition-of-2d-transform-matrices/} Where applicable, variables are named in accordance with this document.
*/
const decomposeQRCD = (matrix) => {
const data = matrix.data;
const [a, b, c, d, e, f] = data;
const delta = a * d - b * c;
if (delta === 0) {
return;
}
const s = Math.hypot(c, d);
if (s === 0) {
return;
}
const decomposition = [];
if (e || f) {
decomposition.push({
name: 'translate',
data: [e, f],
});
}
const rotationAngleRads = Math.PI / 2 - (d < 0 ? -1 : 1) * Math.acos(-c / s);
decomposition.push({
name: 'rotate',
data: [mth.deg(rotationAngleRads), 0, 0],
});
const sx = delta / s;
const sy = s;
if (sx !== 1 || sy !== 1) {
decomposition.push({ name: 'scale', data: [sx, sy] });
}
const ac_plus_bd = a * c + b * d;
if (ac_plus_bd) {
decomposition.push({
name: 'skewY',
data: [mth.deg(Math.atan(ac_plus_bd / (c * c + d * d)))],
});
}
return decomposition;
};
/**
* Convert translate(tx,ty)rotate(a) to rotate(a,cx,cy).
* @param {number} tx
* @param {number} ty
* @param {number} a
* @returns {TransformItem}
*/
const mergeTranslateAndRotate = (tx, ty, a) => {
// From https://www.w3.org/TR/SVG11/coords.html#TransformAttribute:
// We have translate(tx,ty) rotate(a). This is equivalent to [cos(a) sin(a) -sin(a) cos(a) tx ty].
//
// rotate(a,cx,cy) is equivalent to translate(cx, cy) rotate(a) translate(-cx, -cy).
// Multiplying the right side gives the matrix
// [cos(a) sin(a) -sin(a) cos(a)
// -cx * cos(a) + cy * sin(a) + cx
// -cx * sin(a) - cy * cos(a) + cy
// ]
//
// We need cx and cy such that
// tx = -cx * cos(a) + cy * sin(a) + cx
// ty = -cx * sin(a) - cy * cos(a) + cy
//
// Solving these for cx and cy gives
// cy = (d * ty + e * tx)/(d^2 + e^2)
// cx = (tx - e * cy) / d
// where d = 1 - cos(a) and e = sin(a)
const rotationAngleRads = mth.rad(a);
const d = 1 - Math.cos(rotationAngleRads);
const e = Math.sin(rotationAngleRads);
const cy = (d * ty + e * tx) / (d * d + e * e);
const cx = (tx - e * cy) / d;
return { name: 'rotate', data: [a, cx, cy] };
};
/**
* @param {TransformItem} t
* @returns {Boolean}
*/
const isIdentityTransform = (t) => {
switch (t.name) {
case 'rotate':
case 'skewX':
case 'skewY':
return t.data[0] === 0;
case 'scale':
return t.data[0] === 1 && t.data[1] === 1;
case 'translate':
return t.data[0] === 0 && t.data[1] === 0;
}
return false;
};
/**
* Optimize matrix of simple transforms.
* @param {ReadonlyArray<TransformItem>} roundedTransforms
* @param {ReadonlyArray<TransformItem>} rawTransforms
* @returns {TransformItem[]}
*/
const optimize = (roundedTransforms, rawTransforms) => {
const optimizedTransforms = [];
for (let index = 0; index < roundedTransforms.length; index++) {
const roundedTransform = roundedTransforms[index];
// Don't include any identity transforms.
if (isIdentityTransform(roundedTransform)) {
continue;
}
const data = roundedTransform.data;
switch (roundedTransform.name) {
case 'rotate':
switch (data[0]) {
case 180:
case -180:
{
// If the next element is a scale, invert it, and don't add the rotate to the optimized array.
const next = roundedTransforms[index + 1];
if (next && next.name === 'scale') {
optimizedTransforms.push(
createScaleTransform(next.data.map((v) => -v)),
);
index++;
} else {
// Otherwise replace the rotate with a scale(-1).
optimizedTransforms.push({
name: 'scale',
data: [-1],
});
}
}
continue;
}
optimizedTransforms.push({
name: 'rotate',
data: data.slice(0, data[1] || data[2] ? 3 : 1),
});
break;
case 'scale':
optimizedTransforms.push(createScaleTransform(data));
break;
case 'skewX':
case 'skewY':
optimizedTransforms.push({
name: roundedTransform.name,
data: [data[0]],
});
break;
case 'translate':
{
// If the next item is a rotate(a,0,0), merge the translate and rotate.
// If the rotation angle is +/-180, assume it will be optimized out, and don't do the merge.
const next = roundedTransforms[index + 1];
if (
next &&
next.name === 'rotate' &&
next.data[0] !== 180 &&
next.data[0] !== -180 &&
next.data[0] !== 0 &&
next.data[1] === 0 &&
next.data[2] === 0
) {
// Use the un-rounded data to do the merge.
const data = rawTransforms[index].data;
optimizedTransforms.push(
mergeTranslateAndRotate(
data[0],
data[1],
rawTransforms[index + 1].data[0],
),
);
// Skip over the rotate.
index++;
continue;
}
}
optimizedTransforms.push({
name: 'translate',
data: data.slice(0, data[1] ? 2 : 1),
});
break;
}
}
// If everything was optimized out, return identity transform scale(1).
return optimizedTransforms.length
? optimizedTransforms
: [{ name: 'scale', data: [1] }];
};
/**
* @param {ReadonlyArray<number>} data
* @returns {TransformItem}
*/
const createScaleTransform = (data) => {
const scaleData = data.slice(0, data[0] === data[1] ? 1 : 2);
return {
name: 'scale',
data: scaleData,
};
};
/**
* Decompose matrix into simple transforms and optimize.
* @param {TransformItem} origMatrix
* @param {TransformParams} params
* @returns {TransformItem[]}
*/
export const matrixToTransform = (origMatrix, params) => {
const decomposed = getDecompositions(origMatrix);
let shortest;
let shortestLen = Number.MAX_VALUE;
for (const decomposition of decomposed) {
// Make a copy of the decomposed matrix, and round all data. We need to keep the original decomposition,
// at full precision, to perform some optimizations.
const roundedTransforms = decomposition.map((transformItem) => {
const transformCopy = {
name: transformItem.name,
data: [...transformItem.data],
};
return roundTransform(transformCopy, params);
});
return transforms;
const optimized = optimize(roundedTransforms, decomposition);
const len = js2transform(optimized, params).length;
if (len < shortestLen) {
shortest = optimized;
shortestLen = len;
}
}
return shortest ?? [origMatrix];
};
/**
* Convert transform to the matrix data.
*
* @type {(transform: TransformItem) => Array<number> }
* @param {TransformItem} transform
* @returns {number[]}
*/
const transformToMatrix = (transform) => {
if (transform.name === 'matrix') {
@@ -265,16 +508,16 @@ const transformToMatrix = (transform) => {
transform.data[0],
0,
0,
transform.data[1] || transform.data[0],
transform.data[1] ?? transform.data[0],
0,
0,
];
case 'rotate':
// [cos(a), sin(a), -sin(a), cos(a), x, y]
var cos = mth.cos(transform.data[0]),
sin = mth.sin(transform.data[0]),
cx = transform.data[1] || 0,
cy = transform.data[2] || 0;
var cos = mth.cos(transform.data[0]);
var sin = mth.sin(transform.data[0]);
var cx = transform.data[1] || 0;
var cy = transform.data[2] || 0;
return [
cos,
sin,
@@ -295,18 +538,18 @@ const transformToMatrix = (transform) => {
};
/**
* Applies transformation to an arc. To do so, we represent ellipse as a matrix, multiply it
* by the transformation matrix and use a singular value decomposition to represent in a form
* rotate(θ)·scale(a b)·rotate(φ). This gives us new ellipse params a, b and θ.
* SVD is being done with the formulae provided by Wolffram|Alpha (svd {{m0, m2}, {m1, m3}})
* Applies transformation to an arc. To do so, we represent ellipse as a matrix,
* multiply it by the transformation matrix and use a singular value
* decomposition to represent in a form rotate(θ)·scale(a b)·rotate(φ). This
* gives us new ellipse params a, b and θ. SVD is being done with the formulae
* provided by Wolfram|Alpha (svd {{m0, m2}, {m1, m3}})
*
* @type {(
* cursor: [x: number, y: number],
* arc: Array<number>,
* transform: Array<number>
* ) => Array<number>}
* @param {[number, number]} cursor
* @param {number[]} arc
* @param {ReadonlyArray<number>} transform
* @returns {number[]}
*/
exports.transformArc = (cursor, arc, transform) => {
export const transformArc = (cursor, arc, transform) => {
const x = arc[5] - cursor[0];
const y = arc[6] - cursor[1];
let a = arc[0];
@@ -365,7 +608,9 @@ exports.transformArc = (cursor, arc, transform) => {
/**
* Multiply transformation matrices.
*
* @type {(a: Array<number>, b: Array<number>) => Array<number>}
* @param {ReadonlyArray<number>} a
* @param {ReadonlyArray<number>} b
* @returns {number[]}
*/
const multiplyTransformMatrices = (a, b) => {
return [
@@ -377,3 +622,134 @@ const multiplyTransformMatrices = (a, b) => {
a[1] * b[4] + a[3] * b[5] + a[5],
];
};
/**
* @param {TransformItem} transform
* @param {TransformParams} params
* @returns {TransformItem}
*/
export const roundTransform = (transform, params) => {
switch (transform.name) {
case 'translate':
transform.data = floatRound(transform.data, params);
break;
case 'rotate':
transform.data = [
...degRound(transform.data.slice(0, 1), params),
...floatRound(transform.data.slice(1), params),
];
break;
case 'skewX':
case 'skewY':
transform.data = degRound(transform.data, params);
break;
case 'scale':
transform.data = transformRound(transform.data, params);
break;
case 'matrix':
transform.data = [
...transformRound(transform.data.slice(0, 4), params),
...floatRound(transform.data.slice(4), params),
];
break;
}
return transform;
};
/**
* @param {number[]} data
* @param {TransformParams} params
* @returns {number[]}
*/
const degRound = (data, params) => {
if (
params.degPrecision != null &&
params.degPrecision >= 1 &&
params.floatPrecision < 20
) {
return smartRound(params.degPrecision, data);
} else {
return round(data);
}
};
/**
* @param {number[]} data
* @param {TransformParams} params
* @returns {number[]}
*/
const floatRound = (data, params) => {
if (params.floatPrecision >= 1 && params.floatPrecision < 20) {
return smartRound(params.floatPrecision, data);
} else {
return round(data);
}
};
/**
* @param {number[]} data
* @param {TransformParams} params
* @returns {number[]}
*/
const transformRound = (data, params) => {
if (params.transformPrecision >= 1 && params.floatPrecision < 20) {
return smartRound(params.transformPrecision, data);
} else {
return round(data);
}
};
/**
* Rounds numbers in array.
*
* @param {ReadonlyArray<number>} data
* @returns {number[]}
*/
const round = (data) => {
return data.map(Math.round);
};
/**
* Decrease accuracy of floating-point numbers in transforms keeping a specified
* number of decimals. Smart rounds values like 2.349 to 2.35.
*
* @param {number} precision
* @param {number[]} data
* @returns {number[]}
*/
const smartRound = (precision, data) => {
for (
let i = data.length,
tolerance = +Math.pow(0.1, precision).toFixed(precision);
i--;
) {
if (toFixed(data[i], precision) !== data[i]) {
const rounded = +data[i].toFixed(precision - 1);
data[i] =
+Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance
? +data[i].toFixed(precision)
: rounded;
}
}
return data;
};
/**
* Convert transforms JS representation to string.
*
* @param {ReadonlyArray<TransformItem>} transformJS
* @param {TransformParams} params
* @returns {string}
*/
export const js2transform = (transformJS, params) => {
const transformString = transformJS
.map((transform) => {
roundTransform(transform, params);
return `${transform.name}(${cleanupOutData(transform.data, params)})`;
})
.join('');
return transformString;
};

View File

@@ -1,11 +1,13 @@
'use strict';
/**
* @typedef AddAttributesToSVGElementParams
* @property {string | Record<string, null | string>=} attribute
* @property {Array<string | Record<string, null | string>>=} attributes
*/
exports.name = 'addAttributesToSVGElement';
exports.type = 'visitor';
exports.active = false;
exports.description = 'adds attributes to an outer <svg> element';
export const name = 'addAttributesToSVGElement';
export const description = 'adds attributes to an outer <svg> element';
var ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters.
const ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters.
It should have a list of "attributes" or one "attribute".
Config example:
@@ -45,16 +47,13 @@ plugins: [
`;
/**
* Add attributes to an outer <svg> element. Example config:
* Add attributes to an outer <svg> element.
*
* @author April Arcus
*
* @type {import('../lib/types').Plugin<{
* attribute?: string | Record<string, null | string>,
* attributes?: Array<string | Record<string, null | string>>
* }>}
* @type {import('../lib/types.js').Plugin<AddAttributesToSVGElementParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
if (!Array.isArray(params.attributes) && !params.attribute) {
console.error(ENOCLS);
return null;
@@ -67,14 +66,14 @@ exports.fn = (root, params) => {
for (const attribute of attributes) {
if (typeof attribute === 'string') {
if (node.attributes[attribute] == null) {
// @ts-ignore disallow explicit nullable attribute value
// @ts-expect-error disallow explicit nullable attribute value
node.attributes[attribute] = undefined;
}
}
if (typeof attribute === 'object') {
for (const key of Object.keys(attribute)) {
if (node.attributes[key] == null) {
// @ts-ignore disallow explicit nullable attribute value
// @ts-expect-error disallow explicit nullable attribute value
node.attributes[key] = attribute[key];
}
}

View File

@@ -1,11 +1,13 @@
'use strict';
/**
* @typedef AddClassesToSVGElementParams
* @property {string | ((node: import('../lib/types.js').XastElement, info: import('../lib/types.js').PluginInfo) => string)=} className
* @property {Array<string | ((node: import('../lib/types.js').XastElement, info: import('../lib/types.js').PluginInfo) => string)>=} classNames
*/
exports.name = 'addClassesToSVGElement';
exports.type = 'visitor';
exports.active = false;
exports.description = 'adds classnames to an outer <svg> element';
export const name = 'addClassesToSVGElement';
export const description = 'adds classnames to an outer <svg> element';
var ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters.
const ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters.
It should have a list of classes in "classNames" or one "className".
Config example:
@@ -51,14 +53,11 @@ plugins: [
*
* @author April Arcus
*
* @type {import('../lib/types').Plugin<{
* className?: string,
* classNames?: Array<string>
* }>}
* @type {import('../lib/types.js').Plugin<AddClassesToSVGElementParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params, info) => {
if (
!(Array.isArray(params.classNames) && params.classNames.some(String)) &&
!(Array.isArray(params.classNames) && params.classNames.length !== 0) &&
!params.className
) {
console.error(ENOCLS);
@@ -72,11 +71,15 @@ exports.fn = (root, params) => {
const classList = new Set(
node.attributes.class == null
? null
: node.attributes.class.split(' ')
: node.attributes.class.split(' '),
);
for (const className of classNames) {
if (className != null) {
classList.add(className);
const classToAdd =
typeof className === 'string'
? className
: className(node, info);
classList.add(classToAdd);
}
}
node.attributes.class = Array.from(classList).join(' ');

View File

@@ -1,143 +1,184 @@
'use strict';
// TODO implement as separate plugin
const {
transformsMultiply,
import { path2js } from './_path.js';
import {
transform2js,
transformArc,
} = require('./_transforms.js');
const { removeLeadingZero } = require('../lib/svgo/tools.js');
const { referencesProps, attrsGroupsDefaults } = require('./_collections.js');
transformsMultiply,
} from './_transforms.js';
import { attrsGroupsDefaults, referencesProps } from './_collections.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';
import { includesUrlReference, removeLeadingZero } from '../lib/svgo/tools.js';
const regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
const defaultStrokeWidth = attrsGroupsDefaults.presentation['stroke-width'];
/**
* Apply transformation(s) to the Path data.
*
* @param {Object} elem current element
* @param {Array} path input path data
* @param {Object} params whether to apply transforms to stroked lines and transform precision (used for stroke width)
* @return {Array} output path data
* @type {import('../lib/types.js').Plugin<{
* transformPrecision: number,
* applyTransformsStroked: boolean,
* }>}
*/
const applyTransforms = (elem, pathData, params) => {
// if there are no 'stroke' attr and references to other objects such as
// gradiends or clip-path which are also subjects to transform.
if (
elem.attributes.transform == null ||
elem.attributes.transform === '' ||
// styles are not considered when applying transform
// can be fixed properly with new style engine
elem.attributes.style != null ||
Object.entries(elem.attributes).some(
([name, value]) =>
referencesProps.includes(name) && value.includes('url(')
)
) {
return;
}
const matrix = transformsMultiply(transform2js(elem.attributes.transform));
const stroke = elem.computedAttr('stroke');
const id = elem.computedAttr('id');
const transformPrecision = params.transformPrecision;
if (stroke && stroke != 'none') {
if (
!params.applyTransformsStroked ||
((matrix.data[0] != matrix.data[3] ||
matrix.data[1] != -matrix.data[2]) &&
(matrix.data[0] != -matrix.data[3] || matrix.data[1] != matrix.data[2]))
)
return;
// "stroke-width" should be inside the part with ID, otherwise it can be overrided in <use>
if (id) {
let idElem = elem;
let hasStrokeWidth = false;
do {
if (idElem.attributes['stroke-width']) {
hasStrokeWidth = true;
}
} while (
idElem.attributes.id !== id &&
!hasStrokeWidth &&
(idElem = idElem.parentNode)
);
if (!hasStrokeWidth) return;
}
const scale = +Math.sqrt(
matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1]
).toFixed(transformPrecision);
if (scale !== 1) {
const strokeWidth =
elem.computedAttr('stroke-width') || defaultStrokeWidth;
if (
elem.attributes['vector-effect'] == null ||
elem.attributes['vector-effect'] !== 'non-scaling-stroke'
) {
if (elem.attributes['stroke-width'] != null) {
elem.attributes['stroke-width'] = elem.attributes['stroke-width']
.trim()
.replace(regNumericValues, (num) => removeLeadingZero(num * scale));
} else {
elem.attributes['stroke-width'] = strokeWidth.replace(
regNumericValues,
(num) => removeLeadingZero(num * scale)
);
export const applyTransforms = (root, params) => {
const stylesheet = collectStylesheet(root);
return {
element: {
enter: (node) => {
if (node.attributes.d == null) {
return;
}
if (elem.attributes['stroke-dashoffset'] != null) {
elem.attributes['stroke-dashoffset'] = elem.attributes[
'stroke-dashoffset'
]
.trim()
.replace(regNumericValues, (num) => removeLeadingZero(num * scale));
// stroke and stroke-width can be redefined with <use>
if (node.attributes.id != null) {
return;
}
if (elem.attributes['stroke-dasharray'] != null) {
elem.attributes['stroke-dasharray'] = elem.attributes[
'stroke-dasharray'
]
.trim()
.replace(regNumericValues, (num) => removeLeadingZero(num * scale));
// if there are no 'stroke' attr and references to other objects such as
// gradients or clip-path which are also subjects to transform.
if (
node.attributes.transform == null ||
node.attributes.transform === '' ||
// styles are not considered when applying transform
// can be fixed properly with new style engine
node.attributes.style != null ||
Object.entries(node.attributes).some(
([name, value]) =>
referencesProps.has(name) && includesUrlReference(value),
)
) {
return;
}
}
}
} else if (id) {
// Stroke and stroke-width can be redefined with <use>
return;
}
applyMatrixToPathData(pathData, matrix.data);
const computedStyle = computeStyle(stylesheet, node);
const transformStyle = computedStyle.transform;
// remove transform attr
delete elem.attributes.transform;
// Transform overridden in <style> tag which is not considered
if (
transformStyle.type === 'static' &&
transformStyle.value !== node.attributes.transform
) {
return;
}
return;
const matrix = transformsMultiply(
transform2js(node.attributes.transform),
);
const stroke =
computedStyle.stroke?.type === 'static'
? computedStyle.stroke.value
: null;
const strokeWidth =
computedStyle['stroke-width']?.type === 'static'
? computedStyle['stroke-width'].value
: null;
const transformPrecision = params.transformPrecision;
if (
computedStyle.stroke?.type === 'dynamic' ||
computedStyle['stroke-width']?.type === 'dynamic'
) {
return;
}
const scale = Number(
Math.hypot(matrix.data[0], matrix.data[1]).toFixed(
transformPrecision,
),
);
if (stroke && stroke != 'none') {
if (!params.applyTransformsStroked) {
return;
}
// stroke cannot be transformed with different vertical and horizontal scale or skew
if (
(matrix.data[0] !== matrix.data[3] ||
matrix.data[1] !== -matrix.data[2]) &&
(matrix.data[0] !== -matrix.data[3] ||
matrix.data[1] !== matrix.data[2])
) {
return;
}
// apply transform to stroke-width, stroke-dashoffset and stroke-dasharray
if (scale !== 1) {
if (node.attributes['vector-effect'] !== 'non-scaling-stroke') {
node.attributes['stroke-width'] = (
strokeWidth || attrsGroupsDefaults.presentation['stroke-width']
)
.trim()
.replace(regNumericValues, (num) =>
removeLeadingZero(Number(num) * scale),
);
if (node.attributes['stroke-dashoffset'] != null) {
node.attributes['stroke-dashoffset'] = node.attributes[
'stroke-dashoffset'
]
.trim()
.replace(regNumericValues, (num) =>
removeLeadingZero(Number(num) * scale),
);
}
if (node.attributes['stroke-dasharray'] != null) {
node.attributes['stroke-dasharray'] = node.attributes[
'stroke-dasharray'
]
.trim()
.replace(regNumericValues, (num) =>
removeLeadingZero(Number(num) * scale),
);
}
}
}
}
const pathData = path2js(node);
applyMatrixToPathData(pathData, matrix.data);
// remove transform attr
delete node.attributes.transform;
},
},
};
};
exports.applyTransforms = applyTransforms;
/**
* @param {ReadonlyArray<number>} matrix
* @param {number} x
* @param {number} y
* @returns {[number, number]}
*/
const transformAbsolutePoint = (matrix, x, y) => {
const newX = matrix[0] * x + matrix[2] * y + matrix[4];
const newY = matrix[1] * x + matrix[3] * y + matrix[5];
return [newX, newY];
};
/**
* @param {ReadonlyArray<number>} matrix
* @param {number} x
* @param {number} y
* @returns {[number, number]}
*/
const transformRelativePoint = (matrix, x, y) => {
const newX = matrix[0] * x + matrix[2] * y;
const newY = matrix[1] * x + matrix[3] * y;
return [newX, newY];
};
/**
* @param {ReadonlyArray<import('../lib/types.js').PathDataItem>} pathData
* @param {ReadonlyArray<number>} matrix
*/
const applyMatrixToPathData = (pathData, matrix) => {
/** @type {[number, number]} */
const start = [0, 0];
/** @type {[number, number]} */
const cursor = [0, 0];
for (const pathItem of pathData) {
@@ -164,7 +205,7 @@ const applyMatrixToPathData = (pathData, matrix) => {
}
// horizontal lineto (x)
// convert to lineto to handle two-dimentional transforms
// convert to lineto to handle two-dimensional transforms
if (command === 'H') {
command = 'L';
args = [args[0], cursor[1]];
@@ -175,7 +216,7 @@ const applyMatrixToPathData = (pathData, matrix) => {
}
// vertical lineto (y)
// convert to lineto to handle two-dimentional transforms
// convert to lineto to handle two-dimensional transforms
if (command === 'V') {
command = 'L';
args = [cursor[0], args[0]];

View File

@@ -1,9 +1,12 @@
'use strict';
/**
* @typedef CleanupAttrsParams
* @property {boolean=} newlines
* @property {boolean=} trim
* @property {boolean=} spaces
*/
exports.name = 'cleanupAttrs';
exports.type = 'visitor';
exports.active = true;
exports.description =
export const name = 'cleanupAttrs';
export const description =
'cleanups attributes from newlines, trailing and repeating spaces';
const regNewlinesNeedSpace = /(\S)\r?\n(\S)/g;
@@ -14,29 +17,24 @@ const regSpaces = /\s{2,}/g;
* Cleanup attributes values from newlines, trailing and repeating spaces.
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* newlines?: boolean,
* trim?: boolean,
* spaces?: boolean
* }>}
* @type {import('../lib/types.js').Plugin<CleanupAttrsParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const { newlines = true, trim = true, spaces = true } = params;
return {
element: {
enter: (node) => {
for (const name of Object.keys(node.attributes)) {
if (newlines) {
// new line which requires a space instead of themselve
// new line which requires a space instead
node.attributes[name] = node.attributes[name].replace(
regNewlinesNeedSpace,
(match, p1, p2) => p1 + ' ' + p2
(match, p1, p2) => p1 + ' ' + p2,
);
// simple new line
node.attributes[name] = node.attributes[name].replace(
regNewlines,
''
'',
);
}
if (trim) {
@@ -45,7 +43,7 @@ exports.fn = (root, params) => {
if (spaces) {
node.attributes[name] = node.attributes[name].replace(
regSpaces,
' '
' ',
);
}
}

View File

@@ -1,32 +1,28 @@
'use strict';
import * as csstree from 'css-tree';
import { visit } from '../lib/util/visit.js';
const { visit } = require('../lib/xast.js');
exports.type = 'visitor';
exports.name = 'cleanupEnableBackground';
exports.active = true;
exports.description =
export const name = 'cleanupEnableBackground';
export const description =
'remove or cleanup enable-background attribute when possible';
const regEnableBackground =
/^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/;
/**
* Remove or cleanup enable-background attr which coincides with a width/height box.
* Remove or cleanup enable-background attr which coincides with a width/height
* box.
*
* @see https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty
*
* @example
* <svg width="100" height="50" enable-background="new 0 0 100 50">
*
* ⬇
* <svg width="100" height="50">
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = (root) => {
const regEnableBackground =
/^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/;
export const fn = (root) => {
let hasFilter = false;
visit(root, {
element: {
enter: (node) => {
@@ -40,36 +36,129 @@ exports.fn = (root) => {
return {
element: {
enter: (node) => {
if (node.attributes['enable-background'] == null) {
return;
}
if (hasFilter) {
if (
(node.name === 'svg' ||
node.name === 'mask' ||
node.name === 'pattern') &&
node.attributes.width != null &&
node.attributes.height != null
) {
const match =
node.attributes['enable-background'].match(regEnableBackground);
if (
match != null &&
node.attributes.width === match[1] &&
node.attributes.height === match[3]
) {
if (node.name === 'svg') {
delete node.attributes['enable-background'];
} else {
node.attributes['enable-background'] = 'new';
/** @type {?csstree.CssNode} */
let newStyle = null;
/** @type {?csstree.ListItem<csstree.CssNode>} */
let enableBackgroundDeclaration = null;
if (node.attributes.style != null) {
newStyle = csstree.parse(node.attributes.style, {
context: 'declarationList',
});
if (newStyle.type === 'DeclarationList') {
/** @type {csstree.ListItem<csstree.CssNode>[]} */
const enableBackgroundDeclarations = [];
csstree.walk(newStyle, (node, nodeItem) => {
if (
node.type === 'Declaration' &&
node.property === 'enable-background'
) {
enableBackgroundDeclarations.push(nodeItem);
enableBackgroundDeclaration = nodeItem;
}
});
for (let i = 0; i < enableBackgroundDeclarations.length - 1; i++) {
newStyle.children.remove(enableBackgroundDeclarations[i]);
}
}
} else {
//we don't need 'enable-background' if we have no filters
}
if (!hasFilter) {
delete node.attributes['enable-background'];
if (newStyle?.type === 'DeclarationList') {
if (enableBackgroundDeclaration) {
newStyle.children.remove(enableBackgroundDeclaration);
}
if (newStyle.children.isEmpty) {
delete node.attributes.style;
} else {
node.attributes.style = csstree.generate(newStyle);
}
}
return;
}
const hasDimensions =
node.attributes.width != null && node.attributes.height != null;
if (
(node.name === 'svg' ||
node.name === 'mask' ||
node.name === 'pattern') &&
hasDimensions
) {
const attrValue = node.attributes['enable-background'];
const attrCleaned = cleanupValue(
attrValue,
node.name,
node.attributes.width,
node.attributes.height,
);
if (attrCleaned) {
node.attributes['enable-background'] = attrCleaned;
} else {
delete node.attributes['enable-background'];
}
if (
newStyle?.type === 'DeclarationList' &&
enableBackgroundDeclaration
) {
const styleValue = csstree.generate(
// @ts-expect-error
enableBackgroundDeclaration.data.value,
);
const styleCleaned = cleanupValue(
styleValue,
node.name,
node.attributes.width,
node.attributes.height,
);
if (styleCleaned) {
// @ts-expect-error
enableBackgroundDeclaration.data.value = {
type: 'Raw',
value: styleCleaned,
};
} else {
newStyle.children.remove(enableBackgroundDeclaration);
}
}
}
if (newStyle?.type === 'DeclarationList') {
if (newStyle.children.isEmpty) {
delete node.attributes.style;
} else {
node.attributes.style = csstree.generate(newStyle);
}
}
},
},
};
};
/**
* @param {string} value Value of an enable-background attribute or style declaration.
* @param {string} nodeName Name of the node the value was assigned to.
* @param {string} width Width of the node the value was assigned to.
* @param {string} height Height of the node the value was assigned to.
* @returns {string | undefined} Cleaned up value, or undefined if it's redundant.
*/
const cleanupValue = (value, nodeName, width, height) => {
const match = regEnableBackground.exec(value);
if (match != null && width === match[1] && height === match[3]) {
return nodeName === 'svg' ? undefined : 'new';
}
return value;
};

View File

@@ -1,297 +0,0 @@
'use strict';
/**
* @typedef {import('../lib/types').XastElement} XastElement
*/
const { visitSkip } = require('../lib/xast.js');
const { referencesProps } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'cleanupIDs';
exports.active = true;
exports.description = 'removes unused IDs and minifies used';
const regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/;
const regReferencesHref = /^#(.+?)$/;
const regReferencesBegin = /(\w+)\./;
const generateIDchars = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
const maxIDindex = generateIDchars.length - 1;
/**
* Check if an ID starts with any one of a list of strings.
*
* @type {(string: string, prefixes: Array<string>) => boolean}
*/
const hasStringPrefix = (string, prefixes) => {
for (const prefix of prefixes) {
if (string.startsWith(prefix)) {
return true;
}
}
return false;
};
/**
* Generate unique minimal ID.
*
* @type {(currentID: null | Array<number>) => Array<number>}
*/
const generateID = (currentID) => {
if (currentID == null) {
return [0];
}
currentID[currentID.length - 1] += 1;
for (let i = currentID.length - 1; i > 0; i--) {
if (currentID[i] > maxIDindex) {
currentID[i] = 0;
if (currentID[i - 1] !== undefined) {
currentID[i - 1]++;
}
}
}
if (currentID[0] > maxIDindex) {
currentID[0] = 0;
currentID.unshift(0);
}
return currentID;
};
/**
* Get string from generated ID array.
*
* @type {(arr: Array<number>, prefix: string) => string}
*/
const getIDstring = (arr, prefix) => {
return prefix + arr.map((i) => generateIDchars[i]).join('');
};
/**
* Remove unused and minify used IDs
* (only if there are no any <style> or <script>).
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* remove?: boolean,
* minify?: boolean,
* prefix?: string,
* preserve?: Array<string>,
* preservePrefixes?: Array<string>,
* force?: boolean,
* }>}
*/
exports.fn = (_root, params) => {
const {
remove = true,
minify = true,
prefix = '',
preserve = [],
preservePrefixes = [],
force = false,
} = params;
const preserveIDs = new Set(
Array.isArray(preserve) ? preserve : preserve ? [preserve] : []
);
const preserveIDPrefixes = Array.isArray(preservePrefixes)
? preservePrefixes
: preservePrefixes
? [preservePrefixes]
: [];
/**
* @type {Map<string, XastElement>}
*/
const nodeById = new Map();
/**
* @type {Map<string, Array<{element: XastElement, name: string, value: string }>>}
*/
const referencesById = new Map();
let deoptimized = false;
return {
element: {
enter: (node) => {
if (force == false) {
// deoptimize if style or script elements are present
if (
(node.name === 'style' || node.name === 'script') &&
node.children.length !== 0
) {
deoptimized = true;
return;
}
// avoid removing IDs if the whole SVG consists only of defs
if (node.name === 'svg') {
let hasDefsOnly = true;
for (const child of node.children) {
if (child.type !== 'element' || child.name !== 'defs') {
hasDefsOnly = false;
break;
}
}
if (hasDefsOnly) {
return visitSkip;
}
}
}
for (const [name, value] of Object.entries(node.attributes)) {
if (name === 'id') {
// collect all ids
const id = value;
if (nodeById.has(id)) {
delete node.attributes.id; // remove repeated id
} else {
nodeById.set(id, node);
}
} else {
// collect all references
/**
* @type {null | string}
*/
let id = null;
if (referencesProps.includes(name)) {
const match = value.match(regReferencesUrl);
if (match != null) {
id = match[2]; // url() reference
}
}
if (name === 'href' || name.endsWith(':href')) {
const match = value.match(regReferencesHref);
if (match != null) {
id = match[1]; // href reference
}
}
if (name === 'begin') {
const match = value.match(regReferencesBegin);
if (match != null) {
id = match[1]; // href reference
}
}
if (id != null) {
let refs = referencesById.get(id);
if (refs == null) {
refs = [];
referencesById.set(id, refs);
}
refs.push({ element: node, name, value });
}
}
}
},
},
root: {
exit: () => {
if (deoptimized) {
return;
}
/**
* @type {(id: string) => boolean}
**/
const isIdPreserved = (id) =>
preserveIDs.has(id) || hasStringPrefix(id, preserveIDPrefixes);
/**
* @type {null | Array<number>}
*/
let currentID = null;
for (const [id, refs] of referencesById) {
const node = nodeById.get(id);
if (node != null) {
// replace referenced IDs with the minified ones
if (minify && isIdPreserved(id) === false) {
/**
* @type {null | string}
*/
let currentIDString = null;
do {
currentID = generateID(currentID);
currentIDString = getIDstring(currentID, prefix);
} while (isIdPreserved(currentIDString));
node.attributes.id = currentIDString;
for (const { element, name, value } of refs) {
if (value.includes('#')) {
// replace id in href and url()
element.attributes[name] = value.replace(
`#${id}`,
`#${currentIDString}`
);
} else {
// replace id in begin attribute
element.attributes[name] = value.replace(
`${id}.`,
`${currentIDString}.`
);
}
}
}
// keep referenced node
nodeById.delete(id);
}
}
// remove non-referenced IDs attributes from elements
if (remove) {
for (const [id, node] of nodeById) {
if (isIdPreserved(id) === false) {
delete node.attributes.id;
}
}
}
},
},
};
};

266
node_modules/svgo/plugins/cleanupIds.js generated vendored Normal file
View File

@@ -0,0 +1,266 @@
import { visitSkip } from '../lib/util/visit.js';
import { findReferences, hasScripts } from '../lib/svgo/tools.js';
/**
* @typedef CleanupIdsParams
* @property {boolean=} remove
* @property {boolean=} minify
* @property {string[]=} preserve
* @property {string[]=} preservePrefixes
* @property {boolean=} force
*/
export const name = 'cleanupIds';
export const description = 'removes unused IDs and minifies used';
const generateIdChars = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
const maxIdIndex = generateIdChars.length - 1;
/**
* Check if an ID starts with any one of a list of strings.
*
* @param {string} string
* @param {ReadonlyArray<string>} prefixes
* @returns {boolean}
*/
const hasStringPrefix = (string, prefixes) => {
for (const prefix of prefixes) {
if (string.startsWith(prefix)) {
return true;
}
}
return false;
};
/**
* Generate unique minimal ID.
*
* @param {?number[]} currentId
* @returns {number[]}
*/
const generateId = (currentId) => {
if (currentId == null) {
return [0];
}
currentId[currentId.length - 1] += 1;
for (let i = currentId.length - 1; i > 0; i--) {
if (currentId[i] > maxIdIndex) {
currentId[i] = 0;
if (currentId[i - 1] !== undefined) {
currentId[i - 1]++;
}
}
}
if (currentId[0] > maxIdIndex) {
currentId[0] = 0;
currentId.unshift(0);
}
return currentId;
};
/**
* Get string from generated ID array.
*
* @param {ReadonlyArray<number>} arr
* @returns {string}
*/
const getIdString = (arr) => {
return arr.map((i) => generateIdChars[i]).join('');
};
/**
* Remove unused and minify used IDs (only if there are no `<style>` or
* `<script>` nodes).
*
* @author Kir Belevich
*
* @type {import('../lib/types.js').Plugin<CleanupIdsParams>}
*/
export const fn = (_root, params) => {
const {
remove = true,
minify = true,
preserve = [],
preservePrefixes = [],
force = false,
} = params;
const preserveIds = new Set(
Array.isArray(preserve) ? preserve : preserve ? [preserve] : [],
);
const preserveIdPrefixes = Array.isArray(preservePrefixes)
? preservePrefixes
: preservePrefixes
? [preservePrefixes]
: [];
/** @type {Map<string, import('../lib/types.js').XastElement>} */
const nodeById = new Map();
/** @type {Map<string, {element: import('../lib/types.js').XastElement, name: string }[]>} */
const referencesById = new Map();
let deoptimized = false;
return {
element: {
enter: (node) => {
if (!force) {
// deoptimize if style or scripts are present
if (
(node.name === 'style' && node.children.length !== 0) ||
hasScripts(node)
) {
deoptimized = true;
return;
}
// avoid removing IDs if the whole SVG consists only of defs
if (node.name === 'svg') {
let hasDefsOnly = true;
for (const child of node.children) {
if (child.type !== 'element' || child.name !== 'defs') {
hasDefsOnly = false;
break;
}
}
if (hasDefsOnly) {
return visitSkip;
}
}
}
for (const [name, value] of Object.entries(node.attributes)) {
if (name === 'id') {
// collect all ids
const id = value;
if (nodeById.has(id)) {
delete node.attributes.id; // remove repeated id
} else {
nodeById.set(id, node);
}
} else {
const ids = findReferences(name, value);
for (const id of ids) {
let refs = referencesById.get(id);
if (refs == null) {
refs = [];
referencesById.set(id, refs);
}
refs.push({ element: node, name });
}
}
}
},
},
root: {
exit: () => {
if (deoptimized) {
return;
}
/**
* @param {string} id
* @returns {boolean}
*/
const isIdPreserved = (id) =>
preserveIds.has(id) || hasStringPrefix(id, preserveIdPrefixes);
/** @type {?number[]} */
let currentId = null;
for (const [id, refs] of referencesById) {
const node = nodeById.get(id);
if (node != null) {
// replace referenced IDs with the minified ones
if (minify && isIdPreserved(id) === false) {
/** @type {?string} */
let currentIdString;
do {
currentId = generateId(currentId);
currentIdString = getIdString(currentId);
} while (
isIdPreserved(currentIdString) ||
(referencesById.has(currentIdString) &&
nodeById.get(currentIdString) == null)
);
node.attributes.id = currentIdString;
for (const { element, name } of refs) {
const value = element.attributes[name];
if (value.includes('#')) {
// replace id in href and url()
element.attributes[name] = value
.replace(`#${encodeURI(id)}`, `#${currentIdString}`)
.replace(`#${id}`, `#${currentIdString}`);
} else {
// replace id in begin attribute
element.attributes[name] = value.replace(
`${id}.`,
`${currentIdString}.`,
);
}
}
}
// keep referenced node
nodeById.delete(id);
}
}
// remove non-referenced IDs attributes from elements
if (remove) {
for (const [id, node] of nodeById) {
if (isIdPreserved(id) === false) {
delete node.attributes.id;
}
}
}
},
},
};
};

View File

@@ -1,11 +1,15 @@
'use strict';
import { removeLeadingZero } from '../lib/svgo/tools.js';
const { removeLeadingZero } = require('../lib/svgo/tools.js');
/**
* @typedef CleanupListOfValuesParams
* @property {number=} floatPrecision
* @property {boolean=} leadingZero
* @property {boolean=} defaultPx
* @property {boolean=} convertToPx
*/
exports.name = 'cleanupListOfValues';
exports.type = 'visitor';
exports.active = false;
exports.description = 'rounds list of values to the fixed precision';
export const name = 'cleanupListOfValues';
export const description = 'rounds list of values to the fixed precision';
const regNumericValues =
/^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/;
@@ -25,23 +29,18 @@ const absoluteLengths = {
*
* @example
* <svg viewBox="0 0 200.28423 200.28423" enable-background="new 0 0 200.28423 200.28423">
*
* ⬇
* <svg viewBox="0 0 200.284 200.284" enable-background="new 0 0 200.284 200.284">
*
* <polygon points="208.250977 77.1308594 223.069336 ... "/>
*
* ⬇
* <polygon points="208.251 77.131 223.069 ... "/>
*
* @author kiyopikko
*
* @type {import('../lib/types').Plugin<{
* floatPrecision?: number,
* leadingZero?: boolean,
* defaultPx?: boolean,
* convertToPx?: boolean
* }>}
* @type {import('../lib/types.js').Plugin<CleanupListOfValuesParams>}
*/
exports.fn = (_root, params) => {
export const fn = (_root, params) => {
const {
floatPrecision = 3,
leadingZero = true,
@@ -50,7 +49,8 @@ exports.fn = (_root, params) => {
} = params;
/**
* @type {(lists: string) => string}
* @param {string} lists
* @returns {string}
*/
const roundValues = (lists) => {
const roundedList = [];
@@ -63,19 +63,15 @@ exports.fn = (_root, params) => {
if (match) {
// round it to the fixed precision
let num = Number(Number(match[1]).toFixed(floatPrecision));
/**
* @type {any}
*/
let matchedUnit = match[3] || '';
/**
* @type{'' | keyof typeof absoluteLengths}
*/
/** @type {any} */
const matchedUnit = match[3] || '';
/** @type {'' | keyof typeof absoluteLengths} */
let units = matchedUnit;
// convert absolute values to pixels
if (convertToPx && units && units in absoluteLengths) {
const pxNum = Number(
(absoluteLengths[units] * Number(match[1])).toFixed(floatPrecision)
(absoluteLengths[units] * Number(match[1])).toFixed(floatPrecision),
);
if (pxNum.toString().length < match[0].length) {
@@ -119,7 +115,7 @@ exports.fn = (_root, params) => {
if (node.attributes['enable-background'] != null) {
node.attributes['enable-background'] = roundValues(
node.attributes['enable-background']
node.attributes['enable-background'],
);
}
@@ -129,7 +125,7 @@ exports.fn = (_root, params) => {
if (node.attributes['stroke-dasharray'] != null) {
node.attributes['stroke-dasharray'] = roundValues(
node.attributes['stroke-dasharray']
node.attributes['stroke-dasharray'],
);
}

View File

@@ -1,12 +1,16 @@
'use strict';
import { removeLeadingZero } from '../lib/svgo/tools.js';
const { removeLeadingZero } = require('../lib/svgo/tools');
/**
* @typedef CleanupNumericValuesParams
* @property {number=} floatPrecision
* @property {boolean=} leadingZero
* @property {boolean=} defaultPx
* @property {boolean=} convertToPx
*/
exports.name = 'cleanupNumericValues';
exports.type = 'visitor';
exports.active = true;
exports.description =
'rounds numeric values to the fixed precision, removes default px units';
export const name = 'cleanupNumericValues';
export const description =
'rounds numeric values to the fixed precision, removes default "px" units';
const regNumericValues =
/^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/;
@@ -22,19 +26,13 @@ const absoluteLengths = {
};
/**
* Round numeric values to the fixed precision,
* remove default 'px' units.
* Round numeric values to the fixed precision, remove default 'px' units.
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* floatPrecision?: number,
* leadingZero?: boolean,
* defaultPx?: boolean,
* convertToPx?: boolean
* }>}
* @type {import('../lib/types.js').Plugin<CleanupNumericValuesParams>}
*/
exports.fn = (_root, params) => {
export const fn = (_root, params) => {
const {
floatPrecision = 3,
leadingZero = true,
@@ -46,7 +44,7 @@ exports.fn = (_root, params) => {
element: {
enter: (node) => {
if (node.attributes.viewBox != null) {
const nums = node.attributes.viewBox.split(/\s,?\s*|,\s*/g);
const nums = node.attributes.viewBox.trim().split(/(?:\s,?|,)\s*/g);
node.attributes.viewBox = nums
.map((value) => {
const num = Number(value);
@@ -63,27 +61,23 @@ exports.fn = (_root, params) => {
continue;
}
const match = value.match(regNumericValues);
const match = regNumericValues.exec(value);
// if attribute value matches regNumericValues
if (match) {
// round it to the fixed precision
let num = Number(Number(match[1]).toFixed(floatPrecision));
/**
* @type {any}
*/
let matchedUnit = match[3] || '';
/**
* @type{'' | keyof typeof absoluteLengths}
*/
/** @type {any} */
const matchedUnit = match[3] || '';
/** @type {'' | keyof typeof absoluteLengths} */
let units = matchedUnit;
// convert absolute values to pixels
if (convertToPx && units !== '' && units in absoluteLengths) {
const pxNum = Number(
(absoluteLengths[units] * Number(match[1])).toFixed(
floatPrecision
)
floatPrecision,
),
);
if (pxNum.toString().length < match[0].length) {
num = pxNum;

View File

@@ -1,23 +1,18 @@
'use strict';
import { collectStylesheet, computeStyle } from '../lib/style.js';
import { elemsGroups, inheritableAttrs } from './_collections.js';
export const name = 'collapseGroups';
export const description = 'collapses useless groups';
/**
* @typedef {import('../lib/types').XastNode} XastNode
*/
const { inheritableAttrs, elemsGroups } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'collapseGroups';
exports.active = true;
exports.description = 'collapses useless groups';
/**
* @type {(node: XastNode, name: string) => boolean}
* @param {import('../lib/types.js').XastNode} node
* @param {string} name
* @returns {boolean}
*/
const hasAnimatedAttr = (node, name) => {
if (node.type === 'element') {
if (
elemsGroups.animation.includes(node.name) &&
elemsGroups.animation.has(node.name) &&
node.attributes.attributeName === name
) {
return true;
@@ -40,20 +35,22 @@ const hasAnimatedAttr = (node, name) => {
* <path d="..."/>
* </g>
* </g>
*
* ⬇
* <g>
* <g>
* <path attr1="val1" d="..."/>
* </g>
* </g>
*
* ⬇
* <path attr1="val1" d="..."/>
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = (root) => {
const stylesheet = collectStylesheet(root);
return {
element: {
exit: (node, parentNode) => {
@@ -65,17 +62,20 @@ exports.fn = () => {
return;
}
// move group attibutes to the single child element
// move group attributes to the single child element
if (
Object.keys(node.attributes).length !== 0 &&
node.children.length === 1
) {
const firstChild = node.children[0];
const nodeHasFilter = !!(
node.attributes.filter || computeStyle(stylesheet, node).filter
);
// TODO untangle this mess
if (
firstChild.type === 'element' &&
firstChild.attributes.id == null &&
node.attributes.filter == null &&
!nodeHasFilter &&
(node.attributes.class == null ||
firstChild.attributes.class == null) &&
((node.attributes['clip-path'] == null &&
@@ -84,26 +84,30 @@ exports.fn = () => {
node.attributes.transform == null &&
firstChild.attributes.transform == null))
) {
const newChildElemAttrs = { ...firstChild.attributes };
for (const [name, value] of Object.entries(node.attributes)) {
// avoid copying to not conflict with animated attribute
if (hasAnimatedAttr(firstChild, name)) {
return;
}
if (firstChild.attributes[name] == null) {
firstChild.attributes[name] = value;
if (newChildElemAttrs[name] == null) {
newChildElemAttrs[name] = value;
} else if (name === 'transform') {
firstChild.attributes[name] =
value + ' ' + firstChild.attributes[name];
} else if (firstChild.attributes[name] === 'inherit') {
firstChild.attributes[name] = value;
newChildElemAttrs[name] = value + ' ' + newChildElemAttrs[name];
} else if (newChildElemAttrs[name] === 'inherit') {
newChildElemAttrs[name] = value;
} else if (
inheritableAttrs.includes(name) === false &&
firstChild.attributes[name] !== value
!inheritableAttrs.has(name) &&
newChildElemAttrs[name] !== value
) {
return;
}
delete node.attributes[name];
}
node.attributes = {};
firstChild.attributes = newChildElemAttrs;
}
}
@@ -114,7 +118,7 @@ exports.fn = () => {
for (const child of node.children) {
if (
child.type === 'element' &&
elemsGroups.animation.includes(child.name)
elemsGroups.animation.has(child.name)
) {
return;
}
@@ -122,12 +126,6 @@ exports.fn = () => {
// replace current node with all its children
const index = parentNode.children.indexOf(node);
parentNode.children.splice(index, 1, ...node.children);
// TODO remove in v3
for (const child of node.children) {
// @ts-ignore parentNode is forbidden for public usage
// and will be moved in v3
child.parentNode = parentNode;
}
}
},
},

View File

@@ -1,16 +1,24 @@
'use strict';
import { colorsNames, colorsProps, colorsShortNames } from './_collections.js';
import { includesUrlReference } from '../lib/svgo/tools.js';
const collections = require('./_collections.js');
/**
* @typedef ConvertColorsParams
* @property {boolean | string | RegExp=} currentColor
* @property {boolean=} names2hex
* @property {boolean=} rgb2hex
* @property {false | 'lower' | 'upper'=} convertCase
* @property {boolean=} shorthex
* @property {boolean=} shortname
*/
exports.type = 'visitor';
exports.name = 'convertColors';
exports.active = true;
exports.description = 'converts colors: rgb() to #rrggbb and #rrggbb to #rgb';
export const name = 'convertColors';
export const description =
'converts colors: rgb() to #rrggbb and #rrggbb to #rgb';
const rNumber = '([+-]?(?:\\d*\\.\\d+|\\d+\\.?)%?)';
const rComma = '\\s*,\\s*';
const rComma = '(?:\\s*,\\s*|\\s+)';
const regRGB = new RegExp(
'^rgb\\(\\s*' + rNumber + rComma + rNumber + rComma + rNumber + '\\s*\\)$'
'^rgb\\(\\s*' + rNumber + rComma + rNumber + rComma + rNumber + '\\s*\\)$',
);
const regHEX = /^#(([a-fA-F0-9])\2){3}$/;
@@ -24,7 +32,8 @@ const regHEX = /^#(([a-fA-F0-9])\2){3}$/;
*
* @author Jed Schmidt
*
* @type {(rgb: Array<number>) => string}
* @param {ReadonlyArray<number>} param0
* @returns {string}
*/
const convertRgbToHex = ([r, g, b]) => {
// combine the octets into a 32-bit integer as: [1][r][g][b]
@@ -63,32 +72,32 @@ const convertRgbToHex = ([r, g, b]) => {
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* currentColor?: boolean | string | RegExp,
* names2hex?: boolean,
* rgb2hex?: boolean,
* shorthex?: boolean,
* shortname?: boolean,
* }>}
* @type {import('../lib/types.js').Plugin<ConvertColorsParams>}
*/
exports.fn = (_root, params) => {
export const fn = (_root, params) => {
const {
currentColor = false,
names2hex = true,
rgb2hex = true,
convertCase = 'lower',
shorthex = true,
shortname = true,
} = params;
let maskCounter = 0;
return {
element: {
enter: (node) => {
if (node.name === 'mask') {
maskCounter++;
}
for (const [name, value] of Object.entries(node.attributes)) {
if (collections.colorsProps.includes(name)) {
if (colorsProps.has(name)) {
let val = value;
// convert colors to currentColor
if (currentColor) {
if (currentColor && maskCounter === 0) {
let matched;
if (typeof currentColor === 'string') {
matched = val === currentColor;
@@ -105,16 +114,16 @@ exports.fn = (_root, params) => {
// convert color name keyword to long hex
if (names2hex) {
const colorName = val.toLowerCase();
if (collections.colorsNames[colorName] != null) {
val = collections.colorsNames[colorName];
if (colorsNames[colorName] != null) {
val = colorsNames[colorName];
}
}
// convert rgb() to long hex
if (rgb2hex) {
let match = val.match(regRGB);
const match = val.match(regRGB);
if (match != null) {
let nums = match.slice(1, 4).map((m) => {
const nums = match.slice(1, 4).map((m) => {
let n;
if (m.indexOf('%') > -1) {
n = Math.round(parseFloat(m) * 2.55);
@@ -127,9 +136,21 @@ exports.fn = (_root, params) => {
}
}
if (
convertCase &&
!includesUrlReference(val) &&
val !== 'currentColor'
) {
if (convertCase === 'lower') {
val = val.toLowerCase();
} else if (convertCase === 'upper') {
val = val.toUpperCase();
}
}
// convert long hex to short hex
if (shorthex) {
let match = val.match(regHEX);
const match = regHEX.exec(val);
if (match != null) {
val = '#' + match[0][1] + match[0][3] + match[0][5];
}
@@ -138,8 +159,8 @@ exports.fn = (_root, params) => {
// convert hex to short name
if (shortname) {
const colorName = val.toLowerCase();
if (collections.colorsShortNames[colorName] != null) {
val = collections.colorsShortNames[colorName];
if (colorsShortNames[colorName] != null) {
val = colorsShortNames[colorName];
}
}
@@ -147,6 +168,11 @@ exports.fn = (_root, params) => {
}
}
},
exit: (node) => {
if (node.name === 'mask') {
maskCounter--;
}
},
},
};
};

View File

@@ -1,9 +1,5 @@
'use strict';
exports.name = 'convertEllipseToCircle';
exports.type = 'visitor';
exports.active = true;
exports.description = 'converts non-eccentric <ellipse>s to <circle>s';
export const name = 'convertEllipseToCircle';
export const description = 'converts non-eccentric <ellipse>s to <circle>s';
/**
* Converts non-eccentric <ellipse>s to <circle>s.
@@ -12,9 +8,9 @@ exports.description = 'converts non-eccentric <ellipse>s to <circle>s';
*
* @author Taylor Hunt
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node) => {

157
node_modules/svgo/plugins/convertOneStopGradients.js generated vendored Normal file
View File

@@ -0,0 +1,157 @@
import { attrsGroupsDefaults, colorsProps } from './_collections.js';
import {
detachNodeFromParent,
querySelector,
querySelectorAll,
} from '../lib/xast.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';
export const name = 'convertOneStopGradients';
export const description =
'converts one-stop (single color) gradients to a plain color';
/**
* Converts one-stop (single color) gradients to a plain color.
*
* @author Seth Falco <seth@falco.fun>
* @type {import('../lib/types.js').Plugin}
* @see https://developer.mozilla.org/docs/Web/SVG/Element/linearGradient
* @see https://developer.mozilla.org/docs/Web/SVG/Element/radialGradient
*/
export const fn = (root) => {
const stylesheet = collectStylesheet(root);
/**
* Parent defs that had gradients elements removed from them.
*
* @type {Set<import('../lib/types.js').XastElement>}
*/
const effectedDefs = new Set();
/** @type {Map<import('../lib/types.js').XastElement, import('../lib/types.js').XastParent>} */
const allDefs = new Map();
/** @type {Map<import('../lib/types.js').XastElement, import('../lib/types.js').XastParent>} */
const gradientsToDetach = new Map();
/** Number of references to the xlink:href attribute. */
let xlinkHrefCount = 0;
return {
element: {
enter: (node, parentNode) => {
if (node.attributes['xlink:href'] != null) {
xlinkHrefCount++;
}
if (node.name === 'defs') {
allDefs.set(node, parentNode);
return;
}
if (node.name !== 'linearGradient' && node.name !== 'radialGradient') {
return;
}
const stops = node.children.filter((child) => {
return child.type === 'element' && child.name === 'stop';
});
const href = node.attributes['xlink:href'] || node.attributes['href'];
const effectiveNode =
stops.length === 0 && href != null && href.startsWith('#')
? querySelector(root, href)
: node;
if (effectiveNode == null || effectiveNode.type !== 'element') {
gradientsToDetach.set(node, parentNode);
return;
}
const effectiveStops = effectiveNode.children.filter((child) => {
return child.type === 'element' && child.name === 'stop';
});
if (
effectiveStops.length !== 1 ||
effectiveStops[0].type !== 'element'
) {
return;
}
if (parentNode.type === 'element' && parentNode.name === 'defs') {
effectedDefs.add(parentNode);
}
gradientsToDetach.set(node, parentNode);
let color;
const style = computeStyle(stylesheet, effectiveStops[0])['stop-color'];
if (style != null && style.type === 'static') {
color = style.value;
}
const selectorVal = `url(#${node.attributes.id})`;
const selector = [...colorsProps]
.map((attr) => `[${attr}="${selectorVal}"]`)
.join(',');
const elements = querySelectorAll(root, selector);
for (const element of elements) {
if (element.type !== 'element') {
continue;
}
for (const attr of colorsProps) {
if (element.attributes[attr] !== selectorVal) {
continue;
}
if (color != null) {
element.attributes[attr] = color;
} else {
delete element.attributes[attr];
}
}
}
const styledElements = querySelectorAll(
root,
`[style*=${selectorVal}]`,
);
for (const element of styledElements) {
if (element.type !== 'element') {
continue;
}
element.attributes.style = element.attributes.style.replace(
selectorVal,
color || attrsGroupsDefaults.presentation['stop-color'],
);
}
},
exit: (node) => {
if (node.name === 'svg') {
for (const [gradient, parent] of gradientsToDetach.entries()) {
if (gradient.attributes['xlink:href'] != null) {
xlinkHrefCount--;
}
detachNodeFromParent(gradient, parent);
}
if (xlinkHrefCount === 0) {
delete node.attributes['xmlns:xlink'];
}
for (const [defs, parent] of allDefs.entries()) {
if (effectedDefs.has(defs) && defs.children.length === 0) {
detachNodeFromParent(defs, parent);
}
}
}
},
},
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,28 @@
'use strict';
import { stringifyPathData } from '../lib/path.js';
import { detachNodeFromParent } from '../lib/xast.js';
/**
* @typedef {import('../lib/types').PathDataItem} PathDataItem
* @typedef ConvertShapeToPathParams
* @property {boolean=} convertArcs
* @property {number=} floatPrecision
*/
const { stringifyPathData } = require('../lib/path.js');
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'convertShapeToPath';
exports.type = 'visitor';
exports.active = true;
exports.description = 'converts basic shapes to more compact path form';
export const name = 'convertShapeToPath';
export const description = 'converts basic shapes to more compact path form';
const regNumber = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
/**
* Converts basic shape to more compact path.
* It also allows further optimizations like
* combining paths with similar attributes.
* Converts basic shape to more compact path. It also allows further
* optimizations like combining paths with similar attributes.
*
* @see https://www.w3.org/TR/SVG11/shapes.html
*
* @author Lev Solntsev
*
* @type {import('../lib/types').Plugin<{
* convertArcs?: boolean,
* floatPrecision?: number
* }>}
* @type {import('../lib/types.js').Plugin<ConvertShapeToPathParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const { convertArcs = false, floatPrecision: precision } = params;
return {
@@ -49,10 +43,10 @@ exports.fn = (root, params) => {
// Values like '100%' compute to NaN, thus running after
// cleanupNumericValues when 'px' units has already been removed.
// TODO: Calculate sizes from % and non-px units if possible.
if (Number.isNaN(x - y + width - height)) return;
/**
* @type {Array<PathDataItem>}
*/
if (Number.isNaN(x - y + width - height)) {
return;
}
/** @type {import('../lib/types.js').PathDataItem[]} */
const pathData = [
{ command: 'M', args: [x, y] },
{ command: 'H', args: [x + width] },
@@ -74,10 +68,10 @@ exports.fn = (root, params) => {
const y1 = Number(node.attributes.y1 || '0');
const x2 = Number(node.attributes.x2 || '0');
const y2 = Number(node.attributes.y2 || '0');
if (Number.isNaN(x1 - y1 + x2 - y2)) return;
/**
* @type {Array<PathDataItem>}
*/
if (Number.isNaN(x1 - y1 + x2 - y2)) {
return;
}
/** @type {import('../lib/types.js').PathDataItem[]} */
const pathData = [
{ command: 'M', args: [x1, y1] },
{ command: 'L', args: [x2, y2] },
@@ -96,15 +90,13 @@ exports.fn = (root, params) => {
node.attributes.points != null
) {
const coords = (node.attributes.points.match(regNumber) || []).map(
Number
Number,
);
if (coords.length < 4) {
detachNodeFromParent(node, parentNode);
return;
}
/**
* @type {Array<PathDataItem>}
*/
/** @type {import('../lib/types.js').PathDataItem[]} */
const pathData = [];
for (let i = 0; i < coords.length; i += 2) {
pathData.push({
@@ -128,9 +120,7 @@ exports.fn = (root, params) => {
if (Number.isNaN(cx - cy + r)) {
return;
}
/**
* @type {Array<PathDataItem>}
*/
/** @type {import('../lib/types.js').PathDataItem[]} */
const pathData = [
{ command: 'M', args: [cx, cy - r] },
{ command: 'A', args: [r, r, 0, 1, 0, cx, cy + r] },
@@ -144,7 +134,7 @@ exports.fn = (root, params) => {
delete node.attributes.r;
}
// optionally covert ellipse
// optionally convert ellipse
if (node.name === 'ellipse' && convertArcs) {
const ecx = Number(node.attributes.cx || '0');
const ecy = Number(node.attributes.cy || '0');
@@ -153,9 +143,7 @@ exports.fn = (root, params) => {
if (Number.isNaN(ecx - ecy + rx - ry)) {
return;
}
/**
* @type {Array<PathDataItem>}
*/
/** @type {import('../lib/types.js').PathDataItem[]} */
const pathData = [
{ command: 'M', args: [ecx, ecy - ry] },
{ command: 'A', args: [rx, ry, 0, 1, 0, ecx, ecy + ry] },

View File

@@ -1,132 +1,145 @@
'use strict';
import { attrsGroups } from './_collections.js';
exports.name = 'convertStyleToAttrs';
/**
* @typedef ConvertStyleToAttrsParams
* @property {boolean=} keepImportant
*/
exports.type = 'perItem';
export const name = 'convertStyleToAttrs';
export const description = 'converts style to attributes';
exports.active = false;
exports.description = 'converts style to attributes';
exports.params = {
keepImportant: false,
/**
* @param {...string} args
* @returns {string}
*/
const g = (...args) => {
return '(?:' + args.join('|') + ')';
};
var stylingProps = require('./_collections').attrsGroups.presentation,
rEscape = '\\\\(?:[0-9a-f]{1,6}\\s?|\\r\\n|.)', // Like \" or \2051. Code points consume one space.
rAttr = '\\s*(' + g('[^:;\\\\]', rEscape) + '*?)\\s*', // attribute name like fill
rSingleQuotes = "'(?:[^'\\n\\r\\\\]|" + rEscape + ")*?(?:'|$)", // string in single quotes: 'smth'
rQuotes = '"(?:[^"\\n\\r\\\\]|' + rEscape + ')*?(?:"|$)', // string in double quotes: "smth"
rQuotedString = new RegExp('^' + g(rSingleQuotes, rQuotes) + '$'),
// Parentheses, E.g.: url(data:image/png;base64,iVBO...).
// ':' and ';' inside of it should be threated as is. (Just like in strings.)
rParenthesis =
'\\(' + g('[^\'"()\\\\]+', rEscape, rSingleQuotes, rQuotes) + '*?' + '\\)',
// The value. It can have strings and parentheses (see above). Fallbacks to anything in case of unexpected input.
rValue =
'\\s*(' +
g(
'[^!\'"();\\\\]+?',
rEscape,
rSingleQuotes,
rQuotes,
rParenthesis,
'[^;]*?'
) +
'*?' +
')',
// End of declaration. Spaces outside of capturing groups help to do natural trimming.
rDeclEnd = '\\s*(?:;\\s*|$)',
// Important rule
rImportant = '(\\s*!important(?![-(\\w]))?',
// Final RegExp to parse CSS declarations.
regDeclarationBlock = new RegExp(
rAttr + ':' + rValue + rImportant + rDeclEnd,
'ig'
),
// Comments expression. Honors escape sequences and strings.
regStripComments = new RegExp(
g(rEscape, rSingleQuotes, rQuotes, '/\\*[^]*?\\*/'),
'ig'
);
const stylingProps = attrsGroups.presentation;
const rEscape = '\\\\(?:[0-9a-f]{1,6}\\s?|\\r\\n|.)'; // Like \" or \2051. Code points consume one space.
/** Pattern to match attribute name like: 'fill' */
const rAttr = '\\s*(' + g('[^:;\\\\]', rEscape) + '*?)\\s*';
/** Pattern to match string in single quotes like: 'foo' */
const rSingleQuotes = "'(?:[^'\\n\\r\\\\]|" + rEscape + ")*?(?:'|$)";
/** Pattern to match string in double quotes like: "foo" */
const rQuotes = '"(?:[^"\\n\\r\\\\]|' + rEscape + ')*?(?:"|$)';
const rQuotedString = new RegExp('^' + g(rSingleQuotes, rQuotes) + '$');
// Parentheses, E.g.: url(data:image/png;base64,iVBO...).
// ':' and ';' inside of it should be treated as is. (Just like in strings.)
const rParenthesis =
'\\(' + g('[^\'"()\\\\]+', rEscape, rSingleQuotes, rQuotes) + '*?' + '\\)';
// The value. It can have strings and parentheses (see above). Fallbacks to anything in case of unexpected input.
const rValue =
'\\s*(' +
g(
'[^!\'"();\\\\]+?',
rEscape,
rSingleQuotes,
rQuotes,
rParenthesis,
'[^;]*?',
) +
'*?' +
')';
// End of declaration. Spaces outside of capturing groups help to do natural trimming.
const rDeclEnd = '\\s*(?:;\\s*|$)';
// Important rule
const rImportant = '(\\s*!important(?![-(\\w]))?';
// Final RegExp to parse CSS declarations.
const regDeclarationBlock = new RegExp(
rAttr + ':' + rValue + rImportant + rDeclEnd,
'ig',
);
// Comments expression. Honors escape sequences and strings.
const regStripComments = new RegExp(
g(rEscape, rSingleQuotes, rQuotes, '/\\*[^]*?\\*/'),
'ig',
);
/**
* Convert style in attributes. Cleanups comments and illegal declarations (without colon) as a side effect.
*
* @example
* <g style="fill:#000; color: #fff;">
*
* ⬇
* <g fill="#000" color="#fff">
*
* @example
* <g style="fill:#000; color: #fff; -webkit-blah: blah">
*
* ⬇
* <g fill="#000" color="#fff" style="-webkit-blah: blah">
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*
* @type {import('../lib/types.js').Plugin<ConvertStyleToAttrsParams>}
*/
exports.fn = function (item, params) {
if (item.type === 'element' && item.attributes.style != null) {
// ['opacity: 1', 'color: #000']
let styles = [];
const newAttributes = {};
export const fn = (_root, params) => {
const { keepImportant = false } = params;
return {
element: {
enter: (node) => {
if (node.attributes.style != null) {
// ['opacity: 1', 'color: #000']
let styles = [];
/** @type {Record<string, string>} */
const newAttributes = {};
// Strip CSS comments preserving escape sequences and strings.
const styleValue = item.attributes.style.replace(
regStripComments,
(match) => {
return match[0] == '/'
? ''
: match[0] == '\\' && /[-g-z]/i.test(match[1])
? match[1]
: match;
}
);
// Strip CSS comments preserving escape sequences and strings.
const styleValue = node.attributes.style.replace(
regStripComments,
(match) => {
return match[0] == '/'
? ''
: match[0] == '\\' && /[-g-z]/i.test(match[1])
? match[1]
: match;
},
);
regDeclarationBlock.lastIndex = 0;
// eslint-disable-next-line no-cond-assign
for (var rule; (rule = regDeclarationBlock.exec(styleValue)); ) {
if (!params.keepImportant || !rule[3]) {
styles.push([rule[1], rule[2]]);
}
}
if (styles.length) {
styles = styles.filter(function (style) {
if (style[0]) {
var prop = style[0].toLowerCase(),
val = style[1];
if (rQuotedString.test(val)) {
val = val.slice(1, -1);
regDeclarationBlock.lastIndex = 0;
for (var rule; (rule = regDeclarationBlock.exec(styleValue)); ) {
if (!keepImportant || !rule[3]) {
styles.push([rule[1], rule[2]]);
}
}
if (stylingProps.includes(prop)) {
newAttributes[prop] = val;
if (styles.length) {
styles = styles.filter(function (style) {
if (style[0]) {
const prop = style[0].toLowerCase();
let val = style[1];
return false;
if (rQuotedString.test(val)) {
val = val.slice(1, -1);
}
if (stylingProps.has(prop)) {
newAttributes[prop] = val;
return false;
}
}
return true;
});
Object.assign(node.attributes, newAttributes);
if (styles.length) {
node.attributes.style = styles
.map((declaration) => declaration.join(':'))
.join(';');
} else {
delete node.attributes.style;
}
}
}
return true;
});
Object.assign(item.attributes, newAttributes);
if (styles.length) {
item.attributes.style = styles
.map((declaration) => declaration.join(':'))
.join(';');
} else {
delete item.attributes.style;
}
}
}
},
},
};
};
function g() {
return '(?:' + Array.prototype.join.call(arguments, '|') + ')';
}

View File

@@ -1,20 +1,48 @@
'use strict';
/**
* @typedef {import('../lib/types').XastElement} XastElement
*/
const { cleanupOutData } = require('../lib/svgo/tools.js');
const {
import {
js2transform,
matrixToTransform,
roundTransform,
transform2js,
transformsMultiply,
matrixToTransform,
} = require('./_transforms.js');
} from './_transforms.js';
exports.type = 'visitor';
exports.name = 'convertTransform';
exports.active = true;
exports.description = 'collapses multiple transformations and optimizes it';
/**
* @typedef ConvertTransformParams
* @property {boolean=} convertToShorts
* @property {number=} degPrecision
* @property {number=} floatPrecision
* @property {number=} transformPrecision
* @property {boolean=} matrixToTransform
* @property {boolean=} shortTranslate
* @property {boolean=} shortScale
* @property {boolean=} shortRotate
* @property {boolean=} removeUseless
* @property {boolean=} collapseIntoOne
* @property {boolean=} leadingZero
* @property {boolean=} negativeExtraSpace
*
* @typedef TransformParams
* @property {boolean} convertToShorts
* @property {number=} degPrecision
* @property {number} floatPrecision
* @property {number} transformPrecision
* @property {boolean} matrixToTransform
* @property {boolean} shortTranslate
* @property {boolean} shortScale
* @property {boolean} shortRotate
* @property {boolean} removeUseless
* @property {boolean} collapseIntoOne
* @property {boolean} leadingZero
* @property {boolean} negativeExtraSpace
*
* @typedef TransformItem
* @property {string} name
* @property {number[]} data
*/
export const name = 'convertTransform';
export const description =
'collapses multiple transformations and optimizes it';
/**
* Convert matrices to the short aliases,
@@ -26,22 +54,9 @@ exports.description = 'collapses multiple transformations and optimizes it';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* convertToShorts?: boolean,
* degPrecision?: number,
* floatPrecision?: number,
* transformPrecision?: number,
* matrixToTransform?: boolean,
* shortTranslate?: boolean,
* shortScale?: boolean,
* shortRotate?: boolean,
* removeUseless?: boolean,
* collapseIntoOne?: boolean,
* leadingZero?: boolean,
* negativeExtraSpace?: boolean,
* }>}
* @type {import('../lib/types.js').Plugin<ConvertTransformParams>}
*/
exports.fn = (_root, params) => {
export const fn = (_root, params) => {
const {
convertToShorts = true,
// degPrecision = 3, // transformPrecision (or matrix precision) - 2 by default
@@ -74,15 +89,14 @@ exports.fn = (_root, params) => {
return {
element: {
enter: (node) => {
// transform
if (node.attributes.transform != null) {
convertTransform(node, 'transform', newParams);
}
// gradientTransform
if (node.attributes.gradientTransform != null) {
convertTransform(node, 'gradientTransform', newParams);
}
// patternTransform
if (node.attributes.patternTransform != null) {
convertTransform(node, 'patternTransform', newParams);
}
@@ -92,30 +106,9 @@ exports.fn = (_root, params) => {
};
/**
* @typedef {{
* convertToShorts: boolean,
* degPrecision?: number,
* floatPrecision: number,
* transformPrecision: number,
* matrixToTransform: boolean,
* shortTranslate: boolean,
* shortScale: boolean,
* shortRotate: boolean,
* removeUseless: boolean,
* collapseIntoOne: boolean,
* leadingZero: boolean,
* negativeExtraSpace: boolean,
* }} TransformParams
*/
/**
* @typedef {{ name: string, data: Array<number> }} TransformItem
*/
/**
* Main function.
*
* @type {(item: XastElement, attrName: string, params: TransformParams) => void}
* @param {import('../lib/types.js').XastElement} item
* @param {string} attrName
* @param {TransformParams} params
*/
const convertTransform = (item, attrName, params) => {
let data = transform2js(item.attributes[attrName]);
@@ -144,14 +137,17 @@ const convertTransform = (item, attrName, params) => {
/**
* Defines precision to work with certain parts.
* transformPrecision - for scale and four first matrix parameters (needs a better precision due to multiplying),
* floatPrecision - for translate including two last matrix and rotate parameters,
* degPrecision - for rotate and skew. By default it's equal to (rougly)
* transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params.
*
* @type {(data: Array<TransformItem>, params: TransformParams) => TransformParams}
* - `transformPrecision` - for scale and four first matrix parameters (needs a better precision due to multiplying).
* - `floatPrecision` - for translate including two last matrix and rotate parameters.
* - `degPrecision` - for rotate and skew. By default it's equal to (roughly).
* - `transformPrecision` - 2 or floatPrecision whichever is lower. Can be set in params.
*
* clone params so it don't affect other elements transformations.
* Clone parameters so that it doesn't affect other element transformations.
*
* @param {ReadonlyArray<TransformItem>} data
* @param {TransformParams} param1
* @returns {TransformParams}
*/
const definePrecision = (data, { ...newParams }) => {
const matrixData = [];
@@ -160,71 +156,37 @@ const definePrecision = (data, { ...newParams }) => {
matrixData.push(...item.data.slice(0, 4));
}
}
let significantDigits = newParams.transformPrecision;
let numberOfDigits = newParams.transformPrecision;
// Limit transform precision with matrix one. Calculating with larger precision doesn't add any value.
if (matrixData.length) {
newParams.transformPrecision = Math.min(
newParams.transformPrecision,
Math.max.apply(Math, matrixData.map(floatDigits)) ||
newParams.transformPrecision
newParams.transformPrecision,
);
significantDigits = Math.max.apply(
numberOfDigits = Math.max.apply(
Math,
matrixData.map(
(n) => n.toString().replace(/\D+/g, '').length // Number of digits in a number. 123.45 → 5
)
(n) => n.toString().replace(/\D+/g, '').length, // Number of digits in a number. 123.45 → 5
),
);
}
// No sense in angle precision more then number of significant digits in matrix.
// No sense in angle precision more than number of significant digits in matrix.
if (newParams.degPrecision == null) {
newParams.degPrecision = Math.max(
0,
Math.min(newParams.floatPrecision, significantDigits - 2)
Math.min(newParams.floatPrecision, numberOfDigits - 2),
);
}
return newParams;
};
/**
* @type {(data: Array<number>, params: TransformParams) => Array<number>}
*/
const degRound = (data, params) => {
if (
params.degPrecision != null &&
params.degPrecision >= 1 &&
params.floatPrecision < 20
) {
return smartRound(params.degPrecision, data);
} else {
return round(data);
}
};
/**
* @type {(data: Array<number>, params: TransformParams) => Array<number>}
*/
const floatRound = (data, params) => {
if (params.floatPrecision >= 1 && params.floatPrecision < 20) {
return smartRound(params.floatPrecision, data);
} else {
return round(data);
}
};
/**
* @type {(data: Array<number>, params: TransformParams) => Array<number>}
*/
const transformRound = (data, params) => {
if (params.transformPrecision >= 1 && params.floatPrecision < 20) {
return smartRound(params.transformPrecision, data);
} else {
return round(data);
}
};
/**
* Returns number of digits after the point. 0.125 → 3
* Returns number of digits after the point.
*
* @type {(n: number) => number}
* @example 0.125 → 3
* @param {number} n
* @returns {number}
*/
const floatDigits = (n) => {
const str = n.toString();
@@ -234,15 +196,17 @@ const floatDigits = (n) => {
/**
* Convert transforms to the shorthand alternatives.
*
* @type {(transforms: Array<TransformItem>, params: TransformParams) => Array<TransformItem>}
* @param {TransformItem[]} transforms
* @param {TransformParams} params
* @returns {TransformItem[]}
*/
const convertToShorts = (transforms, params) => {
for (var i = 0; i < transforms.length; i++) {
var transform = transforms[i];
for (let i = 0; i < transforms.length; i++) {
let transform = transforms[i];
// convert matrix to the short aliases
if (params.matrixToTransform && transform.name === 'matrix') {
var decomposed = matrixToTransform(transform, params);
const decomposed = matrixToTransform(transform, params);
if (
js2transform(decomposed, params).length <=
js2transform([transform], params).length
@@ -282,8 +246,7 @@ const convertToShorts = (transforms, params) => {
// translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy)
if (
params.shortRotate &&
transforms[i - 2] &&
transforms[i - 2].name === 'translate' &&
transforms[i - 2]?.name === 'translate' &&
transforms[i - 1].name === 'rotate' &&
transforms[i].name === 'translate' &&
transforms[i - 2].data[0] === -transforms[i].data[0] &&
@@ -309,7 +272,8 @@ const convertToShorts = (transforms, params) => {
/**
* Remove useless transforms.
*
* @type {(trasforms: Array<TransformItem>) => Array<TransformItem>}
* @param {ReadonlyArray<TransformItem>} transforms
* @returns {TransformItem[]}
*/
const removeUseless = (transforms) => {
return transforms.filter((transform) => {
@@ -343,90 +307,3 @@ const removeUseless = (transforms) => {
return true;
});
};
/**
* Convert transforms JS representation to string.
*
* @type {(transformJS: Array<TransformItem>, params: TransformParams) => string}
*/
const js2transform = (transformJS, params) => {
var transformString = '';
// collect output value string
transformJS.forEach((transform) => {
roundTransform(transform, params);
transformString +=
(transformString && ' ') +
transform.name +
'(' +
cleanupOutData(transform.data, params) +
')';
});
return transformString;
};
/**
* @type {(transform: TransformItem, params: TransformParams) => TransformItem}
*/
const roundTransform = (transform, params) => {
switch (transform.name) {
case 'translate':
transform.data = floatRound(transform.data, params);
break;
case 'rotate':
transform.data = [
...degRound(transform.data.slice(0, 1), params),
...floatRound(transform.data.slice(1), params),
];
break;
case 'skewX':
case 'skewY':
transform.data = degRound(transform.data, params);
break;
case 'scale':
transform.data = transformRound(transform.data, params);
break;
case 'matrix':
transform.data = [
...transformRound(transform.data.slice(0, 4), params),
...floatRound(transform.data.slice(4), params),
];
break;
}
return transform;
};
/**
* Rounds numbers in array.
*
* @type {(data: Array<number>) => Array<number>}
*/
const round = (data) => {
return data.map(Math.round);
};
/**
* Decrease accuracy of floating-point numbers
* in transforms keeping a specified number of decimals.
* Smart rounds values like 2.349 to 2.35.
*
* @type {(precision: number, data: Array<number>) => Array<number>}
*/
const smartRound = (precision, data) => {
for (
var i = data.length,
tolerance = +Math.pow(0.1, precision).toFixed(precision);
i--;
) {
if (Number(data[i].toFixed(precision)) !== data[i]) {
var rounded = +data[i].toFixed(precision - 1);
data[i] =
+Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance
? +data[i].toFixed(precision)
: rounded;
}
}
return data;
};

View File

@@ -1,72 +1,48 @@
'use strict';
import * as csstree from 'css-tree';
import { syntax } from 'csso';
import { attrsGroups, pseudoClasses } from './_collections.js';
import { detachNodeFromParent, querySelectorAll } from '../lib/xast.js';
import { visitSkip } from '../lib/util/visit.js';
import { compareSpecificity, includesAttrSelector } from '../lib/style.js';
/**
* @typedef {import('../lib/types').Specificity} Specificity
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').XastParent} XastParent
* @typedef InlineStylesParams
* @property {boolean=} onlyMatchedOnce Inlines selectors that match once only.
* @property {boolean=} removeMatchedSelectors
* Clean up matched selectors. Unused selects are left as-is.
* @property {string[]=} useMqs
* Media queries to use. An empty string indicates all selectors outside of
* media queries.
* @property {string[]=} usePseudos
* Pseudo-classes and elements to use. An empty string indicates all
* non-pseudo-classes and elements.
*/
const csstree = require('css-tree');
// @ts-ignore not defined in @types/csso
const specificity = require('csso/lib/restructure/prepare/specificity');
const stable = require('stable');
const {
visitSkip,
querySelectorAll,
detachNodeFromParent,
} = require('../lib/xast.js');
exports.type = 'visitor';
exports.name = 'inlineStyles';
exports.active = true;
exports.description = 'inline styles (additional options)';
export const name = 'inlineStyles';
export const description = 'inline styles (additional options)';
/**
* Compares two selector specificities.
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
* Some pseudo-classes can only be calculated by clients, like :visited,
* :future, or :hover, but there are other pseudo-classes that we can evaluate
* during optimization.
*
* @type {(a: Specificity, b: Specificity) => number}
* Pseudo-classes that we can evaluate during optimization, and shouldn't be
* toggled conditionally through the `usePseudos` parameter.
*
* @see https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes
*/
const compareSpecificity = (a, b) => {
for (var i = 0; i < 4; i += 1) {
if (a[i] < b[i]) {
return -1;
} else if (a[i] > b[i]) {
return 1;
}
}
return 0;
};
const preservedPseudos = [
...pseudoClasses.functional,
...pseudoClasses.treeStructural,
];
/**
* Moves + merges styles from style elements to element styles
*
* Options
* onlyMatchedOnce (default: true)
* inline only selectors that match once
*
* removeMatchedSelectors (default: true)
* clean up matched selectors,
* leave selectors that hadn't matched
*
* useMqs (default: ['', 'screen'])
* what media queries to be used
* empty string element for styles outside media queries
*
* usePseudos (default: [''])
* what pseudo-classes/-elements to be used
* empty string element for all non-pseudo-classes and/or -elements
* Merges styles from style nodes into inline styles.
*
* @type {import('../lib/types.js').Plugin<InlineStylesParams>}
* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<{
* onlyMatchedOnce?: boolean,
* removeMatchedSelectors?: boolean,
* useMqs?: Array<string>,
* usePseudos?: Array<string>
* }>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const {
onlyMatchedOnce = true,
removeMatchedSelectors = true,
@@ -75,31 +51,32 @@ exports.fn = (root, params) => {
} = params;
/**
* @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>}
* @type {{
* node: import('../lib/types.js').XastElement,
* parentNode: import('../lib/types.js').XastParent,
* cssAst: csstree.StyleSheet
* }[]}
*/
const styles = [];
/**
* @type {Array<{
* @type {{
* node: csstree.Selector,
* item: csstree.ListItem<csstree.CssNode>,
* rule: csstree.Rule,
* matchedElements?: Array<XastElement>
* }>}
* matchedElements?: import('../lib/types.js').XastElement[]
* }[]}
*/
let selectors = [];
const selectors = [];
return {
element: {
enter: (node, parentNode) => {
// skip <foreignObject /> content
if (node.name === 'foreignObject') {
return visitSkip;
}
// collect only non-empty <style /> elements
if (node.name !== 'style' || node.children.length === 0) {
return;
}
// values other than the empty string or text/css are not used
if (
node.attributes.type != null &&
node.attributes.type !== '' &&
@@ -107,16 +84,13 @@ exports.fn = (root, params) => {
) {
return;
}
// parse css in style element
let cssText = '';
for (const child of node.children) {
if (child.type === 'text' || child.type === 'cdata') {
cssText += child.value;
}
}
/**
* @type {null | csstree.CssNode}
*/
const cssText = node.children
.filter((child) => child.type === 'text' || child.type === 'cdata')
.map((child) => child.value)
.join('');
/** @type {?csstree.CssNode} */
let cssAst = null;
try {
cssAst = csstree.parse(cssText, {
@@ -132,63 +106,68 @@ exports.fn = (root, params) => {
// collect selectors
csstree.walk(cssAst, {
visit: 'Selector',
enter(node, item) {
visit: 'Rule',
enter(node) {
const atrule = this.atrule;
const rule = this.rule;
if (rule == null) {
return;
}
// skip media queries not included into useMqs param
let mq = '';
let mediaQuery = '';
if (atrule != null) {
mq = atrule.name;
mediaQuery = atrule.name;
if (atrule.prelude != null) {
mq += ` ${csstree.generate(atrule.prelude)}`;
mediaQuery += ` ${csstree.generate(atrule.prelude)}`;
}
}
if (useMqs.includes(mq) === false) {
if (!useMqs.includes(mediaQuery)) {
return;
}
/**
* @type {Array<{
* item: csstree.ListItem<csstree.CssNode>,
* list: csstree.List<csstree.CssNode>
* }>}
*/
const pseudos = [];
if (node.type === 'Selector') {
node.children.each((childNode, childItem, childList) => {
if (
childNode.type === 'PseudoClassSelector' ||
childNode.type === 'PseudoElementSelector'
) {
pseudos.push({ item: childItem, list: childList });
if (node.prelude.type === 'SelectorList') {
node.prelude.children.forEach((childNode, item) => {
if (childNode.type === 'Selector') {
/**
* @type {{
* item: csstree.ListItem<csstree.CssNode>,
* list: csstree.List<csstree.CssNode>
* }[]}
*/
const pseudos = [];
childNode.children.forEach(
(grandchildNode, grandchildItem, grandchildList) => {
const isPseudo =
grandchildNode.type === 'PseudoClassSelector' ||
grandchildNode.type === 'PseudoElementSelector';
if (
isPseudo &&
!preservedPseudos.includes(grandchildNode.name)
) {
pseudos.push({
item: grandchildItem,
list: grandchildList,
});
}
},
);
const pseudoSelectors = csstree.generate({
type: 'Selector',
children: new csstree.List().fromArray(
pseudos.map((pseudo) => pseudo.item.data),
),
});
if (usePseudos.includes(pseudoSelectors)) {
for (const pseudo of pseudos) {
pseudo.list.remove(pseudo.item);
}
}
selectors.push({ node: childNode, rule: node, item: item });
}
});
}
// skip pseudo classes and pseudo elements not includes into usePseudos param
const pseudoSelectors = csstree.generate({
type: 'Selector',
children: new csstree.List().fromArray(
pseudos.map((pseudo) => pseudo.item.data)
),
});
if (usePseudos.includes(pseudoSelectors) === false) {
return;
}
// remove pseudo classes and elements to allow querySelector match elements
// TODO this is not very accurate since some pseudo classes like first-child
// are used for selection
for (const pseudo of pseudos) {
pseudo.list.remove(pseudo.item);
}
selectors.push({ node, item, rule });
},
});
},
@@ -199,19 +178,19 @@ exports.fn = (root, params) => {
if (styles.length === 0) {
return;
}
// stable sort selectors
const sortedSelectors = stable(selectors, (a, b) => {
const aSpecificity = specificity(a.item.data);
const bSpecificity = specificity(b.item.data);
return compareSpecificity(aSpecificity, bSpecificity);
}).reverse();
const sortedSelectors = selectors
.slice()
.sort((a, b) => {
const aSpecificity = syntax.specificity(a.item.data);
const bSpecificity = syntax.specificity(b.item.data);
return compareSpecificity(aSpecificity, bSpecificity);
})
.reverse();
for (const selector of sortedSelectors) {
// match selectors
const selectorText = csstree.generate(selector.item.data);
/**
* @type {Array<XastElement>}
*/
/** @type {import('../lib/types.js').XastElement[]} */
const matchedElements = [];
try {
for (const node of querySelectorAll(root, selectorText)) {
@@ -219,7 +198,7 @@ exports.fn = (root, params) => {
matchedElements.push(node);
}
}
} catch (selectError) {
} catch {
continue;
}
// nothing selected
@@ -236,22 +215,28 @@ exports.fn = (root, params) => {
// apply <style/> to matched elements
for (const selectedEl of matchedElements) {
const styleDeclarationList = csstree.parse(
selectedEl.attributes.style == null
? ''
: selectedEl.attributes.style,
selectedEl.attributes.style ?? '',
{
context: 'declarationList',
parseValue: false,
}
},
);
if (styleDeclarationList.type !== 'DeclarationList') {
continue;
}
const styleDeclarationItems = new Map();
/** @type {csstree.ListItem<csstree.CssNode>} */
let firstListItem;
csstree.walk(styleDeclarationList, {
visit: 'Declaration',
enter(node, item) {
styleDeclarationItems.set(node.property, item);
if (firstListItem == null) {
firstListItem = item;
}
styleDeclarationItems.set(node.property.toLowerCase(), item);
},
});
// merge declarations
@@ -262,30 +247,42 @@ exports.fn = (root, params) => {
// no inline styles, external styles, external styles used
// inline styles, external styles same priority as inline styles, inline styles used
// inline styles, external styles higher priority than inline styles, external styles used
const matchedItem = styleDeclarationItems.get(
ruleDeclaration.property
);
const property = ruleDeclaration.property;
if (
attrsGroups.presentation.has(property) &&
!selectors.some((selector) =>
includesAttrSelector(selector.item, property),
)
) {
delete selectedEl.attributes[property];
}
const matchedItem = styleDeclarationItems.get(property);
const ruleDeclarationItem =
styleDeclarationList.children.createItem(ruleDeclaration);
if (matchedItem == null) {
styleDeclarationList.children.append(ruleDeclarationItem);
styleDeclarationList.children.insert(
ruleDeclarationItem,
firstListItem,
);
} else if (
matchedItem.data.important !== true &&
ruleDeclaration.important === true
) {
styleDeclarationList.children.replace(
matchedItem,
ruleDeclarationItem
);
styleDeclarationItems.set(
ruleDeclaration.property,
ruleDeclarationItem
ruleDeclarationItem,
);
styleDeclarationItems.set(property, ruleDeclarationItem);
}
},
});
selectedEl.attributes.style =
csstree.generate(styleDeclarationList);
const newStyles = csstree.generate(styleDeclarationList);
if (newStyles.length !== 0) {
selectedEl.attributes.style = newStyles;
}
}
if (
@@ -300,7 +297,7 @@ exports.fn = (root, params) => {
}
// no further processing required
if (removeMatchedSelectors === false) {
if (!removeMatchedSelectors) {
return;
}
@@ -320,15 +317,25 @@ exports.fn = (root, params) => {
const classList = new Set(
selectedEl.attributes.class == null
? null
: selectedEl.attributes.class.split(' ')
: selectedEl.attributes.class.split(' '),
);
const firstSubSelector = selector.node.children.first();
if (
firstSubSelector != null &&
firstSubSelector.type === 'ClassSelector'
) {
classList.delete(firstSubSelector.name);
for (const child of selector.node.children) {
if (
child.type === 'ClassSelector' &&
!selectors.some((selector) =>
includesAttrSelector(
selector.item,
'class',
child.name,
true,
),
)
) {
classList.delete(child.name);
}
}
if (classList.size === 0) {
delete selectedEl.attributes.class;
} else {
@@ -336,13 +343,20 @@ exports.fn = (root, params) => {
}
// ID
const firstSubSelector = selector.node.children.first;
if (
firstSubSelector != null &&
firstSubSelector.type === 'IdSelector'
firstSubSelector?.type === 'IdSelector' &&
selectedEl.attributes.id === firstSubSelector.name &&
!selectors.some((selector) =>
includesAttrSelector(
selector.item,
'id',
firstSubSelector.name,
true,
),
)
) {
if (selectedEl.attributes.id === firstSubSelector.name) {
delete selectedEl.attributes.id;
}
delete selectedEl.attributes.id;
}
}
}
@@ -355,15 +369,15 @@ exports.fn = (root, params) => {
if (
node.type === 'Rule' &&
node.prelude.type === 'SelectorList' &&
node.prelude.children.isEmpty()
node.prelude.children.isEmpty
) {
list.remove(item);
}
},
});
if (style.cssAst.children.isEmpty()) {
// remove emtpy style element
if (style.cssAst.children.isEmpty) {
// remove empty style element
detachNodeFromParent(style.node, style.parentNode);
} else {
// update style element if any styles left

View File

@@ -1,29 +1,43 @@
'use strict';
import { collectStylesheet, computeStyle } from '../lib/style.js';
import { intersects, js2path, path2js } from './_path.js';
import { includesUrlReference } from '../lib/svgo/tools.js';
const { detachNodeFromParent } = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { path2js, js2path, intersects } = require('./_path.js');
/**
* @typedef MergePathsParams
* @property {boolean=} force
* @property {number=} floatPrecision
* @property {boolean=} noSpaceAfterFlags
*/
exports.type = 'visitor';
exports.name = 'mergePaths';
exports.active = true;
exports.description = 'merges multiple paths in one if possible';
export const name = 'mergePaths';
export const description = 'merges multiple paths in one if possible';
/**
* @param {import('../lib/types.js').ComputedStyles} computedStyle
* @param {string} attName
* @returns {boolean}
*/
function elementHasUrl(computedStyle, attName) {
const style = computedStyle[attName];
if (style?.type === 'static') {
return includesUrlReference(style.value);
}
return false;
}
/**
* Merge multiple Paths into one.
*
* @author Kir Belevich, Lev Solntsev
*
* @type {import('../lib/types').Plugin<{
* force?: boolean,
* floatPrecision?: number,
* noSpaceAfterFlags?: boolean
* }>}
* @type {import('../lib/types.js').Plugin<MergePathsParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const {
force = false,
floatPrecision,
floatPrecision = 3,
noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20
} = params;
const stylesheet = collectStylesheet(root);
@@ -31,73 +45,124 @@ exports.fn = (root, params) => {
return {
element: {
enter: (node) => {
let prevChild = null;
if (node.children.length <= 1) {
return;
}
/** @type {import('../lib/types.js').XastChild[]} */
const elementsToRemove = [];
let prevChild = node.children[0];
let prevPathData = null;
/**
* @param {import('../lib/types.js').XastElement} child
* @param {ReadonlyArray<import("../lib/types.js").PathDataItem>} pathData
*/
const updatePreviousPath = (child, pathData) => {
js2path(child, pathData, {
floatPrecision,
noSpaceAfterFlags,
});
prevPathData = null;
};
for (let i = 1; i < node.children.length; i++) {
const child = node.children[i];
for (const child of node.children) {
// skip if previous element is not path or contains animation elements
if (
prevChild == null ||
prevChild.type !== 'element' ||
prevChild.name !== 'path' ||
prevChild.children.length !== 0 ||
prevChild.attributes.d == null
) {
if (prevPathData && prevChild.type === 'element') {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
// skip if element is not path or contains animation elements
if (
child.type !== 'element' ||
child.name !== 'path' ||
child.children.length !== 0 ||
child.attributes.d == null
) {
if (prevPathData) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
// preserve paths with markers
const computedStyle = computeStyle(stylesheet, child);
if (
computedStyle['marker-start'] ||
computedStyle['marker-mid'] ||
computedStyle['marker-end']
computedStyle['marker-end'] ||
computedStyle['clip-path'] ||
computedStyle['mask'] ||
computedStyle['mask-image'] ||
['fill', 'filter', 'stroke'].some((attName) =>
elementHasUrl(computedStyle, attName),
)
) {
if (prevPathData) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
const childAttrs = Object.keys(child.attributes);
if (childAttrs.length !== Object.keys(prevChild.attributes).length) {
if (prevPathData) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
const prevChildAttrs = Object.keys(prevChild.attributes);
const childAttrs = Object.keys(child.attributes);
let attributesAreEqual = prevChildAttrs.length === childAttrs.length;
for (const name of childAttrs) {
if (name !== 'd') {
if (
prevChild.attributes[name] == null ||
prevChild.attributes[name] !== child.attributes[name]
) {
attributesAreEqual = false;
}
}
}
const prevPathJS = path2js(prevChild);
const curPathJS = path2js(child);
const areAttrsEqual = childAttrs.some((attr) => {
return (
attr !== 'd' &&
prevChild.type === 'element' &&
prevChild.attributes[attr] !== child.attributes[attr]
);
});
if (
attributesAreEqual &&
(force || !intersects(prevPathJS, curPathJS))
) {
js2path(prevChild, prevPathJS.concat(curPathJS), {
floatPrecision,
noSpaceAfterFlags,
});
detachNodeFromParent(child, node);
if (areAttrsEqual) {
if (prevPathData) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
continue;
}
const hasPrevPath = prevPathData != null;
const currentPathData = path2js(child);
prevPathData = prevPathData ?? path2js(prevChild);
if (force || !intersects(prevPathData, currentPathData)) {
prevPathData.push(...currentPathData);
elementsToRemove.push(child);
continue;
}
if (hasPrevPath) {
updatePreviousPath(prevChild, prevPathData);
}
prevChild = child;
prevPathData = null;
}
if (prevPathData && prevChild.type === 'element') {
updatePreviousPath(prevChild, prevPathData);
}
node.children = node.children.filter(
(child) => !elementsToRemove.includes(child),
);
},
},
};

View File

@@ -1,30 +1,21 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
import { visitSkip } from '../lib/util/visit.js';
/**
* @typedef {import('../lib/types').XastElement} XastElement
*/
const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const JSAPI = require('../lib/svgo/jsAPI.js');
exports.name = 'mergeStyles';
exports.type = 'visitor';
exports.active = true;
exports.description = 'merge multiple style elements into one';
export const name = 'mergeStyles';
export const description = 'merge multiple style elements into one';
/**
* Merge multiple style elements into one.
*
* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
/**
* @type {null | XastElement}
*/
export const fn = () => {
/** @type {?import('../lib/types.js').XastElement} */
let firstStyleElement = null;
let collectedStyles = '';
/** @type {'text' | 'cdata'} */
let styleContentType = 'text';
return {
@@ -80,12 +71,9 @@ exports.fn = () => {
firstStyleElement = node;
} else {
detachNodeFromParent(node, parentNode);
firstStyleElement.children = [
new JSAPI(
{ type: styleContentType, value: collectedStyles },
firstStyleElement
),
];
/** @type {import('../lib/types.js').XastChild} */
const child = { type: styleContentType, value: collectedStyles };
firstStyleElement.children = [child];
}
},
},

View File

@@ -1,37 +1,63 @@
'use strict';
import * as csso from 'csso';
import { detachNodeFromParent } from '../lib/xast.js';
import { hasScripts } from '../lib/svgo/tools.js';
/**
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef Usage
* @property {boolean=} force
* @property {boolean=} ids
* @property {boolean=} classes
* @property {boolean=} tags
*
* @typedef MinifyStylesParams
* @property {boolean=} restructure Disable or enable a structure optimizations.
* @property {boolean=} forceMediaMerge
* Enables merging of `@media` rules with the same media query split by other
* rules. Unsafe in general, but should work fine in most cases. Use it on
* your own risk.
* @property {'exclamation' | 'first-exclamation' | boolean=} comments
* Specify what comments to leave:
* - `'exclamation'` or `true` — leave all exclamation comments
* - `'first-exclamation'` — remove every comment except first one
* - `false` — remove all comments
* @property {boolean | Usage=} usage Advanced optimizations.
*/
const csso = require('csso');
exports.type = 'visitor';
exports.name = 'minifyStyles';
exports.active = true;
exports.description =
'minifies styles and removes unused styles based on usage data';
export const name = 'minifyStyles';
export const description = 'minifies styles and removes unused styles';
/**
* Minifies styles (<style> element + style attribute) using CSSO
* Minifies styles (<style> element + style attribute) using CSSO.
*
* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<csso.MinifyOptions & Omit<csso.CompressOptions, 'usage'> & {
* usage?: boolean | {
* force?: boolean,
* ids?: boolean,
* classes?: boolean,
* tags?: boolean
* }
* }>}
* @type {import('../lib/types.js').Plugin<MinifyStylesParams>}
*/
exports.fn = (_root, { usage, ...params }) => {
export const fn = (_root, { usage, ...params }) => {
/** @type {Map<import('../lib/types.js').XastElement, import('../lib/types.js').XastParent>} */
const styleElements = new Map();
/** @type {import('../lib/types.js').XastElement[]} */
const elementsWithStyleAttributes = [];
/** @type {Set<string>} */
const tagsUsage = new Set();
/** @type {Set<string>} */
const idsUsage = new Set();
/** @type {Set<string>} */
const classesUsage = new Set();
let enableTagsUsage = true;
let enableIdsUsage = true;
let enableClassesUsage = true;
// force to use usage data even if it unsafe (document contains <script> or on* attributes)
/**
* Force to use usage data even if it unsafe. For example, the document
* contains scripts or in attributes.
*/
let forceUsageDeoptimized = false;
if (typeof usage === 'boolean') {
enableTagsUsage = usage;
enableIdsUsage = usage;
@@ -42,40 +68,17 @@ exports.fn = (_root, { usage, ...params }) => {
enableClassesUsage = usage.classes == null ? true : usage.classes;
forceUsageDeoptimized = usage.force == null ? false : usage.force;
}
/**
* @type {Array<XastElement>}
*/
const styleElements = [];
/**
* @type {Array<XastElement>}
*/
const elementsWithStyleAttributes = [];
let deoptimized = false;
/**
* @type {Set<string>}
*/
const tagsUsage = new Set();
/**
* @type {Set<string>}
*/
const idsUsage = new Set();
/**
* @type {Set<string>}
*/
const classesUsage = new Set();
return {
element: {
enter: (node) => {
// detect deoptimisations
if (node.name === 'script') {
enter: (node, parentNode) => {
// detect deoptimizations
if (hasScripts(node)) {
deoptimized = true;
}
for (const name of Object.keys(node.attributes)) {
if (name.startsWith('on')) {
deoptimized = true;
}
}
// collect tags, ids and classes usage
tagsUsage.add(node.name);
if (node.attributes.id != null) {
@@ -88,7 +91,7 @@ exports.fn = (_root, { usage, ...params }) => {
}
// collect style elements or elements with style attribute
if (node.name === 'style' && node.children.length !== 0) {
styleElements.push(node);
styleElements.set(node, parentNode);
} else if (node.attributes.style != null) {
elementsWithStyleAttributes.push(node);
}
@@ -97,40 +100,44 @@ exports.fn = (_root, { usage, ...params }) => {
root: {
exit: () => {
/**
* @type {csso.Usage}
*/
/** @type {csso.Usage} */
const cssoUsage = {};
if (deoptimized === false || forceUsageDeoptimized === true) {
if (enableTagsUsage && tagsUsage.size !== 0) {
if (!deoptimized || forceUsageDeoptimized) {
if (enableTagsUsage) {
cssoUsage.tags = Array.from(tagsUsage);
}
if (enableIdsUsage && idsUsage.size !== 0) {
if (enableIdsUsage) {
cssoUsage.ids = Array.from(idsUsage);
}
if (enableClassesUsage && classesUsage.size !== 0) {
if (enableClassesUsage) {
cssoUsage.classes = Array.from(classesUsage);
}
}
// minify style elements
for (const node of styleElements) {
for (const [styleNode, styleNodeParent] of styleElements.entries()) {
if (
node.children[0].type === 'text' ||
node.children[0].type === 'cdata'
styleNode.children[0].type === 'text' ||
styleNode.children[0].type === 'cdata'
) {
const cssText = node.children[0].value;
const cssText = styleNode.children[0].value;
const minified = csso.minify(cssText, {
...params,
usage: cssoUsage,
}).css;
if (minified.length === 0) {
detachNodeFromParent(styleNode, styleNodeParent);
continue;
}
// preserve cdata if necessary
// TODO split cdata -> text optimisation into separate plugin
// TODO split cdata -> text optimization into separate plugin
if (cssText.indexOf('>') >= 0 || cssText.indexOf('<') >= 0) {
node.children[0].type = 'cdata';
node.children[0].value = minified;
styleNode.children[0].type = 'cdata';
styleNode.children[0].value = minified;
} else {
node.children[0].type = 'text';
node.children[0].value = minified;
styleNode.children[0].type = 'text';
styleNode.children[0].value = minified;
}
}
}

View File

@@ -1,12 +1,9 @@
'use strict';
import { visit } from '../lib/util/visit.js';
import { inheritableAttrs, pathElems } from './_collections.js';
const { visit } = require('../lib/xast.js');
const { inheritableAttrs, pathElems } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'moveElemsAttrsToGroup';
exports.active = true;
exports.description = 'Move common attributes of group children to the group';
export const name = 'moveElemsAttrsToGroup';
export const description =
'Move common attributes of group children to the group';
/**
* Move common attributes of group children to the group
@@ -18,7 +15,7 @@ exports.description = 'Move common attributes of group children to the group';
* </g>
* <circle attr2="val2" attr3="val3"/>
* </g>
*
* ⬇
* <g attr1="val1" attr2="val2">
* <g>
* text
@@ -28,9 +25,9 @@ exports.description = 'Move common attributes of group children to the group';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = (root) => {
export const fn = (root) => {
// find if any style element is present
let deoptimizedWithStyles = false;
visit(root, {
@@ -46,7 +43,7 @@ exports.fn = (root) => {
return {
element: {
exit: (node) => {
// process only groups with more than 1 children
// process only groups with more than 1 child
if (node.name !== 'g' || node.children.length <= 1) {
return;
}
@@ -58,7 +55,8 @@ exports.fn = (root) => {
}
/**
* find common attributes in group children
* Find common attributes in group children.
*
* @type {Map<string, string>}
*/
const commonAttributes = new Map();
@@ -66,7 +64,7 @@ exports.fn = (root) => {
let everyChildIsPath = true;
for (const child of node.children) {
if (child.type === 'element') {
if (pathElems.includes(child.name) === false) {
if (!pathElems.has(child.name)) {
everyChildIsPath = false;
}
if (initial) {
@@ -74,7 +72,7 @@ exports.fn = (root) => {
// collect all inheritable attributes from first child element
for (const [name, value] of Object.entries(child.attributes)) {
// consider only inheritable attributes
if (inheritableAttrs.includes(name)) {
if (inheritableAttrs.has(name)) {
commonAttributes.set(name, value);
}
}
@@ -89,8 +87,9 @@ exports.fn = (root) => {
}
}
// preserve transform on children when group has clip-path or mask
// preserve transform on children when group has filter or clip-path or mask
if (
node.attributes['filter'] != null ||
node.attributes['clip-path'] != null ||
node.attributes.mask != null
) {

View File

@@ -1,14 +1,9 @@
'use strict';
import { pathElems, referencesProps } from './_collections.js';
import { includesUrlReference } from '../lib/svgo/tools.js';
const { pathElems, referencesProps } = require('./_collections.js');
exports.name = 'moveGroupAttrsToElems';
exports.type = 'perItem';
exports.active = true;
exports.description = 'moves some group attributes to the content elements';
export const name = 'moveGroupAttrsToElems';
export const description =
'moves some group attributes to the content elements';
const pathElemsWithGroupsAndText = [...pathElems, 'g', 'text'];
@@ -20,43 +15,50 @@ const pathElemsWithGroupsAndText = [...pathElems, 'g', 'text'];
* <path transform="rotate(45)" d="M0,0 L10,20"/>
* <path transform="translate(10, 20)" d="M0,10 L20,30"/>
* </g>
*
* ⬇
* <g>
* <path transform="scale(2) rotate(45)" d="M0,0 L10,20"/>
* <path transform="scale(2) translate(10, 20)" d="M0,10 L20,30"/>
* </g>
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = function (item) {
// move group transform attr to content's pathElems
if (
item.type === 'element' &&
item.name === 'g' &&
item.children.length !== 0 &&
item.attributes.transform != null &&
Object.entries(item.attributes).some(
([name, value]) =>
referencesProps.includes(name) && value.includes('url(')
) === false &&
item.children.every(
(inner) =>
pathElemsWithGroupsAndText.includes(inner.name) &&
inner.attributes.id == null
)
) {
for (const inner of item.children) {
const value = item.attributes.transform;
if (inner.attributes.transform != null) {
inner.attributes.transform = value + ' ' + inner.attributes.transform;
} else {
inner.attributes.transform = value;
}
}
export const fn = () => {
return {
element: {
enter: (node) => {
// move group transform attr to content's pathElems
if (
node.name === 'g' &&
node.children.length !== 0 &&
node.attributes.transform != null &&
Object.entries(node.attributes).some(
([name, value]) =>
referencesProps.has(name) && includesUrlReference(value),
) === false &&
node.children.every(
(child) =>
child.type === 'element' &&
pathElemsWithGroupsAndText.includes(child.name) &&
child.attributes.id == null,
)
) {
for (const child of node.children) {
const value = node.attributes.transform;
if (child.type === 'element') {
if (child.attributes.transform != null) {
child.attributes.transform = `${value} ${child.attributes.transform}`;
} else {
child.attributes.transform = value;
}
}
}
delete item.attributes.transform;
}
delete node.attributes.transform;
}
},
},
};
};

56
node_modules/svgo/plugins/plugins.js generated vendored
View File

@@ -1,56 +0,0 @@
'use strict';
// builtin presets
exports['preset-default'] = require('./preset-default.js');
// builtin plugins
exports.addAttributesToSVGElement = require('./addAttributesToSVGElement.js');
exports.addClassesToSVGElement = require('./addClassesToSVGElement.js');
exports.cleanupAttrs = require('./cleanupAttrs.js');
exports.cleanupEnableBackground = require('./cleanupEnableBackground.js');
exports.cleanupIDs = require('./cleanupIDs.js');
exports.cleanupListOfValues = require('./cleanupListOfValues.js');
exports.cleanupNumericValues = require('./cleanupNumericValues.js');
exports.collapseGroups = require('./collapseGroups.js');
exports.convertColors = require('./convertColors.js');
exports.convertEllipseToCircle = require('./convertEllipseToCircle.js');
exports.convertPathData = require('./convertPathData.js');
exports.convertShapeToPath = require('./convertShapeToPath.js');
exports.convertStyleToAttrs = require('./convertStyleToAttrs.js');
exports.convertTransform = require('./convertTransform.js');
exports.mergeStyles = require('./mergeStyles.js');
exports.inlineStyles = require('./inlineStyles.js');
exports.mergePaths = require('./mergePaths.js');
exports.minifyStyles = require('./minifyStyles.js');
exports.moveElemsAttrsToGroup = require('./moveElemsAttrsToGroup.js');
exports.moveGroupAttrsToElems = require('./moveGroupAttrsToElems.js');
exports.prefixIds = require('./prefixIds.js');
exports.removeAttributesBySelector = require('./removeAttributesBySelector.js');
exports.removeAttrs = require('./removeAttrs.js');
exports.removeComments = require('./removeComments.js');
exports.removeDesc = require('./removeDesc.js');
exports.removeDimensions = require('./removeDimensions.js');
exports.removeDoctype = require('./removeDoctype.js');
exports.removeEditorsNSData = require('./removeEditorsNSData.js');
exports.removeElementsByAttr = require('./removeElementsByAttr.js');
exports.removeEmptyAttrs = require('./removeEmptyAttrs.js');
exports.removeEmptyContainers = require('./removeEmptyContainers.js');
exports.removeEmptyText = require('./removeEmptyText.js');
exports.removeHiddenElems = require('./removeHiddenElems.js');
exports.removeMetadata = require('./removeMetadata.js');
exports.removeNonInheritableGroupAttrs = require('./removeNonInheritableGroupAttrs.js');
exports.removeOffCanvasPaths = require('./removeOffCanvasPaths.js');
exports.removeRasterImages = require('./removeRasterImages.js');
exports.removeScriptElement = require('./removeScriptElement.js');
exports.removeStyleElement = require('./removeStyleElement.js');
exports.removeTitle = require('./removeTitle.js');
exports.removeUnknownsAndDefaults = require('./removeUnknownsAndDefaults.js');
exports.removeUnusedNS = require('./removeUnusedNS.js');
exports.removeUselessDefs = require('./removeUselessDefs.js');
exports.removeUselessStrokeAndFill = require('./removeUselessStrokeAndFill.js');
exports.removeViewBox = require('./removeViewBox.js');
exports.removeXMLNS = require('./removeXMLNS.js');
exports.removeXMLProcInst = require('./removeXMLProcInst.js');
exports.reusePaths = require('./reusePaths.js');
exports.sortAttrs = require('./sortAttrs.js');
exports.sortDefsChildren = require('./sortDefsChildren.js');

View File

@@ -1,25 +1,26 @@
'use strict';
const csstree = require('css-tree');
const { referencesProps } = require('./_collections.js');
import * as csstree from 'css-tree';
import { referencesProps } from './_collections.js';
/**
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').PluginInfo} PluginInfo
* @typedef PrefixIdsParams
* @property {boolean | string | ((node: import('../lib/types.js').XastElement, info: import('../lib/types.js').PluginInfo) => string)=} prefix
* @property {string=} delim
* @property {boolean=} prefixIds
* @property {boolean=} prefixClassNames
*/
exports.type = 'visitor';
exports.name = 'prefixIds';
exports.active = false;
exports.description = 'prefix IDs';
export const name = 'prefixIds';
export const description = 'prefix IDs';
/**
* extract basename from path
* @type {(path: string) => string}
* Extract basename from path.
*
* @param {string} path
* @returns {string}
*/
const getBasename = (path) => {
// extract everything after latest slash or backslash
const matched = path.match(/[/\\]?([^/\\]+)$/);
const matched = /[/\\]?([^/\\]+)$/.exec(path);
if (matched) {
return matched[1];
}
@@ -27,15 +28,18 @@ const getBasename = (path) => {
};
/**
* escapes a string for being used as ID
* @type {(string: string) => string}
* Escapes a string for being used as ID.
*
* @param {string} str
* @returns {string}
*/
const escapeIdentifierName = (str) => {
return str.replace(/[. ]/g, '_');
};
/**
* @type {(string: string) => string}
* @param {string} string
* @returns {string}
*/
const unquote = (string) => {
if (
@@ -48,59 +52,100 @@ const unquote = (string) => {
};
/**
* prefix an ID
* @type {(prefix: string, name: string) => string}
* Prefix the given string, unless it already starts with the generated prefix.
*
* @param {(id: string) => string} prefixGenerator Function to generate a prefix.
* @param {string} body An arbitrary string.
* @returns {string} The given string with a prefix prepended to it.
*/
const prefixId = (prefix, value) => {
if (value.startsWith(prefix)) {
return value;
const prefixId = (prefixGenerator, body) => {
const prefix = prefixGenerator(body);
if (body.startsWith(prefix)) {
return body;
}
return prefix + value;
return prefix + body;
};
/**
* prefix an #ID
* @type {(prefix: string, name: string) => string | null}
* Insert the prefix in a reference string. A reference string is already
* prefixed with #, so the prefix is inserted after the first character.
*
* @param {(id: string) => string} prefixGenerator Function to generate a prefix.
* @param {string} reference An arbitrary string, should start with "#".
* @returns {?string} The given string with a prefix inserted, or null if the string did not start with "#".
*/
const prefixReference = (prefix, value) => {
if (value.startsWith('#')) {
return '#' + prefixId(prefix, value.slice(1));
const prefixReference = (prefixGenerator, reference) => {
if (reference.startsWith('#')) {
return '#' + prefixId(prefixGenerator, reference.slice(1));
}
return null;
};
/**
* Generates a prefix for the given string.
*
* @param {string} body An arbitrary string.
* @param {import('../lib/types.js').XastElement} node XML node that the identifier belongs to.
* @param {import('../lib/types.js').PluginInfo} info
* @param {((node: import('../lib/types.js').XastElement, info: import('../lib/types.js').PluginInfo) => string) | string | boolean | undefined} prefixGenerator Some way of obtaining a prefix.
* @param {string} delim Content to insert between the prefix and original value.
* @param {Map<string, string>} history Map of previously generated prefixes to IDs.
* @returns {string} A generated prefix.
*/
const generatePrefix = (body, node, info, prefixGenerator, delim, history) => {
if (typeof prefixGenerator === 'function') {
let prefix = history.get(body);
if (prefix != null) {
return prefix;
}
prefix = prefixGenerator(node, info) + delim;
history.set(body, prefix);
return prefix;
}
if (typeof prefixGenerator === 'string') {
return prefixGenerator + delim;
}
if (prefixGenerator === false) {
return '';
}
if (info.path != null && info.path.length > 0) {
return escapeIdentifierName(getBasename(info.path)) + delim;
}
return 'prefix' + delim;
};
/**
* Prefixes identifiers
*
* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<{
* prefix?: boolean | string | ((node: XastElement, info: PluginInfo) => string),
* delim?: string,
* prefixIds?: boolean,
* prefixClassNames?: boolean,
* }>}
* @type {import('../lib/types.js').Plugin<PrefixIdsParams>}
*/
exports.fn = (_root, params, info) => {
const { delim = '__', prefixIds = true, prefixClassNames = true } = params;
export const fn = (_root, params, info) => {
const {
delim = '__',
prefix,
prefixIds = true,
prefixClassNames = true,
} = params;
/** @type {Map<string, string>} */
const prefixMap = new Map();
return {
element: {
enter: (node) => {
/**
* prefix, from file name or option
* @type {string}
* @param {string} id A node identifier or class.
* @returns {string} Given string with a prefix inserted, or null if the string did not start with "#".
*/
let prefix = 'prefix' + delim;
if (typeof params.prefix === 'function') {
prefix = params.prefix(node, info) + delim;
} else if (typeof params.prefix === 'string') {
prefix = params.prefix + delim;
} else if (params.prefix === false) {
prefix = '';
} else if (info.path != null && info.path.length > 0) {
prefix = escapeIdentifierName(getBasename(info.path)) + delim;
}
const prefixGenerator = (id) =>
generatePrefix(id, node, info, prefix, delim, prefixMap);
// prefix id/class selectors and url() references in styles
if (node.name === 'style') {
@@ -109,60 +154,44 @@ exports.fn = (_root, params, info) => {
return;
}
// parse styles
let cssText = '';
if (
node.children[0].type === 'text' ||
node.children[0].type === 'cdata'
) {
cssText = node.children[0].value;
}
/**
* @type {null | csstree.CssNode}
*/
let cssAst = null;
try {
cssAst = csstree.parse(cssText, {
parseValue: true,
parseCustomProperty: false,
});
} catch {
return;
}
for (const child of node.children) {
if (child.type !== 'text' && child.type !== 'cdata') {
continue;
}
csstree.walk(cssAst, (node) => {
// #ID, .class selectors
if (
(prefixIds && node.type === 'IdSelector') ||
(prefixClassNames && node.type === 'ClassSelector')
) {
node.name = prefixId(prefix, node.name);
const cssText = child.value;
/** @type {?csstree.CssNode} */
let cssAst;
try {
cssAst = csstree.parse(cssText, {
parseValue: true,
parseCustomProperty: false,
});
} catch {
return;
}
// url(...) references
if (
node.type === 'Url' &&
node.value.value &&
node.value.value.length > 0
) {
const prefixed = prefixReference(
prefix,
unquote(node.value.value)
);
if (prefixed != null) {
node.value.value = prefixed;
}
}
});
// update styles
if (
node.children[0].type === 'text' ||
node.children[0].type === 'cdata'
) {
node.children[0].value = csstree.generate(cssAst);
csstree.walk(cssAst, (node) => {
if (
(prefixIds && node.type === 'IdSelector') ||
(prefixClassNames && node.type === 'ClassSelector')
) {
node.name = prefixId(prefixGenerator, node.name);
return;
}
if (node.type === 'Url' && node.value.length > 0) {
const prefixed = prefixReference(
prefixGenerator,
unquote(node.value),
);
if (prefixed != null) {
node.value = prefixed;
}
}
});
child.value = csstree.generate(cssAst);
}
return;
}
// prefix an ID attribute value
@@ -171,7 +200,7 @@ exports.fn = (_root, params, info) => {
node.attributes.id != null &&
node.attributes.id.length !== 0
) {
node.attributes.id = prefixId(prefix, node.attributes.id);
node.attributes.id = prefixId(prefixGenerator, node.attributes.id);
}
// prefix a class attribute value
@@ -182,39 +211,42 @@ exports.fn = (_root, params, info) => {
) {
node.attributes.class = node.attributes.class
.split(/\s+/)
.map((name) => prefixId(prefix, name))
.map((name) => prefixId(prefixGenerator, name))
.join(' ');
}
// prefix a href attribute value
// prefix an href attribute value
// xlink:href is deprecated, must be still supported
for (const name of ['href', 'xlink:href']) {
if (
node.attributes[name] != null &&
node.attributes[name].length !== 0
) {
const prefixed = prefixReference(prefix, node.attributes[name]);
const prefixed = prefixReference(
prefixGenerator,
node.attributes[name],
);
if (prefixed != null) {
node.attributes[name] = prefixed;
}
}
}
// prefix an URL attribute value
// prefix a URL attribute value
for (const name of referencesProps) {
if (
node.attributes[name] != null &&
node.attributes[name].length !== 0
) {
node.attributes[name] = node.attributes[name].replace(
/url\((.*?)\)/gi,
(match, url) => {
const prefixed = prefixReference(prefix, url);
/\burl\((["'])?(#.+?)\1\)/gi,
(match, _, url) => {
const prefixed = prefixReference(prefixGenerator, url);
if (prefixed == null) {
return match;
}
return `url(${prefixed})`;
}
},
);
}
}
@@ -228,7 +260,7 @@ exports.fn = (_root, params, info) => {
const parts = node.attributes[name].split(/\s*;\s+/).map((val) => {
if (val.endsWith('.end') || val.endsWith('.start')) {
const [id, postfix] = val.split('.');
return `${prefixId(prefix, id)}.${postfix}`;
return `${prefixId(prefixGenerator, id)}.${postfix}`;
}
return val;
});

View File

@@ -1,62 +1,59 @@
'use strict';
const { createPreset } = require('../lib/svgo/plugins.js');
const removeDoctype = require('./removeDoctype.js');
const removeXMLProcInst = require('./removeXMLProcInst.js');
const removeComments = require('./removeComments.js');
const removeMetadata = require('./removeMetadata.js');
const removeEditorsNSData = require('./removeEditorsNSData.js');
const cleanupAttrs = require('./cleanupAttrs.js');
const mergeStyles = require('./mergeStyles.js');
const inlineStyles = require('./inlineStyles.js');
const minifyStyles = require('./minifyStyles.js');
const cleanupIDs = require('./cleanupIDs.js');
const removeUselessDefs = require('./removeUselessDefs.js');
const cleanupNumericValues = require('./cleanupNumericValues.js');
const convertColors = require('./convertColors.js');
const removeUnknownsAndDefaults = require('./removeUnknownsAndDefaults.js');
const removeNonInheritableGroupAttrs = require('./removeNonInheritableGroupAttrs.js');
const removeUselessStrokeAndFill = require('./removeUselessStrokeAndFill.js');
const removeViewBox = require('./removeViewBox.js');
const cleanupEnableBackground = require('./cleanupEnableBackground.js');
const removeHiddenElems = require('./removeHiddenElems.js');
const removeEmptyText = require('./removeEmptyText.js');
const convertShapeToPath = require('./convertShapeToPath.js');
const convertEllipseToCircle = require('./convertEllipseToCircle.js');
const moveElemsAttrsToGroup = require('./moveElemsAttrsToGroup.js');
const moveGroupAttrsToElems = require('./moveGroupAttrsToElems.js');
const collapseGroups = require('./collapseGroups.js');
const convertPathData = require('./convertPathData.js');
const convertTransform = require('./convertTransform.js');
const removeEmptyAttrs = require('./removeEmptyAttrs.js');
const removeEmptyContainers = require('./removeEmptyContainers.js');
const mergePaths = require('./mergePaths.js');
const removeUnusedNS = require('./removeUnusedNS.js');
const sortDefsChildren = require('./sortDefsChildren.js');
const removeTitle = require('./removeTitle.js');
const removeDesc = require('./removeDesc.js');
import { createPreset } from '../lib/svgo/plugins.js';
import * as removeDoctype from './removeDoctype.js';
import * as removeXMLProcInst from './removeXMLProcInst.js';
import * as removeComments from './removeComments.js';
import * as removeDeprecatedAttrs from './removeDeprecatedAttrs.js';
import * as removeMetadata from './removeMetadata.js';
import * as removeEditorsNSData from './removeEditorsNSData.js';
import * as cleanupAttrs from './cleanupAttrs.js';
import * as mergeStyles from './mergeStyles.js';
import * as inlineStyles from './inlineStyles.js';
import * as minifyStyles from './minifyStyles.js';
import * as cleanupIds from './cleanupIds.js';
import * as removeUselessDefs from './removeUselessDefs.js';
import * as cleanupNumericValues from './cleanupNumericValues.js';
import * as convertColors from './convertColors.js';
import * as removeUnknownsAndDefaults from './removeUnknownsAndDefaults.js';
import * as removeNonInheritableGroupAttrs from './removeNonInheritableGroupAttrs.js';
import * as removeUselessStrokeAndFill from './removeUselessStrokeAndFill.js';
import * as cleanupEnableBackground from './cleanupEnableBackground.js';
import * as removeHiddenElems from './removeHiddenElems.js';
import * as removeEmptyText from './removeEmptyText.js';
import * as convertShapeToPath from './convertShapeToPath.js';
import * as convertEllipseToCircle from './convertEllipseToCircle.js';
import * as moveElemsAttrsToGroup from './moveElemsAttrsToGroup.js';
import * as moveGroupAttrsToElems from './moveGroupAttrsToElems.js';
import * as collapseGroups from './collapseGroups.js';
import * as convertPathData from './convertPathData.js';
import * as convertTransform from './convertTransform.js';
import * as removeEmptyAttrs from './removeEmptyAttrs.js';
import * as removeEmptyContainers from './removeEmptyContainers.js';
import * as mergePaths from './mergePaths.js';
import * as removeUnusedNS from './removeUnusedNS.js';
import * as sortAttrs from './sortAttrs.js';
import * as sortDefsChildren from './sortDefsChildren.js';
import * as removeDesc from './removeDesc.js';
const presetDefault = createPreset({
name: 'presetDefault',
name: 'preset-default',
plugins: [
removeDoctype,
removeXMLProcInst,
removeComments,
removeDeprecatedAttrs,
removeMetadata,
removeEditorsNSData,
cleanupAttrs,
mergeStyles,
inlineStyles,
minifyStyles,
cleanupIDs,
cleanupIds,
removeUselessDefs,
cleanupNumericValues,
convertColors,
removeUnknownsAndDefaults,
removeNonInheritableGroupAttrs,
removeUselessStrokeAndFill,
removeViewBox,
cleanupEnableBackground,
removeHiddenElems,
removeEmptyText,
@@ -71,10 +68,10 @@ const presetDefault = createPreset({
removeEmptyContainers,
mergePaths,
removeUnusedNS,
sortAttrs,
sortDefsChildren,
removeTitle,
removeDesc,
],
});
module.exports = presetDefault;
export default presetDefault;

View File

@@ -1,11 +1,7 @@
'use strict';
import { querySelectorAll } from '../lib/xast.js';
const { querySelectorAll } = require('../lib/xast.js');
exports.name = 'removeAttributesBySelector';
exports.type = 'visitor';
exports.active = false;
exports.description =
export const name = 'removeAttributesBySelector';
export const description =
'removes attributes of elements that match a css selector';
/**
@@ -71,13 +67,13 @@ exports.description =
* ↓
* <rect x="0" y="0" width="100" height="100"/>
*
* @link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors
* @link https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors
*
* @author Bradley Mease
*
* @type {import('../lib/types').Plugin<any>}
* @type {import('../lib/types.js').Plugin<any>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const selectors = Array.isArray(params.selectors)
? params.selectors
: [params];

View File

@@ -1,9 +1,12 @@
'use strict';
/**
* @typedef RemoveAttrsParams
* @property {string=} elemSeparator
* @property {boolean=} preserveCurrentColor
* @property {string | string[]} attrs
*/
exports.name = 'removeAttrs';
exports.type = 'visitor';
exports.active = false;
exports.description = 'removes specified attributes';
export const name = 'removeAttrs';
export const description = 'removes specified attributes';
const DEFAULT_SEPARATOR = ':';
const ENOATTRS = `Warning: The plugin "removeAttrs" requires the "attrs" parameter.
@@ -83,13 +86,9 @@ plugins: [
*
* @author Benny Schudel
*
* @type {import('../lib/types').Plugin<{
* elemSeparator?: string,
* preserveCurrentColor?: boolean,
* attrs: string | Array<string>
* }>}
* @type {import('../lib/types.js').Plugin<RemoveAttrsParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
if (typeof params.attrs == 'undefined') {
console.warn(ENOATTRS);
return null;
@@ -110,13 +109,11 @@ exports.fn = (root, params) => {
enter: (node) => {
for (let pattern of attrs) {
// if no element separators (:), assume it's attribute name, and apply to all elements *regardless of value*
if (pattern.includes(elemSeparator) === false) {
pattern = ['.*', elemSeparator, pattern, elemSeparator, '.*'].join(
''
);
if (!pattern.includes(elemSeparator)) {
pattern = ['.*', pattern, '.*'].join(elemSeparator);
// if only 1 separator, assume it's element and attribute name, and apply regardless of attribute value
} else if (pattern.split(elemSeparator).length < 3) {
pattern = [pattern, elemSeparator, '.*'].join('');
pattern = [pattern, '.*'].join(elemSeparator);
}
// create regexps for element, attribute name, and attribute value
@@ -132,14 +129,11 @@ exports.fn = (root, params) => {
if (list[0].test(node.name)) {
// loop attributes
for (const [name, value] of Object.entries(node.attributes)) {
const isCurrentColor = value.toLowerCase() === 'currentcolor';
const isFillCurrentColor =
preserveCurrentColor &&
name == 'fill' &&
value == 'currentColor';
preserveCurrentColor && name == 'fill' && isCurrentColor;
const isStrokeCurrentColor =
preserveCurrentColor &&
name == 'stroke' &&
value == 'currentColor';
preserveCurrentColor && name == 'stroke' && isCurrentColor;
if (
!isFillCurrentColor &&
!isStrokeCurrentColor &&

View File

@@ -1,11 +1,18 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
/**
* @typedef RemoveCommentsParams
* @property {ReadonlyArray<RegExp | string> | false=} preservePatterns
*/
exports.name = 'removeComments';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes comments';
export const name = 'removeComments';
export const description = 'removes comments';
/**
* If a comment matches one of the following patterns, it will be
* preserved by default. Particularly for copyright/license information.
*/
const DEFAULT_PRESERVE_PATTERNS = [/^!/];
/**
* Remove comments.
@@ -16,15 +23,31 @@ exports.description = 'removes comments';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin<RemoveCommentsParams>}
*/
exports.fn = () => {
export const fn = (_root, params) => {
const { preservePatterns = DEFAULT_PRESERVE_PATTERNS } = params;
return {
comment: {
enter: (node, parentNode) => {
if (node.value.charAt(0) !== '!') {
detachNodeFromParent(node, parentNode);
if (preservePatterns) {
if (!Array.isArray(preservePatterns)) {
throw Error(
`Expected array in removeComments preservePatterns parameter but received ${preservePatterns}`,
);
}
const matches = preservePatterns.some((pattern) => {
return new RegExp(pattern).test(node.value);
});
if (matches) {
return;
}
}
detachNodeFromParent(node, parentNode);
},
},
};

120
node_modules/svgo/plugins/removeDeprecatedAttrs.js generated vendored Normal file
View File

@@ -0,0 +1,120 @@
import * as csswhat from 'css-what';
import { attrsGroupsDeprecated, elems } from './_collections.js';
import { collectStylesheet } from '../lib/style.js';
/**
* @typedef RemoveDeprecatedAttrsParams
* @property {boolean=} removeUnsafe
*/
export const name = 'removeDeprecatedAttrs';
export const description = 'removes deprecated attributes';
/**
* @param {import('../lib/types.js').Stylesheet} stylesheet
* @returns {Set<string>}
*/
function extractAttributesInStylesheet(stylesheet) {
const attributesInStylesheet = new Set();
stylesheet.rules.forEach((rule) => {
const selectors = csswhat.parse(rule.selector);
selectors.forEach((subselector) => {
subselector.forEach((segment) => {
if (segment.type !== 'attribute') {
return;
}
attributesInStylesheet.add(segment.name);
});
});
});
return attributesInStylesheet;
}
/**
* @param {import('../lib/types.js').XastElement} node
* @param {{ safe?: Set<string>; unsafe?: Set<string> }|undefined} deprecatedAttrs
* @param {import('../lib/types.js').DefaultPlugins['removeDeprecatedAttrs']} params
* @param {Set<string>} attributesInStylesheet
*/
function processAttributes(
node,
deprecatedAttrs,
params,
attributesInStylesheet,
) {
if (!deprecatedAttrs) {
return;
}
if (deprecatedAttrs.safe) {
deprecatedAttrs.safe.forEach((name) => {
if (attributesInStylesheet.has(name)) {
return;
}
delete node.attributes[name];
});
}
if (params.removeUnsafe && deprecatedAttrs.unsafe) {
deprecatedAttrs.unsafe.forEach((name) => {
if (attributesInStylesheet.has(name)) {
return;
}
delete node.attributes[name];
});
}
}
/**
* Remove deprecated attributes.
*
* @type {import('../lib/types.js').Plugin<RemoveDeprecatedAttrsParams>}
*/
export function fn(root, params) {
const stylesheet = collectStylesheet(root);
const attributesInStylesheet = extractAttributesInStylesheet(stylesheet);
return {
element: {
enter: (node) => {
const elemConfig = elems[node.name];
if (!elemConfig) {
return;
}
// Special cases
// Removing deprecated xml:lang is safe when the lang attribute exists.
if (
elemConfig.attrsGroups.has('core') &&
node.attributes['xml:lang'] &&
!attributesInStylesheet.has('xml:lang') &&
node.attributes['lang']
) {
delete node.attributes['xml:lang'];
}
// General cases
elemConfig.attrsGroups.forEach((attrsGroup) => {
processAttributes(
node,
attrsGroupsDeprecated[attrsGroup],
params,
attributesInStylesheet,
);
});
processAttributes(
node,
elemConfig.deprecated,
params,
attributesInStylesheet,
);
},
},
};
}

View File

@@ -1,27 +1,28 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
/**
* @typedef RemoveDescParams
* @property {boolean=} removeAny
*/
exports.name = 'removeDesc';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes <desc>';
export const name = 'removeDesc';
export const description = 'removes <desc>';
const standardDescs = /^(Created with|Created using)/;
/**
* Removes <desc>.
* Removes only standard editors content or empty elements 'cause it can be used for accessibility.
* Enable parameter 'removeAny' to remove any description.
*
* https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
* Removes only standard editors content or empty elements because it can be
* used for accessibility. Enable parameter 'removeAny' to remove any
* description.
*
* @author Daniel Wabyick
* @see https://developer.mozilla.org/docs/Web/SVG/Element/desc
*
* @type {import('../lib/types').Plugin<{ removeAny?: boolean }>}
* @type {import('../lib/types.js').Plugin<RemoveDescParams>}
*/
exports.fn = (root, params) => {
const { removeAny = true } = params;
export const fn = (root, params) => {
const { removeAny = false } = params;
return {
element: {
enter: (node, parentNode) => {

View File

@@ -1,13 +1,6 @@
'use strict';
exports.name = 'removeDimensions';
exports.type = 'perItem';
exports.active = false;
exports.description =
'removes width and height in presence of viewBox (opposite to removeViewBox, disable it first)';
export const name = 'removeDimensions';
export const description =
'removes width and height in presence of viewBox (opposite to removeViewBox)';
/**
* Remove width/height attributes and add the viewBox attribute if it's missing
@@ -17,27 +10,32 @@ exports.description =
* ↓
* <svg viewBox="0 0 100 50" />
*
* @param {Object} item current iteration item
* @return {Boolean} if true, with and height will be filtered out
*
* @author Benny Schudel
*
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = function (item) {
if (item.type === 'element' && item.name === 'svg') {
if (item.attributes.viewBox != null) {
delete item.attributes.width;
delete item.attributes.height;
} else if (
item.attributes.width != null &&
item.attributes.height != null &&
Number.isNaN(Number(item.attributes.width)) === false &&
Number.isNaN(Number(item.attributes.height)) === false
) {
const width = Number(item.attributes.width);
const height = Number(item.attributes.height);
item.attributes.viewBox = `0 0 ${width} ${height}`;
delete item.attributes.width;
delete item.attributes.height;
}
}
export const fn = () => {
return {
element: {
enter: (node) => {
if (node.name === 'svg') {
if (node.attributes.viewBox != null) {
delete node.attributes.width;
delete node.attributes.height;
} else if (
node.attributes.width != null &&
node.attributes.height != null &&
Number.isNaN(Number(node.attributes.width)) === false &&
Number.isNaN(Number(node.attributes.height)) === false
) {
const width = Number(node.attributes.width);
const height = Number(node.attributes.height);
node.attributes.viewBox = `0 0 ${width} ${height}`;
delete node.attributes.width;
delete node.attributes.height;
}
}
},
},
};
};

View File

@@ -1,11 +1,7 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'removeDoctype';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes doctype declaration';
export const name = 'removeDoctype';
export const description = 'removes doctype declaration';
/**
* Remove DOCTYPE declaration.
@@ -29,9 +25,9 @@ exports.description = 'removes doctype declaration';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
doctype: {
enter: (node, parentNode) => {

View File

@@ -1,12 +1,14 @@
'use strict';
import { editorNamespaces } from './_collections.js';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
const { editorNamespaces } = require('./_collections.js');
/**
* @typedef RemoveEditorsNSDataParams
* @property {string[]=} additionalNamespaces
*/
exports.type = 'visitor';
exports.name = 'removeEditorsNSData';
exports.active = true;
exports.description = 'removes editors namespaces, elements and attributes';
export const name = 'removeEditorsNSData';
export const description =
'removes editors namespaces, elements and attributes';
/**
* Remove editors namespaces, elements and attributes.
@@ -18,23 +20,19 @@ exports.description = 'removes editors namespaces, elements and attributes';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* additionalNamespaces?: Array<string>
* }>}
* @type {import('../lib/types.js').Plugin<RemoveEditorsNSDataParams>}
*/
exports.fn = (_root, params) => {
let namespaces = editorNamespaces;
export const fn = (_root, params) => {
let namespaces = [...editorNamespaces];
if (Array.isArray(params.additionalNamespaces)) {
namespaces = [...editorNamespaces, ...params.additionalNamespaces];
}
/**
* @type {Array<string>}
*/
/** @type {string[]} */
const prefixes = [];
return {
element: {
enter: (node, parentNode) => {
// collect namespace aliases from svg element
// collect namespace prefixes from svg element
if (node.name === 'svg') {
for (const [name, value] of Object.entries(node.attributes)) {
if (name.startsWith('xmlns:') && namespaces.includes(value)) {

View File

@@ -1,11 +1,13 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
/**
* @typedef RemoveElementsByAttrParams
* @property {string | string[]=} id
* @property {string | string[]=} class
*/
exports.name = 'removeElementsByAttr';
exports.type = 'visitor';
exports.active = false;
exports.description =
export const name = 'removeElementsByAttr';
export const description =
'removes arbitrary elements by ID or className (disabled by default)';
/**
@@ -39,20 +41,17 @@ exports.description =
*
* @author Eli Dupuis (@elidupuis)
*
* @type {import('../lib/types').Plugin<{
* id?: string | Array<string>,
* class?: string | Array<string>
* }>}
* @type {import('../lib/types.js').Plugin<RemoveElementsByAttrParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const ids =
params.id == null ? [] : Array.isArray(params.id) ? params.id : [params.id];
const classes =
params.class == null
? []
: Array.isArray(params.class)
? params.class
: [params.class];
? params.class
: [params.class];
return {
element: {
enter: (node, parentNode) => {

View File

@@ -1,20 +1,16 @@
'use strict';
import { attrsGroups } from './_collections.js';
const { attrsGroups } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'removeEmptyAttrs';
exports.active = true;
exports.description = 'removes empty attributes';
export const name = 'removeEmptyAttrs';
export const description = 'removes empty attributes';
/**
* Remove attributes with empty values.
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node) => {
@@ -22,7 +18,7 @@ exports.fn = () => {
if (
value === '' &&
// empty conditional processing attributes prevents elements from rendering
attrsGroups.conditionalProcessing.includes(name) === false
!attrsGroups.conditionalProcessing.has(name)
) {
delete node.attributes[name];
}

View File

@@ -1,12 +1,9 @@
'use strict';
import { elemsGroups } from './_collections.js';
import { detachNodeFromParent } from '../lib/xast.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';
const { detachNodeFromParent } = require('../lib/xast.js');
const { elemsGroups } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'removeEmptyContainers';
exports.active = true;
exports.description = 'removes empty container elements';
export const name = 'removeEmptyContainers';
export const description = 'removes empty container elements';
/**
* Remove empty containers.
@@ -21,16 +18,18 @@ exports.description = 'removes empty container elements';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = (root) => {
const stylesheet = collectStylesheet(root);
return {
element: {
exit: (node, parentNode) => {
// remove only empty non-svg containers
if (
node.name === 'svg' ||
elemsGroups.container.includes(node.name) === false ||
!elemsGroups.container.has(node.name) ||
node.children.length !== 0
) {
return;
@@ -42,15 +41,25 @@ exports.fn = () => {
) {
return;
}
// The <g> may not have content, but the filter may cause a rectangle
// to be created and filled with pattern.
if (node.name === 'g' && node.attributes.filter != null) {
return;
}
// empty <mask> hides masked element
if (node.name === 'mask' && node.attributes.id != null) {
return;
}
if (parentNode.type === 'element' && parentNode.name === 'switch') {
return;
}
// The <g> may not have content, but the filter may cause a rectangle
// to be created and filled with pattern.
if (
node.name === 'g' &&
(node.attributes.filter != null ||
computeStyle(stylesheet, node).filter)
) {
return;
}
detachNodeFromParent(node, parentNode);
},
},

View File

@@ -1,11 +1,14 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
/**
* @typedef RemoveEmptyTextParams
* @property {boolean=} text
* @property {boolean=} tspan
* @property {boolean=} tref
*/
exports.name = 'removeEmptyText';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes empty <text> elements';
export const name = 'removeEmptyText';
export const description = 'removes empty <text> elements';
/**
* Remove empty Text elements.
@@ -24,13 +27,9 @@ exports.description = 'removes empty <text> elements';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* text?: boolean,
* tspan?: boolean,
* tref?: boolean
* }>}
* @type {import('../lib/types.js').Plugin<RemoveEmptyTextParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const { text = true, tspan = true, tref = true } = params;
return {
element: {

View File

@@ -1,17 +1,33 @@
'use strict';
import { elemsGroups } from './_collections.js';
import { detachNodeFromParent, querySelector } from '../lib/xast.js';
import { visit, visitSkip } from '../lib/util/visit.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';
import { parsePathData } from '../lib/path.js';
import { findReferences, hasScripts } from '../lib/svgo/tools.js';
const {
querySelector,
closestByName,
detachNodeFromParent,
} = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { parsePathData } = require('../lib/path.js');
/**
* @typedef RemoveHiddenElemsParams
* @property {boolean=} isHidden
* @property {boolean=} displayNone
* @property {boolean=} opacity0
* @property {boolean=} circleR0
* @property {boolean=} ellipseRX0
* @property {boolean=} ellipseRY0
* @property {boolean=} rectWidth0
* @property {boolean=} rectHeight0
* @property {boolean=} patternWidth0
* @property {boolean=} patternHeight0
* @property {boolean=} imageWidth0
* @property {boolean=} imageHeight0
* @property {boolean=} pathEmptyD
* @property {boolean=} polylineEmptyPoints
* @property {boolean=} polygonEmptyPoints
*/
exports.name = 'removeHiddenElems';
exports.type = 'visitor';
exports.active = true;
exports.description =
const nonRendering = elemsGroups.nonRendering;
export const name = 'removeHiddenElems';
export const description =
'removes hidden elements (zero sized, with absent attributes)';
/**
@@ -29,25 +45,9 @@ exports.description =
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* isHidden: boolean,
* displayNone: boolean,
* opacity0: boolean,
* circleR0: boolean,
* ellipseRX0: boolean,
* ellipseRY0: boolean,
* rectWidth0: boolean,
* rectHeight0: boolean,
* patternWidth0: boolean,
* patternHeight0: boolean,
* imageWidth0: boolean,
* imageHeight0: boolean,
* pathEmptyD: boolean,
* polylineEmptyPoints: boolean,
* polygonEmptyPoints: boolean,
* }>}
* @type {import('../lib/types.js').Plugin<RemoveHiddenElemsParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const {
isHidden = true,
displayNone = true,
@@ -67,9 +67,129 @@ exports.fn = (root, params) => {
} = params;
const stylesheet = collectStylesheet(root);
/**
* Skip non-rendered nodes initially, and only detach if they have no ID, or
* their ID is not referenced by another node.
*
* @type {Map<import('../lib/types.js').XastElement, import('../lib/types.js').XastParent>}
*/
const nonRenderedNodes = new Map();
/**
* IDs for removed hidden definitions.
*
* @type {Set<string>}
*/
const removedDefIds = new Set();
/** @type {Map<import('../lib/types.js').XastElement, import('../lib/types.js').XastParent>} */
const allDefs = new Map();
/** @type {Set<string>} */
const allReferences = new Set();
/** @type {Map<string, Array<{ node: import('../lib/types.js').XastElement, parentNode: import('../lib/types.js').XastParent }>>} */
const referencesById = new Map();
/**
* If styles are present, we can't be sure if a definition is unused or not
*/
let deoptimized = false;
/**
* Nodes can't be removed if they or any of their children have an id attribute that is referenced.
* @param {import('../lib/types.js').XastElement} node
* @returns boolean
*/
function canRemoveNonRenderingNode(node) {
if (allReferences.has(node.attributes.id)) {
return false;
}
for (const child of node.children) {
if (child.type === 'element' && !canRemoveNonRenderingNode(child)) {
return false;
}
}
return true;
}
/**
* @param {import('../lib/types.js').XastChild} node
* @param {import('../lib/types.js').XastParent} parentNode
*/
function removeElement(node, parentNode) {
if (
node.type === 'element' &&
node.attributes.id != null &&
parentNode.type === 'element' &&
parentNode.name === 'defs'
) {
removedDefIds.add(node.attributes.id);
}
detachNodeFromParent(node, parentNode);
}
visit(root, {
element: {
enter: (node, parentNode) => {
// transparent non-rendering elements still apply where referenced
if (nonRendering.has(node.name)) {
nonRenderedNodes.set(node, parentNode);
return visitSkip;
}
const computedStyle = computeStyle(stylesheet, node);
// opacity="0"
//
// https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties
if (
opacity0 &&
computedStyle.opacity &&
computedStyle.opacity.type === 'static' &&
computedStyle.opacity.value === '0'
) {
if (node.name === 'path') {
nonRenderedNodes.set(node, parentNode);
return visitSkip;
}
removeElement(node, parentNode);
}
},
},
});
return {
element: {
enter: (node, parentNode) => {
if (
(node.name === 'style' && node.children.length !== 0) ||
hasScripts(node)
) {
deoptimized = true;
return;
}
if (node.name === 'defs') {
allDefs.set(node, parentNode);
}
if (node.name === 'use') {
for (const attr of Object.keys(node.attributes)) {
if (attr !== 'href' && !attr.endsWith(':href')) {
continue;
}
const value = node.attributes[attr];
const id = value.slice(1);
let refs = referencesById.get(id);
if (!refs) {
refs = [];
referencesById.set(id, refs);
}
refs.push({ node, parentNode });
}
}
// Removes hidden elements
// https://www.w3schools.com/cssref/pr_class_visibility.asp
const computedStyle = computeStyle(stylesheet, node);
@@ -81,7 +201,7 @@ exports.fn = (root, params) => {
// keep if any descendant enables visibility
querySelector(node, '[visibility=visible]') == null
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -98,22 +218,7 @@ exports.fn = (root, params) => {
// markers with display: none still rendered
node.name !== 'marker'
) {
detachNodeFromParent(node, parentNode);
return;
}
// opacity="0"
//
// https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties
if (
opacity0 &&
computedStyle.opacity &&
computedStyle.opacity.type === 'static' &&
computedStyle.opacity.value === '0' &&
// transparent element inside clipPath still affect clipped elements
closestByName(node, 'clipPath') == null
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -129,7 +234,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.r === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -145,7 +250,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.rx === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -161,7 +266,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.ry === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -177,7 +282,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.width === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -194,7 +299,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.height === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -209,7 +314,7 @@ exports.fn = (root, params) => {
node.name === 'pattern' &&
node.attributes.width === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -224,7 +329,7 @@ exports.fn = (root, params) => {
node.name === 'pattern' &&
node.attributes.height === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -239,7 +344,7 @@ exports.fn = (root, params) => {
node.name === 'image' &&
node.attributes.width === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -254,7 +359,7 @@ exports.fn = (root, params) => {
node.name === 'image' &&
node.attributes.height === '0'
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -265,12 +370,12 @@ exports.fn = (root, params) => {
// <path d=""/>
if (pathEmptyD && node.name === 'path') {
if (node.attributes.d == null) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
const pathData = parsePathData(node.attributes.d);
if (pathData.length === 0) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
// keep single point paths for markers
@@ -279,10 +384,9 @@ exports.fn = (root, params) => {
computedStyle['marker-start'] == null &&
computedStyle['marker-end'] == null
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
return;
}
// Polyline with empty points
@@ -295,7 +399,7 @@ exports.fn = (root, params) => {
node.name === 'polyline' &&
node.attributes.points == null
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
@@ -309,9 +413,46 @@ exports.fn = (root, params) => {
node.name === 'polygon' &&
node.attributes.points == null
) {
detachNodeFromParent(node, parentNode);
removeElement(node, parentNode);
return;
}
for (const [name, value] of Object.entries(node.attributes)) {
const ids = findReferences(name, value);
for (const id of ids) {
allReferences.add(id);
}
}
},
},
root: {
exit: () => {
for (const id of removedDefIds) {
const refs = referencesById.get(id);
if (refs) {
for (const { node, parentNode } of refs) {
detachNodeFromParent(node, parentNode);
}
}
}
if (!deoptimized) {
for (const [
nonRenderedNode,
nonRenderedParent,
] of nonRenderedNodes.entries()) {
if (canRemoveNonRenderingNode(nonRenderedNode)) {
detachNodeFromParent(nonRenderedNode, nonRenderedParent);
}
}
}
for (const [node, parentNode] of allDefs.entries()) {
if (node.children.length === 0) {
detachNodeFromParent(node, parentNode);
}
}
},
},
};

View File

@@ -1,11 +1,7 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'removeMetadata';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes <metadata>';
export const name = 'removeMetadata';
export const description = 'removes <metadata>';
/**
* Remove <metadata>.
@@ -14,9 +10,9 @@ exports.description = 'removes <metadata>';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node, parentNode) => {

View File

@@ -1,38 +1,36 @@
'use strict';
exports.name = 'removeNonInheritableGroupAttrs';
exports.type = 'perItem';
exports.active = true;
exports.description =
'removes non-inheritable groups presentational attributes';
const {
inheritableAttrs,
import {
attrsGroups,
inheritableAttrs,
presentationNonInheritableGroupAttrs,
} = require('./_collections');
} from './_collections.js';
export const name = 'removeNonInheritableGroupAttrs';
export const description =
"removes non-inheritable group's presentational attributes";
/**
* Remove non-inheritable group's "presentation" attributes.
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
*
* @author Kir Belevich
*
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = function (item) {
if (item.type === 'element' && item.name === 'g') {
for (const name of Object.keys(item.attributes)) {
if (
attrsGroups.presentation.includes(name) === true &&
inheritableAttrs.includes(name) === false &&
presentationNonInheritableGroupAttrs.includes(name) === false
) {
delete item.attributes[name];
}
}
}
export const fn = () => {
return {
element: {
enter: (node) => {
if (node.name === 'g') {
for (const name of Object.keys(node.attributes)) {
if (
attrsGroups.presentation.has(name) &&
!inheritableAttrs.has(name) &&
!presentationNonInheritableGroupAttrs.has(name)
) {
delete node.attributes[name];
}
}
}
},
},
};
};

View File

@@ -1,29 +1,22 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
import { visitSkip } from '../lib/util/visit.js';
import { parsePathData } from '../lib/path.js';
import { intersects } from './_path.js';
export const name = 'removeOffCanvasPaths';
export const description =
'removes elements that are drawn outside of the viewBox (disabled by default)';
/**
* @typedef {import('../lib/types').PathDataItem} PathDataItem
*/
const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const { parsePathData } = require('../lib/path.js');
const { intersects } = require('./_path.js');
exports.type = 'visitor';
exports.name = 'removeOffCanvasPaths';
exports.active = false;
exports.description =
'removes elements that are drawn outside of the viewbox (disabled by default)';
/**
* Remove elements that are drawn outside of the viewbox.
* Remove elements that are drawn outside of the viewBox.
*
* @author JoshyPHP
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
/**
* @type {null | {
* @type {?{
* top: number,
* right: number,
* bottom: number,
@@ -39,7 +32,7 @@ exports.fn = () => {
enter: (node, parentNode) => {
if (node.name === 'svg' && parentNode.type === 'root') {
let viewBox = '';
// find viewbox
// find viewBox
if (node.attributes.viewBox != null) {
// remove commas and plus signs, normalize and trim whitespace
viewBox = node.attributes.viewBox;
@@ -50,7 +43,7 @@ exports.fn = () => {
viewBox = `0 0 ${node.attributes.width} ${node.attributes.height}`;
}
// parse viewbox
// parse viewBox
// remove commas and plus signs, normalize and trim whitespace
viewBox = viewBox
.replace(/[,+]|px/g, ' ')
@@ -59,7 +52,7 @@ exports.fn = () => {
// ensure that the dimensions are 4 values separated by space
const m =
/^(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+)$/.exec(
viewBox
viewBox,
);
if (m == null) {
return;
@@ -92,7 +85,7 @@ exports.fn = () => {
) {
const pathData = parsePathData(node.attributes.d);
// consider that a M command within the viewBox is visible
// consider that an M command within the viewBox is visible
let visible = false;
for (const pathDataItem of pathData) {
if (pathDataItem.command === 'M') {
@@ -117,9 +110,7 @@ exports.fn = () => {
}
const { left, top, width, height } = viewBoxData;
/**
* @type {Array<PathDataItem>}
*/
/** @type {ReadonlyArray<import('../lib/types.js').PathDataItem>} */
const viewBoxPathData = [
{ command: 'M', args: [left, top] },
{ command: 'h', args: [width] },

View File

@@ -1,11 +1,7 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'removeRasterImages';
exports.type = 'visitor';
exports.active = false;
exports.description = 'removes raster images (disabled by default)';
export const name = 'removeRasterImages';
export const description = 'removes raster images (disabled by default)';
/**
* Remove raster images references in <image>.
@@ -14,16 +10,16 @@ exports.description = 'removes raster images (disabled by default)';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node, parentNode) => {
if (
node.name === 'image' &&
node.attributes['xlink:href'] != null &&
/(\.|image\/)(jpg|png|gif)/.test(node.attributes['xlink:href'])
/(\.|image\/)(jpe?g|png|gif)/.test(node.attributes['xlink:href'])
) {
detachNodeFromParent(node, parentNode);
}

View File

@@ -1,29 +0,0 @@
'use strict';
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'removeScriptElement';
exports.type = 'visitor';
exports.active = false;
exports.description = 'removes <script> elements (disabled by default)';
/**
* Remove <script>.
*
* https://www.w3.org/TR/SVG11/script.html
*
* @author Patrick Klingemann
*
* @type {import('../lib/types').Plugin<void>}
*/
exports.fn = () => {
return {
element: {
enter: (node, parentNode) => {
if (node.name === 'script') {
detachNodeFromParent(node, parentNode);
}
},
},
};
};

63
node_modules/svgo/plugins/removeScripts.js generated vendored Normal file
View File

@@ -0,0 +1,63 @@
import { attrsGroups } from './_collections.js';
import { detachNodeFromParent } from '../lib/xast.js';
export const name = 'removeScripts';
export const description = 'removes scripts (disabled by default)';
/** Union of all event attributes. */
const eventAttrs = [
...attrsGroups.animationEvent,
...attrsGroups.documentEvent,
...attrsGroups.documentElementEvent,
...attrsGroups.globalEvent,
...attrsGroups.graphicalEvent,
];
/**
* Remove scripts.
*
* https://www.w3.org/TR/SVG11/script.html
*
* @author Patrick Klingemann
* @type {import('../lib/types.js').Plugin}
*/
export const fn = () => {
return {
element: {
enter: (node, parentNode) => {
if (node.name === 'script') {
detachNodeFromParent(node, parentNode);
return;
}
for (const attr of eventAttrs) {
if (node.attributes[attr] != null) {
delete node.attributes[attr];
}
}
},
exit: (node, parentNode) => {
if (node.name !== 'a') {
return;
}
for (const attr of Object.keys(node.attributes)) {
if (attr === 'href' || attr.endsWith(':href')) {
if (
node.attributes[attr] == null ||
!node.attributes[attr].trimStart().startsWith('javascript:')
) {
continue;
}
const index = parentNode.children.indexOf(node);
const usefulChildren = node.children.filter(
(child) => child.type !== 'text',
);
parentNode.children.splice(index, 1, ...usefulChildren);
}
}
},
},
};
};

View File

@@ -1,11 +1,7 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'removeStyleElement';
exports.type = 'visitor';
exports.active = false;
exports.description = 'removes <style> element (disabled by default)';
export const name = 'removeStyleElement';
export const description = 'removes <style> element (disabled by default)';
/**
* Remove <style>.
@@ -14,9 +10,9 @@ exports.description = 'removes <style> element (disabled by default)';
*
* @author Betsy Dupuis
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node, parentNode) => {

View File

@@ -1,22 +1,18 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'removeTitle';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes <title>';
export const name = 'removeTitle';
export const description = 'removes <title>';
/**
* Remove <title>.
*
* https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title
* https://developer.mozilla.org/docs/Web/SVG/Element/title
*
* @author Igor Kalashnikov
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node, parentNode) => {

View File

@@ -1,40 +1,44 @@
'use strict';
const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const {
elems,
import {
attrsGroups,
elemsGroups,
attrsGroupsDefaults,
elems,
elemsGroups,
presentationNonInheritableGroupAttrs,
} = require('./_collections');
} from './_collections.js';
import { detachNodeFromParent } from '../lib/xast.js';
import { visitSkip } from '../lib/util/visit.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';
exports.type = 'visitor';
exports.name = 'removeUnknownsAndDefaults';
exports.active = true;
exports.description =
/**
* @typedef RemoveUnknownsAndDefaultsParams
* @property {boolean=} unknownContent
* @property {boolean=} unknownAttrs
* @property {boolean=} defaultAttrs
* @property {boolean=} defaultMarkupDeclarations
* If to remove XML declarations that are assigned their default value. XML
* declarations are the properties in the `<?xml … ?>` block at the top of the
* document.
* @property {boolean=} uselessOverrides
* @property {boolean=} keepDataAttrs
* @property {boolean=} keepAriaAttrs
* @property {boolean=} keepRoleAttr
*/
export const name = 'removeUnknownsAndDefaults';
export const description =
'removes unknown elements content and attributes, removes attrs with default values';
// resolve all groups references
/**
* @type {Map<string, Set<string>>}
*/
/** @type {Map<string, Set<string>>} */
const allowedChildrenPerElement = new Map();
/**
* @type {Map<string, Set<string>>}
*/
/** @type {Map<string, Set<string>>} */
const allowedAttributesPerElement = new Map();
/**
* @type {Map<string, Map<string, string>>}
*/
/** @type {Map<string, Map<string, string>>} */
const attributesDefaultsPerElement = new Map();
for (const [name, config] of Object.entries(elems)) {
/**
* @type {Set<string>}
*/
/** @type {Set<string>} */
const allowedChildren = new Set();
if (config.content) {
for (const elementName of config.content) {
@@ -51,18 +55,14 @@ for (const [name, config] of Object.entries(elems)) {
}
}
}
/**
* @type {Set<string>}
*/
/** @type {Set<string>} */
const allowedAttributes = new Set();
if (config.attrs) {
for (const attrName of config.attrs) {
allowedAttributes.add(attrName);
}
}
/**
* @type {Map<string, string>}
*/
/** @type {Map<string, string>} */
const attributesDefaults = new Map();
if (config.defaults) {
for (const [attrName, defaultValue] of Object.entries(config.defaults)) {
@@ -94,21 +94,14 @@ for (const [name, config] of Object.entries(elems)) {
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* unknownContent?: boolean,
* unknownAttrs?: boolean,
* defaultAttrs?: boolean,
* uselessOverrides?: boolean,
* keepDataAttrs?: boolean,
* keepAriaAttrs?: boolean,
* keepRoleAttr?: boolean,
* }>}
* @type {import('../lib/types.js').Plugin<RemoveUnknownsAndDefaultsParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const {
unknownContent = true,
unknownAttrs = true,
defaultAttrs = true,
defaultMarkupDeclarations = true,
uselessOverrides = true,
keepDataAttrs = true,
keepAriaAttrs = true,
@@ -117,6 +110,13 @@ exports.fn = (root, params) => {
const stylesheet = collectStylesheet(root);
return {
instruction: {
enter: (node) => {
if (defaultMarkupDeclarations) {
node.value = node.value.replace(/\s*standalone\s*=\s*(["'])no\1/, '');
}
},
},
element: {
enter: (node, parentNode) => {
// skip namespaced elements
@@ -131,7 +131,7 @@ exports.fn = (root, params) => {
// remove unknown element's content
if (unknownContent && parentNode.type === 'element') {
const allowedChildren = allowedChildrenPerElement.get(
parentNode.name
parentNode.name,
);
if (allowedChildren == null || allowedChildren.size === 0) {
// remove unknown elements
@@ -192,18 +192,14 @@ exports.fn = (root, params) => {
attributesDefaults.get(name) === value
) {
// keep defaults if parent has own or inherited style
if (
computedParentStyle == null ||
computedParentStyle[name] == null
) {
if (computedParentStyle?.[name] == null) {
delete node.attributes[name];
}
}
if (uselessOverrides && node.attributes.id == null) {
const style =
computedParentStyle == null ? null : computedParentStyle[name];
const style = computedParentStyle?.[name];
if (
presentationNonInheritableGroupAttrs.includes(name) === false &&
presentationNonInheritableGroupAttrs.has(name) === false &&
style != null &&
style.type === 'static' &&
style.value === value

View File

@@ -1,9 +1,5 @@
'use strict';
exports.type = 'visitor';
exports.name = 'removeUnusedNS';
exports.active = true;
exports.description = 'removes unused namespaces declaration';
export const name = 'removeUnusedNS';
export const description = 'removes unused namespaces declaration';
/**
* Remove unused namespaces declaration from svg element
@@ -11,12 +7,10 @@ exports.description = 'removes unused namespaces declaration';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
/**
* @type {Set<string>}
*/
export const fn = () => {
/** @type {Set<string>} */
const unusedNamespaces = new Set();
return {
element: {

View File

@@ -1,48 +1,32 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
import { elemsGroups } from './_collections.js';
/**
* @typedef {import('../lib/types').XastElement} XastElement
*/
const { detachNodeFromParent } = require('../lib/xast.js');
const { elemsGroups } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'removeUselessDefs';
exports.active = true;
exports.description = 'removes elements in <defs> without id';
export const name = 'removeUselessDefs';
export const description = 'removes elements in <defs> without id';
/**
* Removes content of defs and properties that aren't rendered directly without ids.
*
* @author Lev Solntsev
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node, parentNode) => {
if (node.name === 'defs') {
/**
* @type {Array<XastElement>}
*/
if (
node.name === 'defs' ||
(elemsGroups.nonRendering.has(node.name) &&
node.attributes.id == null)
) {
/** @type {import('../lib/types.js').XastElement[]} */
const usefulNodes = [];
collectUsefulNodes(node, usefulNodes);
if (usefulNodes.length === 0) {
detachNodeFromParent(node, parentNode);
}
// TODO remove in SVGO 3
for (const usefulNode of usefulNodes) {
// @ts-ignore parentNode is legacy
usefulNode.parentNode = node;
}
node.children = usefulNodes;
} else if (
elemsGroups.nonRendering.includes(node.name) &&
node.attributes.id == null
) {
detachNodeFromParent(node, parentNode);
}
},
},
@@ -50,7 +34,8 @@ exports.fn = () => {
};
/**
* @type {(node: XastElement, usefulNodes: Array<XastElement>) => void}
* @param {import('../lib/types.js').XastElement} node
* @param {import('../lib/types.js').XastElement[]} usefulNodes
*/
const collectUsefulNodes = (node, usefulNodes) => {
for (const child of node.children) {

View File

@@ -1,38 +1,39 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
import { visit, visitSkip } from '../lib/util/visit.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';
import { hasScripts } from '../lib/svgo/tools.js';
import { elemsGroups } from './_collections.js';
const { visit, visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { elemsGroups } = require('./_collections.js');
/**
* @typedef RemoveUselessStrokeAndFillParams
* @property {boolean=} stroke
* @property {boolean=} fill
* @property {boolean=} removeNone
*/
exports.type = 'visitor';
exports.name = 'removeUselessStrokeAndFill';
exports.active = true;
exports.description = 'removes useless stroke and fill attributes';
export const name = 'removeUselessStrokeAndFill';
export const description = 'removes useless stroke and fill attributes';
/**
* Remove useless stroke and fill attrs.
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* stroke?: boolean,
* fill?: boolean,
* removeNone?: boolean
* }>}
* @type {import('../lib/types.js').Plugin<RemoveUselessStrokeAndFillParams>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const {
stroke: removeStroke = true,
fill: removeFill = true,
removeNone = false,
} = params;
// style and script elements deoptimise this plugin
// style and script elements deoptimize this plugin
let hasStyleOrScript = false;
visit(root, {
element: {
enter: (node) => {
if (node.name === 'style' || node.name === 'script') {
if (node.name === 'style' || hasScripts(node)) {
hasStyleOrScript = true;
}
},
@@ -47,11 +48,11 @@ exports.fn = (root, params) => {
return {
element: {
enter: (node, parentNode) => {
// id attribute deoptimise the whole subtree
// id attribute deoptimize the whole subtree
if (node.attributes.id != null) {
return visitSkip;
}
if (elemsGroups.shape.includes(node.name) == false) {
if (!elemsGroups.shape.has(node.name)) {
return;
}
const computedStyle = computeStyle(stylesheet, node);

View File

@@ -1,11 +1,7 @@
'use strict';
export const name = 'removeViewBox';
export const description = 'removes viewBox attribute when possible';
exports.type = 'visitor';
exports.name = 'removeViewBox';
exports.active = true;
exports.description = 'removes viewBox attribute when possible';
const viewBoxElems = ['svg', 'pattern', 'symbol'];
const viewBoxElems = new Set(['pattern', 'svg', 'symbol']);
/**
* Remove viewBox attr which coincides with a width/height box.
@@ -14,19 +10,19 @@ const viewBoxElems = ['svg', 'pattern', 'symbol'];
*
* @example
* <svg width="100" height="50" viewBox="0 0 100 50">
*
* ⬇
* <svg width="100" height="50">
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node, parentNode) => {
if (
viewBoxElems.includes(node.name) &&
viewBoxElems.has(node.name) &&
node.attributes.viewBox != null &&
node.attributes.width != null &&
node.attributes.height != null

View File

@@ -1,12 +1,5 @@
'use strict';
exports.name = 'removeXMLNS';
exports.type = 'perItem';
exports.active = false;
exports.description =
export const name = 'removeXMLNS';
export const description =
'removes xmlns attribute (for inline svg, disabled by default)';
/**
@@ -17,14 +10,18 @@ exports.description =
* ↓
* <svg viewBox="0 0 100 50">
*
* @param {Object} item current iteration item
* @return {Boolean} if true, xmlns will be filtered out
*
* @author Ricardo Tomasi
*
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = function (item) {
if (item.type === 'element' && item.name === 'svg') {
delete item.attributes.xmlns;
delete item.attributes['xmlns:xlink'];
}
export const fn = () => {
return {
element: {
enter: (node) => {
if (node.name === 'svg') {
delete node.attributes.xmlns;
}
},
},
};
};

View File

@@ -1,11 +1,7 @@
'use strict';
import { detachNodeFromParent } from '../lib/xast.js';
const { detachNodeFromParent } = require('../lib/xast.js');
exports.name = 'removeXMLProcInst';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes XML processing instructions';
export const name = 'removeXMLProcInst';
export const description = 'removes XML processing instructions';
/**
* Remove XML Processing Instruction.
@@ -15,9 +11,9 @@ exports.description = 'removes XML processing instructions';
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
instruction: {
enter: (node, parentNode) => {

227
node_modules/svgo/plugins/removeXlink.js generated vendored Normal file
View File

@@ -0,0 +1,227 @@
import { elems } from './_collections.js';
/**
* @typedef RemoveXlinkParams
* @property {boolean=} includeLegacy
* By default this plugin ignores legacy elements that were deprecated or
* removed in SVG 2. Set to true to force performing operations on those too.
*/
export const name = 'removeXlink';
export const description =
'remove xlink namespace and replaces attributes with the SVG 2 equivalent where applicable';
/** URI indicating the Xlink namespace. */
const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink';
/**
* Map of `xlink:show` values to the SVG 2 `target` attribute values.
*
* @type {Record<string, string>}
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:show#usage_notes
*/
const SHOW_TO_TARGET = {
new: '_blank',
replace: '_self',
};
/**
* Elements that use xlink:href, but were deprecated in SVG 2 and therefore
* don't support the SVG 2 href attribute.
*
* @type {Set<string>}
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/href
*/
const LEGACY_ELEMENTS = new Set([
'cursor',
'filter',
'font-face-uri',
'glyphRef',
'tref',
]);
/**
* @param {import('../lib/types.js').XastElement} node
* @param {ReadonlyArray<string>} prefixes
* @param {string} attr
* @returns {string[]}
*/
const findPrefixedAttrs = (node, prefixes, attr) => {
return prefixes
.map((prefix) => `${prefix}:${attr}`)
.filter((attr) => node.attributes[attr] != null);
};
/**
* Removes XLink namespace prefixes and converts references to XLink attributes
* to the native SVG equivalent.
*
* XLink namespace is deprecated in SVG 2.
*
* @type {import('../lib/types.js').Plugin<RemoveXlinkParams>}
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href
*/
export const fn = (_, params) => {
const { includeLegacy } = params;
/**
* XLink namespace prefixes that are currently in the stack.
*
* @type {string[]}
*/
const xlinkPrefixes = [];
/**
* Namespace prefixes that exist in {@link xlinkPrefixes} but were overridden
* in a child element to point to another namespace, and is not treated as an
* XLink attribute.
*
* @type {string[]}
*/
const overriddenPrefixes = [];
/**
* Namespace prefixes that were used in one of the {@link LEGACY_ELEMENTS}.
*
* @type {string[]}
*/
const usedInLegacyElement = [];
return {
element: {
enter: (node) => {
for (const [key, value] of Object.entries(node.attributes)) {
if (key.startsWith('xmlns:')) {
const prefix = key.split(':', 2)[1];
if (value === XLINK_NAMESPACE) {
xlinkPrefixes.push(prefix);
continue;
}
if (xlinkPrefixes.includes(prefix)) {
overriddenPrefixes.push(prefix);
}
}
}
if (
overriddenPrefixes.some((prefix) => xlinkPrefixes.includes(prefix))
) {
return;
}
const showAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'show');
let showHandled = node.attributes.target != null;
for (let i = showAttrs.length - 1; i >= 0; i--) {
const attr = showAttrs[i];
const value = node.attributes[attr];
const mapping = SHOW_TO_TARGET[value];
if (showHandled || mapping == null) {
delete node.attributes[attr];
continue;
}
if (mapping !== elems[node.name]?.defaults?.target) {
node.attributes.target = mapping;
}
delete node.attributes[attr];
showHandled = true;
}
const titleAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'title');
for (let i = titleAttrs.length - 1; i >= 0; i--) {
const attr = titleAttrs[i];
const value = node.attributes[attr];
const hasTitle = node.children.filter(
(child) => child.type === 'element' && child.name === 'title',
);
if (hasTitle.length > 0) {
delete node.attributes[attr];
continue;
}
/** @type {import('../lib/types.js').XastElement} */
const titleTag = {
type: 'element',
name: 'title',
attributes: {},
children: [
{
type: 'text',
value,
},
],
};
Object.defineProperty(titleTag, 'parentNode', {
writable: true,
value: node,
});
node.children.unshift(titleTag);
delete node.attributes[attr];
}
const hrefAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'href');
if (
hrefAttrs.length > 0 &&
LEGACY_ELEMENTS.has(node.name) &&
!includeLegacy
) {
hrefAttrs
.map((attr) => attr.split(':', 1)[0])
.forEach((prefix) => usedInLegacyElement.push(prefix));
return;
}
for (let i = hrefAttrs.length - 1; i >= 0; i--) {
const attr = hrefAttrs[i];
const value = node.attributes[attr];
if (node.attributes.href != null) {
delete node.attributes[attr];
continue;
}
node.attributes.href = value;
delete node.attributes[attr];
}
},
exit: (node) => {
for (const [key, value] of Object.entries(node.attributes)) {
const [prefix, attr] = key.split(':', 2);
if (
xlinkPrefixes.includes(prefix) &&
!overriddenPrefixes.includes(prefix) &&
!usedInLegacyElement.includes(prefix) &&
!includeLegacy
) {
delete node.attributes[key];
continue;
}
if (key.startsWith('xmlns:') && !usedInLegacyElement.includes(attr)) {
if (value === XLINK_NAMESPACE) {
const index = xlinkPrefixes.indexOf(attr);
xlinkPrefixes.splice(index, 1);
delete node.attributes[key];
continue;
}
if (overriddenPrefixes.includes(prefix)) {
const index = overriddenPrefixes.indexOf(attr);
overriddenPrefixes.splice(index, 1);
}
}
}
},
},
};
};

View File

@@ -1,17 +1,8 @@
'use strict';
import { collectStylesheet } from '../lib/style.js';
import { detachNodeFromParent, querySelectorAll } from '../lib/xast.js';
/**
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').XastParent} XastParent
* @typedef {import('../lib/types').XastNode} XastNode
*/
const JSAPI = require('../lib/svgo/jsAPI.js');
exports.type = 'visitor';
exports.name = 'reusePaths';
exports.active = false;
exports.description =
export const name = 'reusePaths';
export const description =
'Finds <path> elements with the same d, fill, and ' +
'stroke, and converts them to <use> elements ' +
'referencing a single <path> def.';
@@ -22,17 +13,33 @@ exports.description =
*
* @author Jacob Howcroft
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
/**
* @type {Map<string, Array<XastElement>>}
*/
export const fn = (root) => {
const stylesheet = collectStylesheet(root);
/** @type {Map<string, import('../lib/types.js').XastElement[]>} */
const paths = new Map();
/**
* Reference to the first defs element that is a direct child of the svg
* element if one exists.
*
* @type {import('../lib/types.js').XastElement}
* @see https://developer.mozilla.org/docs/Web/SVG/Element/defs
*/
let svgDefs;
/**
* Set of hrefs that reference the id of another node.
*
* @type {Set<string>}
*/
const hrefs = new Set();
return {
element: {
enter: (node) => {
enter: (node, parentNode) => {
if (node.name === 'path' && node.attributes.d != null) {
const d = node.attributes.d;
const fill = node.attributes.fill || '';
@@ -45,58 +52,110 @@ exports.fn = () => {
}
list.push(node);
}
if (
svgDefs == null &&
node.name === 'defs' &&
parentNode.type === 'element' &&
parentNode.name === 'svg'
) {
svgDefs = node;
}
if (node.name === 'use') {
for (const name of ['href', 'xlink:href']) {
const href = node.attributes[name];
if (href != null && href.startsWith('#') && href.length > 1) {
hrefs.add(href.slice(1));
}
}
}
},
exit: (node, parentNode) => {
if (node.name === 'svg' && parentNode.type === 'root') {
/**
* @type {XastElement}
*/
const rawDefs = {
type: 'element',
name: 'defs',
attributes: {},
children: [],
};
/**
* @type {XastElement}
*/
const defsTag = new JSAPI(rawDefs, node);
let defsTag = svgDefs;
if (defsTag == null) {
defsTag = {
type: 'element',
name: 'defs',
attributes: {},
children: [],
};
}
let index = 0;
for (const list of paths.values()) {
if (list.length > 1) {
// add reusable path to defs
/**
* @type {XastElement}
*/
const rawPath = {
/** @type {import('../lib/types.js').XastElement} */
const reusablePath = {
type: 'element',
name: 'path',
attributes: { ...list[0].attributes },
attributes: {},
children: [],
};
delete rawPath.attributes.transform;
let id;
if (rawPath.attributes.id == null) {
id = 'reuse-' + index;
index += 1;
rawPath.attributes.id = id;
for (const attr of ['fill', 'stroke', 'd']) {
if (list[0].attributes[attr] != null) {
reusablePath.attributes[attr] = list[0].attributes[attr];
}
}
const originalId = list[0].attributes.id;
if (
originalId == null ||
hrefs.has(originalId) ||
stylesheet.rules.some(
(rule) => rule.selector === `#${originalId}`,
)
) {
reusablePath.attributes.id = 'reuse-' + index++;
} else {
id = rawPath.attributes.id;
reusablePath.attributes.id = originalId;
delete list[0].attributes.id;
}
/**
* @type {XastElement}
*/
const reusablePath = new JSAPI(rawPath, defsTag);
defsTag.children.push(reusablePath);
// convert paths to <use>
for (const pathNode of list) {
pathNode.name = 'use';
pathNode.attributes['xlink:href'] = '#' + id;
delete pathNode.attributes.d;
delete pathNode.attributes.stroke;
delete pathNode.attributes.fill;
if (
defsTag.children.includes(pathNode) &&
pathNode.children.length === 0
) {
if (Object.keys(pathNode.attributes).length === 0) {
detachNodeFromParent(pathNode, defsTag);
continue;
}
if (
Object.keys(pathNode.attributes).length === 1 &&
pathNode.attributes.id != null
) {
detachNodeFromParent(pathNode, defsTag);
const selector = `[xlink\\:href=#${pathNode.attributes.id}], [href=#${pathNode.attributes.id}]`;
for (const child of querySelectorAll(node, selector)) {
if (child.type !== 'element') {
continue;
}
for (const name of ['href', 'xlink:href']) {
if (child.attributes[name] != null) {
child.attributes[name] =
'#' + reusablePath.attributes.id;
}
}
}
continue;
}
}
pathNode.name = 'use';
pathNode.attributes['xlink:href'] =
'#' + reusablePath.attributes.id;
}
}
}
@@ -104,7 +163,10 @@ exports.fn = () => {
if (node.attributes['xmlns:xlink'] == null) {
node.attributes['xmlns:xlink'] = 'http://www.w3.org/1999/xlink';
}
node.children.unshift(defsTag);
if (svgDefs == null) {
node.children.unshift(defsTag);
}
}
}
},

View File

@@ -1,21 +1,20 @@
'use strict';
/**
* @typedef SortAttrsParams
* @property {ReadonlyArray<string>=} order
* @property {'front' | 'alphabetical'=} xmlnsOrder
*/
exports.type = 'visitor';
exports.name = 'sortAttrs';
exports.active = false;
exports.description = 'Sort element attributes for better compression';
export const name = 'sortAttrs';
export const description = 'Sort element attributes for better compression';
/**
* Sort element attributes for better compression
*
* @author Nikolay Frantsev
*
* @type {import('../lib/types').Plugin<{
* order?: Array<string>
* xmlnsOrder?: 'front' | 'alphabetical'
* }>}
* @type {import('../lib/types.js').Plugin<SortAttrsParams>}
*/
exports.fn = (_root, params) => {
export const fn = (_root, params) => {
const {
order = [
'id',
@@ -40,7 +39,8 @@ exports.fn = (_root, params) => {
} = params;
/**
* @type {(name: string) => number}
* @param {string} name
* @returns {number}
*/
const getNsPriority = (name) => {
if (xmlnsOrder === 'front') {
@@ -62,7 +62,9 @@ exports.fn = (_root, params) => {
};
/**
* @type {(a: [string, string], b: [string, string]) => number}
* @param {[string, string]} param0
* @param {[string, string]} param1
* @returns {number}
*/
const compareAttrs = ([aName], [bName]) => {
// sort namespaces
@@ -99,9 +101,7 @@ exports.fn = (_root, params) => {
enter: (node) => {
const attrs = Object.entries(node.attributes);
attrs.sort(compareAttrs);
/**
* @type {Record<string, string>}
*/
/** @type {Record<string, string>} */
const sortedAttributes = {};
for (const [name, value] of attrs) {
sortedAttributes[name] = value;

View File

@@ -1,26 +1,21 @@
'use strict';
exports.type = 'visitor';
exports.name = 'sortDefsChildren';
exports.active = true;
exports.description = 'Sorts children of <defs> to improve compression';
export const name = 'sortDefsChildren';
export const description = 'Sorts children of <defs> to improve compression';
/**
* Sorts children of defs in order to improve compression.
* Sorted first by frequency then by element name length then by element name (to ensure grouping).
* Sorts children of defs in order to improve compression. Sorted first by
* frequency then by element name length then by element name (to ensure
* grouping).
*
* @author David Leston
*
* @type {import('../lib/types').Plugin<void>}
* @type {import('../lib/types.js').Plugin}
*/
exports.fn = () => {
export const fn = () => {
return {
element: {
enter: (node) => {
if (node.name === 'defs') {
/**
* @type {Map<string, number>}
*/
/** @type {Map<string, number>} */
const frequencies = new Map();
for (const child of node.children) {
if (child.type === 'element') {