Comparing version 2.4.0 to 2.5.0
'use strict'; | ||
/** | ||
* @typedef {import('./types').PathDataItem} PathDataItem | ||
* @typedef {import('./types').PathDataCommand} PathDataCommand | ||
*/ | ||
// Based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF | ||
@@ -29,3 +34,3 @@ | ||
/** | ||
* @param {string} c | ||
* @type {(c: string) => c is PathDataCommand} | ||
*/ | ||
@@ -37,3 +42,3 @@ const isCommand = (c) => { | ||
/** | ||
* @param {string} c | ||
* @type {(c: string) => boolean} | ||
*/ | ||
@@ -51,3 +56,3 @@ const isWsp = (c) => { | ||
/** | ||
* @param {string} c | ||
* @type {(c: string) => boolean} | ||
*/ | ||
@@ -67,5 +72,3 @@ const isDigit = (c) => { | ||
/** | ||
* @param {string} string | ||
* @param {number} cursor | ||
* @return {[number, number | null]} | ||
* @type {(string: string, cursor: number) => [number, number | null]} | ||
*/ | ||
@@ -137,6 +140,12 @@ const readNumber = (string, cursor) => { | ||
/** | ||
* @param {string} string | ||
* @type {(string: string) => Array<PathDataItem>} | ||
*/ | ||
const parsePathData = (string) => { | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const pathData = []; | ||
/** | ||
* @type {null | PathDataCommand} | ||
*/ | ||
let command = null; | ||
@@ -240,11 +249,5 @@ let args = /** @type {number[]} */ ([]); | ||
/** | ||
* @typedef {{ | ||
* number: number; | ||
* precision?: number; | ||
* }} StringifyNumberOptions | ||
* @type {(number: number, precision?: number) => string} | ||
*/ | ||
/** | ||
* @param {StringifyNumberOptions} param | ||
*/ | ||
const stringifyNumber = ({ number, precision }) => { | ||
const stringifyNumber = (number, precision) => { | ||
if (precision != null) { | ||
@@ -259,22 +262,13 @@ const ratio = 10 ** precision; | ||
/** | ||
* @typedef {{ | ||
* command: string; | ||
* args: number[]; | ||
* precision?: number; | ||
* disableSpaceAfterFlags?: boolean; | ||
* }} StringifyArgsOptions | ||
*/ | ||
/** | ||
* | ||
* Elliptical arc large-arc and sweep flags are rendered with spaces | ||
* because many non-browser environments are not able to parse such paths | ||
* | ||
* @param {StringifyArgsOptions} param | ||
* @type {( | ||
* command: string, | ||
* args: number[], | ||
* precision?: number, | ||
* disableSpaceAfterFlags?: boolean | ||
* ) => string} | ||
*/ | ||
const stringifyArgs = ({ | ||
command, | ||
args, | ||
precision, | ||
disableSpaceAfterFlags, | ||
}) => { | ||
const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => { | ||
let result = ''; | ||
@@ -284,3 +278,3 @@ let prev = ''; | ||
const number = args[i]; | ||
const numberString = stringifyNumber({ number, precision }); | ||
const numberString = stringifyNumber(number, precision); | ||
if ( | ||
@@ -309,11 +303,4 @@ disableSpaceAfterFlags && | ||
/** | ||
* | ||
* @typedef {{ | ||
* command: string; | ||
* args: number[]; | ||
* }} Command | ||
*/ | ||
/** | ||
* @typedef {{ | ||
* pathData: Command[]; | ||
* pathData: Array<PathDataItem>; | ||
* precision?: number; | ||
@@ -323,4 +310,5 @@ * disableSpaceAfterFlags?: boolean; | ||
*/ | ||
/** | ||
* @param {StringifyPathDataOptions} param | ||
* @type {(options: StringifyPathDataOptions) => string} | ||
*/ | ||
@@ -335,2 +323,5 @@ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => { | ||
} else { | ||
/** | ||
* @type {PathDataItem} | ||
*/ | ||
const last = combined[combined.length - 1]; | ||
@@ -363,4 +354,3 @@ // match leading moveto with following lineto | ||
result += | ||
command + | ||
stringifyArgs({ command, args, precision, disableSpaceAfterFlags }); | ||
command + stringifyArgs(command, args, precision, disableSpaceAfterFlags); | ||
} | ||
@@ -367,0 +357,0 @@ return result; |
119
lib/style.js
'use strict'; | ||
/** | ||
* @typedef {import('css-tree').Rule} CsstreeRule | ||
* @typedef {import('./types').Specificity} Specificity | ||
* @typedef {import('./types').StylesheetRule} StylesheetRule | ||
* @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration | ||
* @typedef {import('./types').ComputedStyles} ComputedStyles | ||
* @typedef {import('./types').XastRoot} XastRoot | ||
* @typedef {import('./types').XastElement} XastElement | ||
* @typedef {import('./types').XastParent} XastParent | ||
* @typedef {import('./types').XastChild} XastChild | ||
*/ | ||
const stable = require('stable'); | ||
const csstree = require('css-tree'); | ||
// @ts-ignore not defined in @types/csso | ||
const specificity = require('csso/lib/restructure/prepare/specificity'); | ||
const { visit, matches } = require('./xast.js'); | ||
const { compareSpecificity } = require('./css-tools.js'); | ||
const { | ||
@@ -14,5 +26,14 @@ attrsGroups, | ||
// @ts-ignore not defined in @types/csstree | ||
const csstreeWalkSkip = csstree.walk.skip; | ||
/** | ||
* @type {(ruleNode: CsstreeRule, dynamic: boolean) => StylesheetRule} | ||
*/ | ||
const parseRule = (ruleNode, dynamic) => { | ||
let selectors; | ||
let selectorsSpecificity; | ||
/** | ||
* @type {Array<StylesheetDeclaration>} | ||
*/ | ||
const declarations = []; | ||
@@ -31,3 +52,3 @@ csstree.walk(ruleNode, (cssNode) => { | ||
selectors = csstree.generate(newSelectorsNode); | ||
return csstree.walk.skip; | ||
return csstreeWalkSkip; | ||
} | ||
@@ -38,7 +59,10 @@ if (cssNode.type === 'Declaration') { | ||
value: csstree.generate(cssNode.value), | ||
important: cssNode.important, | ||
important: cssNode.important === true, | ||
}); | ||
return csstree.walk.skip; | ||
return csstreeWalkSkip; | ||
} | ||
}); | ||
if (selectors == null || selectorsSpecificity == null) { | ||
throw Error('assert'); | ||
} | ||
return { | ||
@@ -52,13 +76,22 @@ dynamic, | ||
/** | ||
* @type {(css: string, dynamic: boolean) => Array<StylesheetRule>} | ||
*/ | ||
const parseStylesheet = (css, dynamic) => { | ||
/** | ||
* @type {Array<StylesheetRule>} | ||
*/ | ||
const rules = []; | ||
const ast = csstree.parse(css); | ||
const ast = csstree.parse(css, { | ||
parseValue: false, | ||
parseAtrulePrelude: false, | ||
}); | ||
csstree.walk(ast, (cssNode) => { | ||
if (cssNode.type === 'Rule') { | ||
rules.push(parseRule(cssNode, dynamic || false)); | ||
return csstree.walk.skip; | ||
return csstreeWalkSkip; | ||
} | ||
if (cssNode.type === 'Atrule') { | ||
if (cssNode.name === 'keyframes') { | ||
return csstree.walk.skip; | ||
return csstreeWalkSkip; | ||
} | ||
@@ -68,6 +101,6 @@ csstree.walk(cssNode, (ruleNode) => { | ||
rules.push(parseRule(ruleNode, dynamic || true)); | ||
return csstree.walk.skip; | ||
return csstreeWalkSkip; | ||
} | ||
}); | ||
return csstree.walk.skip; | ||
return csstreeWalkSkip; | ||
} | ||
@@ -78,3 +111,33 @@ }); | ||
/** | ||
* @type {(css: string) => Array<StylesheetDeclaration>} | ||
*/ | ||
const parseStyleDeclarations = (css) => { | ||
/** | ||
* @type {Array<StylesheetDeclaration>} | ||
*/ | ||
const declarations = []; | ||
const ast = csstree.parse(css, { | ||
context: 'declarationList', | ||
parseValue: false, | ||
}); | ||
csstree.walk(ast, (cssNode) => { | ||
if (cssNode.type === 'Declaration') { | ||
declarations.push({ | ||
name: cssNode.property, | ||
value: csstree.generate(cssNode.value), | ||
important: cssNode.important === true, | ||
}); | ||
} | ||
}); | ||
return declarations; | ||
}; | ||
/** | ||
* @type {(stylesheet: Array<StylesheetRule>, node: XastElement) => ComputedStyles} | ||
*/ | ||
const computeOwnStyle = (stylesheet, node) => { | ||
/** | ||
* @type {ComputedStyles} | ||
*/ | ||
const computedStyle = {}; | ||
@@ -116,5 +179,8 @@ const importantStyles = new Map(); | ||
// collect inline styles | ||
for (const [name, { value, priority }] of node.style.properties) { | ||
const styleDeclarations = | ||
node.attributes.style == null | ||
? [] | ||
: parseStyleDeclarations(node.attributes.style); | ||
for (const { name, value, important } of styleDeclarations) { | ||
const computed = computedStyle[name]; | ||
const important = priority === 'important'; | ||
if (computed && computed.type === 'dynamic') { | ||
@@ -136,3 +202,27 @@ continue; | ||
/** | ||
* Compares two selector specificities. | ||
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211 | ||
* | ||
* @type {(a: Specificity, b: Specificity) => number} | ||
*/ | ||
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; | ||
}; | ||
/** | ||
* @type {(root: XastRoot) => Array<StylesheetRule>} | ||
*/ | ||
const collectStylesheet = (root) => { | ||
/** | ||
* @type {Array<StylesheetRule>} | ||
*/ | ||
const stylesheet = []; | ||
@@ -170,2 +260,5 @@ // find and parse all styles | ||
/** | ||
* @type {(stylesheet: Array<StylesheetRule>, node: XastElement) => ComputedStyles} | ||
*/ | ||
const computeStyle = (stylesheet, node) => { | ||
@@ -175,3 +268,5 @@ // collect inherited styles | ||
let parent = node; | ||
// @ts-ignore parentNode is forbidden in public usage | ||
while (parent.parentNode && parent.parentNode.type !== 'root') { | ||
// @ts-ignore parentNode is forbidden in public usage | ||
const inheritedStyles = computeOwnStyle(stylesheet, parent.parentNode); | ||
@@ -188,7 +283,7 @@ for (const [name, computed] of Object.entries(inheritedStyles)) { | ||
} | ||
// @ts-ignore parentNode is forbidden in public usage | ||
parent = parent.parentNode; | ||
} | ||
return computedStyles; | ||
}; | ||
exports.computeStyle = computeStyle; |
'use strict'; | ||
/** | ||
* @param {any} node | ||
* @return {node is any} | ||
*/ | ||
const isTag = (node) => { | ||
@@ -8,0 +4,0 @@ return node.type === 'element'; |
'use strict'; | ||
/** | ||
* @typedef {import('../types').PathDataCommand} PathDataCommand | ||
*/ | ||
/** | ||
* Encode plain SVG data string into Data URI string. | ||
* | ||
* @param {string} str input string | ||
* @param {string} type Data URI type | ||
* @return {string} output string | ||
* @type {(str: string, type?: 'base64' | 'enc' | 'unenc') => string} | ||
*/ | ||
exports.encodeSVGDatauri = function (str, type) { | ||
exports.encodeSVGDatauri = (str, type) => { | ||
var prefix = 'data:image/svg+xml'; | ||
@@ -29,6 +31,5 @@ if (!type || type === 'base64') { | ||
* | ||
* @param {string} str input string | ||
* @return {string} output string | ||
* @type {(str: string) => string} | ||
*/ | ||
exports.decodeSVGDatauri = function (str) { | ||
exports.decodeSVGDatauri = (str) => { | ||
var regexp = /data:image\/svg\+xml(;charset=[^;,]*)?(;base64)?,(.*)/; | ||
@@ -56,2 +57,10 @@ var match = regexp.exec(str); | ||
/** | ||
* @typedef {{ | ||
* noSpaceAfterFlags?: boolean, | ||
* leadingZero?: boolean, | ||
* negativeExtraSpace?: boolean | ||
* }} CleanupOutDataParams | ||
*/ | ||
/** | ||
* Convert a row of numbers to an optimized string view. | ||
@@ -62,13 +71,13 @@ * | ||
* | ||
* @param {number[]} data | ||
* @param {Object} params | ||
* @param {string} [command] path data instruction | ||
* @return {string} | ||
* @type {(data: Array<number>, params: CleanupOutDataParams, command?: PathDataCommand) => string} | ||
*/ | ||
exports.cleanupOutData = function (data, params, command) { | ||
var str = '', | ||
delimiter, | ||
prev; | ||
exports.cleanupOutData = (data, params, command) => { | ||
let str = ''; | ||
let delimiter; | ||
/** | ||
* @type {number} | ||
*/ | ||
let prev; | ||
data.forEach(function (item, i) { | ||
data.forEach((item, i) => { | ||
// space delimiter by default | ||
@@ -119,7 +128,5 @@ delimiter = ' '; | ||
* | ||
* @param {number} num input number | ||
* | ||
* @return {string} output number as string | ||
* @type {(num: number) => string} | ||
*/ | ||
var removeLeadingZero = function (num) { | ||
const removeLeadingZero = (num) => { | ||
var strNum = num.toString(); | ||
@@ -136,2 +143,5 @@ | ||
/** | ||
* @type {(name: string) => { prefix: string, local: string }} | ||
*/ | ||
const parseName = (name) => { | ||
@@ -138,0 +148,0 @@ if (name == null) { |
'use strict'; | ||
/** | ||
* @typedef {import('./types').XastNode} XastNode | ||
* @typedef {import('./types').XastChild} XastChild | ||
* @typedef {import('./types').XastParent} XastParent | ||
* @typedef {import('./types').Visitor} Visitor | ||
*/ | ||
const { selectAll, selectOne, is } = require('css-select'); | ||
@@ -11,2 +18,5 @@ const xastAdaptor = require('./svgo/css-select-adapter.js'); | ||
/** | ||
* @type {(node: XastNode, selector: string) => Array<XastChild>} | ||
*/ | ||
const querySelectorAll = (node, selector) => { | ||
@@ -17,2 +27,5 @@ return selectAll(selector, node, cssSelectOptions); | ||
/** | ||
* @type {(node: XastNode, selector: string) => null | XastChild} | ||
*/ | ||
const querySelector = (node, selector) => { | ||
@@ -23,2 +36,5 @@ return selectOne(selector, node, cssSelectOptions); | ||
/** | ||
* @type {(node: XastChild, selector: string) => boolean} | ||
*/ | ||
const matches = (node, selector) => { | ||
@@ -29,2 +45,5 @@ return is(node, selector, cssSelectOptions); | ||
/** | ||
* @type {(node: XastChild, name: string) => null | XastChild} | ||
*/ | ||
const closestByName = (node, name) => { | ||
@@ -36,2 +55,3 @@ let currentNode = node; | ||
} | ||
// @ts-ignore parentNode is hidden from public usage | ||
currentNode = currentNode.parentNode; | ||
@@ -46,2 +66,5 @@ } | ||
/** | ||
* @type {(node: any, fn: any) => any} | ||
*/ | ||
const traverse = (node, fn) => { | ||
@@ -61,6 +84,16 @@ if (fn(node) === traverseBreak) { | ||
const visit = (node, visitor, parentNode = null) => { | ||
const visitSkip = Symbol(); | ||
exports.visitSkip = visitSkip; | ||
/** | ||
* @type {(node: XastNode, visitor: Visitor, parentNode?: any) => void} | ||
*/ | ||
const visit = (node, visitor, parentNode) => { | ||
const callbacks = visitor[node.type]; | ||
if (callbacks && callbacks.enter) { | ||
callbacks.enter(node, parentNode); | ||
// @ts-ignore hard to infer | ||
const symbol = callbacks.enter(node, parentNode); | ||
if (symbol === visitSkip) { | ||
return; | ||
} | ||
} | ||
@@ -83,2 +116,3 @@ // visit root children | ||
if (callbacks && callbacks.exit) { | ||
// @ts-ignore hard to infer | ||
callbacks.exit(node, parentNode); | ||
@@ -89,2 +123,5 @@ } | ||
/** | ||
* @type {(node: XastChild, parentNode: XastParent) => void} | ||
*/ | ||
const detachNodeFromParent = (node, parentNode) => { | ||
@@ -91,0 +128,0 @@ // avoid splice to not break for loops |
{ | ||
"name": "svgo", | ||
"version": "2.4.0", | ||
"version": "2.5.0", | ||
"description": "Nodejs-based tool for optimizing SVG vector graphics files", | ||
@@ -96,6 +96,6 @@ "keywords": [ | ||
"@trysound/sax": "0.1.1", | ||
"colorette": "^1.2.2", | ||
"commander": "^7.1.0", | ||
"colorette": "^1.3.0", | ||
"commander": "^7.2.0", | ||
"css-select": "^4.1.3", | ||
"css-tree": "^1.1.2", | ||
"css-tree": "^1.1.3", | ||
"csso": "^4.2.0", | ||
@@ -107,6 +107,8 @@ "stable": "^0.1.8" | ||
"@rollup/plugin-json": "^4.1.0", | ||
"@rollup/plugin-node-resolve": "^11.2.0", | ||
"@types/jest": "^27.0.0", | ||
"@rollup/plugin-node-resolve": "^11.2.1", | ||
"@types/css-tree": "^1.0.6", | ||
"@types/csso": "^4.2.0", | ||
"@types/jest": "^27.0.1", | ||
"del": "^6.0.0", | ||
"eslint": "^7.22.0", | ||
"eslint": "^7.32.0", | ||
"jest": "^27.0.6", | ||
@@ -116,9 +118,9 @@ "mock-stdin": "^1.0.0", | ||
"pixelmatch": "^5.2.1", | ||
"playwright": "^1.9.2", | ||
"playwright": "^1.14.0", | ||
"pngjs": "^6.0.0", | ||
"prettier": "^2.2.1", | ||
"rollup": "^2.42.1", | ||
"prettier": "^2.3.2", | ||
"rollup": "^2.56.2", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"tar-stream": "^2.2.0", | ||
"typescript": "^4.2.3" | ||
"typescript": "^4.3.5" | ||
}, | ||
@@ -125,0 +127,0 @@ "engines": { |
@@ -90,6 +90,5 @@ 'use strict'; | ||
} else { | ||
elem.attributes[ | ||
'stroke-width' | ||
] = strokeWidth.replace(regNumericValues, (num) => | ||
removeLeadingZero(num * scale) | ||
elem.attributes['stroke-width'] = strokeWidth.replace( | ||
regNumericValues, | ||
(num) => removeLeadingZero(num * scale) | ||
); | ||
@@ -142,7 +141,8 @@ } | ||
const applyMatrixToPathData = (pathData, matrix) => { | ||
let start = [0, 0]; | ||
let cursor = [0, 0]; | ||
const start = [0, 0]; | ||
const cursor = [0, 0]; | ||
for (const pathItem of pathData) { | ||
let { instruction: command, data: args } = pathItem; | ||
let { command, args } = pathItem; | ||
// moveto (x y) | ||
@@ -328,2 +328,3 @@ if (command === 'M') { | ||
// closepath | ||
if (command === 'z' || command === 'Z') { | ||
@@ -334,5 +335,5 @@ cursor[0] = start[0]; | ||
pathItem.instruction = command; | ||
pathItem.data = args; | ||
pathItem.command = command; | ||
pathItem.args = args; | ||
} | ||
}; |
'use strict'; | ||
// https://www.w3.org/TR/SVG11/intro.html#Definitions | ||
/** | ||
* @type {Record<string, Array<string>>} | ||
*/ | ||
exports.elemsGroups = { | ||
@@ -91,2 +95,5 @@ animation: [ | ||
// https://www.w3.org/TR/SVG11/intro.html#Definitions | ||
/** | ||
* @type {Record<string, Array<string>>} | ||
*/ | ||
exports.attrsGroups = { | ||
@@ -192,2 +199,3 @@ animationAddition: ['additive', 'accumulate'], | ||
'transform', | ||
'transform-origin', | ||
'unicode-bidi', | ||
@@ -228,2 +236,5 @@ 'vector-effect', | ||
/** | ||
* @type {Record<string, Record<string, string>>} | ||
*/ | ||
exports.attrsGroupsDefaults = { | ||
@@ -294,2 +305,11 @@ core: { 'xml:space': 'default' }, | ||
// https://www.w3.org/TR/SVG11/eltindex.html | ||
/** | ||
* @type {Record<string, { | ||
* attrsGroups: Array<string>, | ||
* attrs?: Array<string>, | ||
* defaults?: Record<string, string>, | ||
* contentGroups?: Array<string>, | ||
* content?: Array<string>, | ||
* }>} | ||
*/ | ||
exports.elems = { | ||
@@ -965,4 +985,4 @@ a: { | ||
defaults: { | ||
x: 0, | ||
y: 0, | ||
x: '0', | ||
y: '0', | ||
}, | ||
@@ -1656,4 +1676,4 @@ }, | ||
defaults: { | ||
refX: 0, | ||
refY: 0, | ||
refX: '0', | ||
refY: '0', | ||
}, | ||
@@ -1944,6 +1964,9 @@ contentGroups: [ | ||
'unicode-bidi', | ||
'visibility', | ||
]; | ||
// https://www.w3.org/TR/SVG11/single-page.html#types-ColorKeywords | ||
/** | ||
* https://www.w3.org/TR/SVG11/single-page.html#types-ColorKeywords | ||
* | ||
* @type {Record<string, string>} | ||
*/ | ||
exports.colorsNames = { | ||
@@ -2100,2 +2123,5 @@ aliceblue: '#f0f8ff', | ||
/** | ||
* @type {Record<string, string>} | ||
*/ | ||
exports.colorsShortNames = { | ||
@@ -2102,0 +2128,0 @@ '#f0ffff': 'azure', |
'use strict'; | ||
/** | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
* @typedef {import('../lib/types').PathDataItem} PathDataItem | ||
*/ | ||
const { parsePathData, stringifyPathData } = require('../lib/path.js'); | ||
/** | ||
* @type {[number, number]} | ||
*/ | ||
var prevCtrlPoint; | ||
@@ -10,24 +18,24 @@ | ||
* | ||
* @param {String} pathString input string | ||
* @param {Object} params plugin params | ||
* @return {Array} output array | ||
* @type {(path: XastElement) => Array<PathDataItem>} | ||
*/ | ||
exports.path2js = function (path) { | ||
const path2js = (path) => { | ||
// @ts-ignore legacy | ||
if (path.pathJS) return path.pathJS; | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const pathData = []; // JS representation of the path data | ||
const newPathData = parsePathData(path.attributes.d); | ||
for (const { command, args } of newPathData) { | ||
if (command === 'Z' || command === 'z') { | ||
pathData.push({ instruction: 'z' }); | ||
} else { | ||
pathData.push({ instruction: command, data: args }); | ||
} | ||
pathData.push({ command, args }); | ||
} | ||
// First moveto is actually absolute. Subsequent coordinates were separated above. | ||
if (pathData.length && pathData[0].instruction == 'm') { | ||
pathData[0].instruction = 'M'; | ||
if (pathData.length && pathData[0].command == 'm') { | ||
pathData[0].command = 'M'; | ||
} | ||
// @ts-ignore legacy | ||
path.pathJS = pathData; | ||
return pathData; | ||
}; | ||
exports.path2js = path2js; | ||
@@ -37,283 +45,136 @@ /** | ||
* | ||
* @param {Array} data input data | ||
* @return {Array} output data | ||
* @type {(data: Array<PathDataItem>) => Array<PathDataItem>} | ||
* | ||
*/ | ||
var relative2absolute = (exports.relative2absolute = function (data) { | ||
var currentPoint = [0, 0], | ||
subpathPoint = [0, 0], | ||
i; | ||
const convertRelativeToAbsolute = (data) => { | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const newData = []; | ||
let start = [0, 0]; | ||
let cursor = [0, 0]; | ||
return data.map(function (item) { | ||
var instruction = item.instruction, | ||
itemData = item.data && item.data.slice(); | ||
for (let { command, args } of data) { | ||
args = args.slice(); | ||
if (instruction == 'M') { | ||
set(currentPoint, itemData); | ||
set(subpathPoint, itemData); | ||
} else if ('mlcsqt'.indexOf(instruction) > -1) { | ||
for (i = 0; i < itemData.length; i++) { | ||
itemData[i] += currentPoint[i % 2]; | ||
} | ||
set(currentPoint, itemData); | ||
// moveto (x y) | ||
if (command === 'm') { | ||
args[0] += cursor[0]; | ||
args[1] += cursor[1]; | ||
command = 'M'; | ||
} | ||
if (command === 'M') { | ||
cursor[0] = args[0]; | ||
cursor[1] = args[1]; | ||
start[0] = cursor[0]; | ||
start[1] = cursor[1]; | ||
} | ||
if (instruction == 'm') { | ||
set(subpathPoint, itemData); | ||
} | ||
} else if (instruction == 'a') { | ||
itemData[5] += currentPoint[0]; | ||
itemData[6] += currentPoint[1]; | ||
set(currentPoint, itemData); | ||
} else if (instruction == 'h') { | ||
itemData[0] += currentPoint[0]; | ||
currentPoint[0] = itemData[0]; | ||
} else if (instruction == 'v') { | ||
itemData[0] += currentPoint[1]; | ||
currentPoint[1] = itemData[0]; | ||
} else if ('MZLCSQTA'.indexOf(instruction) > -1) { | ||
set(currentPoint, itemData); | ||
} else if (instruction == 'H') { | ||
currentPoint[0] = itemData[0]; | ||
} else if (instruction == 'V') { | ||
currentPoint[1] = itemData[0]; | ||
} else if (instruction == 'z') { | ||
set(currentPoint, subpathPoint); | ||
// horizontal lineto (x) | ||
if (command === 'h') { | ||
args[0] += cursor[0]; | ||
command = 'H'; | ||
} | ||
if (command === 'H') { | ||
cursor[0] = args[0]; | ||
} | ||
return instruction == 'z' | ||
? { instruction: 'z' } | ||
: { | ||
instruction: instruction.toUpperCase(), | ||
data: itemData, | ||
}; | ||
}); | ||
}); | ||
// vertical lineto (y) | ||
if (command === 'v') { | ||
args[0] += cursor[1]; | ||
command = 'V'; | ||
} | ||
if (command === 'V') { | ||
cursor[1] = args[0]; | ||
} | ||
/** | ||
* Compute Cubic Bézie bounding box. | ||
* | ||
* @see https://pomax.github.io/bezierinfo/ | ||
* | ||
* @param {Float} xa | ||
* @param {Float} ya | ||
* @param {Float} xb | ||
* @param {Float} yb | ||
* @param {Float} xc | ||
* @param {Float} yc | ||
* @param {Float} xd | ||
* @param {Float} yd | ||
* | ||
* @return {Object} | ||
*/ | ||
exports.computeCubicBoundingBox = function (xa, ya, xb, yb, xc, yc, xd, yd) { | ||
var minx = Number.POSITIVE_INFINITY, | ||
miny = Number.POSITIVE_INFINITY, | ||
maxx = Number.NEGATIVE_INFINITY, | ||
maxy = Number.NEGATIVE_INFINITY, | ||
ts, | ||
t, | ||
x, | ||
y, | ||
i; | ||
// lineto (x y) | ||
if (command === 'l') { | ||
args[0] += cursor[0]; | ||
args[1] += cursor[1]; | ||
command = 'L'; | ||
} | ||
if (command === 'L') { | ||
cursor[0] = args[0]; | ||
cursor[1] = args[1]; | ||
} | ||
// X | ||
if (xa < minx) { | ||
minx = xa; | ||
} | ||
if (xa > maxx) { | ||
maxx = xa; | ||
} | ||
if (xd < minx) { | ||
minx = xd; | ||
} | ||
if (xd > maxx) { | ||
maxx = xd; | ||
} | ||
// curveto (x1 y1 x2 y2 x y) | ||
if (command === 'c') { | ||
args[0] += cursor[0]; | ||
args[1] += cursor[1]; | ||
args[2] += cursor[0]; | ||
args[3] += cursor[1]; | ||
args[4] += cursor[0]; | ||
args[5] += cursor[1]; | ||
command = 'C'; | ||
} | ||
if (command === 'C') { | ||
cursor[0] = args[4]; | ||
cursor[1] = args[5]; | ||
} | ||
ts = computeCubicFirstDerivativeRoots(xa, xb, xc, xd); | ||
for (i = 0; i < ts.length; i++) { | ||
t = ts[i]; | ||
if (t >= 0 && t <= 1) { | ||
x = computeCubicBaseValue(t, xa, xb, xc, xd); | ||
// y = computeCubicBaseValue(t, ya, yb, yc, yd); | ||
if (x < minx) { | ||
minx = x; | ||
} | ||
if (x > maxx) { | ||
maxx = x; | ||
} | ||
// smooth curveto (x2 y2 x y) | ||
if (command === 's') { | ||
args[0] += cursor[0]; | ||
args[1] += cursor[1]; | ||
args[2] += cursor[0]; | ||
args[3] += cursor[1]; | ||
command = 'S'; | ||
} | ||
} | ||
if (command === 'S') { | ||
cursor[0] = args[2]; | ||
cursor[1] = args[3]; | ||
} | ||
// Y | ||
if (ya < miny) { | ||
miny = ya; | ||
} | ||
if (ya > maxy) { | ||
maxy = ya; | ||
} | ||
if (yd < miny) { | ||
miny = yd; | ||
} | ||
if (yd > maxy) { | ||
maxy = yd; | ||
} | ||
ts = computeCubicFirstDerivativeRoots(ya, yb, yc, yd); | ||
for (i = 0; i < ts.length; i++) { | ||
t = ts[i]; | ||
if (t >= 0 && t <= 1) { | ||
// x = computeCubicBaseValue(t, xa, xb, xc, xd); | ||
y = computeCubicBaseValue(t, ya, yb, yc, yd); | ||
if (y < miny) { | ||
miny = y; | ||
} | ||
if (y > maxy) { | ||
maxy = y; | ||
} | ||
// quadratic Bézier curveto (x1 y1 x y) | ||
if (command === 'q') { | ||
args[0] += cursor[0]; | ||
args[1] += cursor[1]; | ||
args[2] += cursor[0]; | ||
args[3] += cursor[1]; | ||
command = 'Q'; | ||
} | ||
} | ||
if (command === 'Q') { | ||
cursor[0] = args[2]; | ||
cursor[1] = args[3]; | ||
} | ||
return { | ||
minx: minx, | ||
miny: miny, | ||
maxx: maxx, | ||
maxy: maxy, | ||
}; | ||
}; | ||
// compute the value for the cubic bezier function at time=t | ||
function computeCubicBaseValue(t, a, b, c, d) { | ||
var mt = 1 - t; | ||
return ( | ||
mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d | ||
); | ||
} | ||
// compute the value for the first derivative of the cubic bezier function at time=t | ||
function computeCubicFirstDerivativeRoots(a, b, c, d) { | ||
var result = [-1, -1], | ||
tl = -a + 2 * b - c, | ||
tr = -Math.sqrt(-a * (c - d) + b * b - b * (c + d) + c * c), | ||
dn = -a + 3 * b - 3 * c + d; | ||
if (dn !== 0) { | ||
result[0] = (tl + tr) / dn; | ||
result[1] = (tl - tr) / dn; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Compute Quadratic Bézier bounding box. | ||
* | ||
* @see https://pomax.github.io/bezierinfo/ | ||
* | ||
* @param {Float} xa | ||
* @param {Float} ya | ||
* @param {Float} xb | ||
* @param {Float} yb | ||
* @param {Float} xc | ||
* @param {Float} yc | ||
* | ||
* @return {Object} | ||
*/ | ||
exports.computeQuadraticBoundingBox = function (xa, ya, xb, yb, xc, yc) { | ||
var minx = Number.POSITIVE_INFINITY, | ||
miny = Number.POSITIVE_INFINITY, | ||
maxx = Number.NEGATIVE_INFINITY, | ||
maxy = Number.NEGATIVE_INFINITY, | ||
t, | ||
x, | ||
y; | ||
// X | ||
if (xa < minx) { | ||
minx = xa; | ||
} | ||
if (xa > maxx) { | ||
maxx = xa; | ||
} | ||
if (xc < minx) { | ||
minx = xc; | ||
} | ||
if (xc > maxx) { | ||
maxx = xc; | ||
} | ||
t = computeQuadraticFirstDerivativeRoot(xa, xb, xc); | ||
if (t >= 0 && t <= 1) { | ||
x = computeQuadraticBaseValue(t, xa, xb, xc); | ||
// y = computeQuadraticBaseValue(t, ya, yb, yc); | ||
if (x < minx) { | ||
minx = x; | ||
// smooth quadratic Bézier curveto (x y) | ||
if (command === 't') { | ||
args[0] += cursor[0]; | ||
args[1] += cursor[1]; | ||
command = 'T'; | ||
} | ||
if (x > maxx) { | ||
maxx = x; | ||
if (command === 'T') { | ||
cursor[0] = args[0]; | ||
cursor[1] = args[1]; | ||
} | ||
} | ||
// Y | ||
if (ya < miny) { | ||
miny = ya; | ||
} | ||
if (ya > maxy) { | ||
maxy = ya; | ||
} | ||
if (yc < miny) { | ||
miny = yc; | ||
} | ||
if (yc > maxy) { | ||
maxy = yc; | ||
} | ||
t = computeQuadraticFirstDerivativeRoot(ya, yb, yc); | ||
if (t >= 0 && t <= 1) { | ||
// x = computeQuadraticBaseValue(t, xa, xb, xc); | ||
y = computeQuadraticBaseValue(t, ya, yb, yc); | ||
if (y < miny) { | ||
miny = y; | ||
// elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y) | ||
if (command === 'a') { | ||
args[5] += cursor[0]; | ||
args[6] += cursor[1]; | ||
command = 'A'; | ||
} | ||
if (y > maxy) { | ||
maxy = y; | ||
if (command === 'A') { | ||
cursor[0] = args[5]; | ||
cursor[1] = args[6]; | ||
} | ||
} | ||
return { | ||
minx: minx, | ||
miny: miny, | ||
maxx: maxx, | ||
maxy: maxy, | ||
}; | ||
}; | ||
// closepath | ||
if (command === 'z' || command === 'Z') { | ||
cursor[0] = start[0]; | ||
cursor[1] = start[1]; | ||
command = 'z'; | ||
} | ||
// compute the value for the quadratic bezier function at time=t | ||
function computeQuadraticBaseValue(t, a, b, c) { | ||
var mt = 1 - t; | ||
return mt * mt * a + 2 * mt * t * b + t * t * c; | ||
} | ||
// compute the value for the first derivative of the quadratic bezier function at time=t | ||
function computeQuadraticFirstDerivativeRoot(a, b, c) { | ||
var t = -1, | ||
denominator = a - 2 * b + c; | ||
if (denominator !== 0) { | ||
t = (a - b) / denominator; | ||
newData.push({ command, args }); | ||
} | ||
return newData; | ||
}; | ||
return t; | ||
} | ||
/** | ||
* @typedef {{ floatPrecision?: number, noSpaceAfterFlags?: boolean }} Js2PathParams | ||
*/ | ||
@@ -323,7 +184,6 @@ /** | ||
* | ||
* @param {Array} path input path data | ||
* @param {Object} params plugin params | ||
* @return {String} output path string | ||
* @type {(path: XastElement, data: Array<PathDataItem>, params: Js2PathParams) => void} | ||
*/ | ||
exports.js2path = function (path, data, params) { | ||
// @ts-ignore legacy | ||
path.pathJS = data; | ||
@@ -336,3 +196,3 @@ | ||
pathData.length !== 0 && | ||
(item.instruction === 'M' || item.instruction === 'm') | ||
(item.command === 'M' || item.command === 'm') | ||
) { | ||
@@ -345,4 +205,4 @@ const last = pathData[pathData.length - 1]; | ||
pathData.push({ | ||
command: item.instruction, | ||
args: item.data || [], | ||
command: item.command, | ||
args: item.args, | ||
}); | ||
@@ -358,2 +218,5 @@ } | ||
/** | ||
* @type {(dest: Array<number>, source: Array<number>) => Array<number>} | ||
*/ | ||
function set(dest, source) { | ||
@@ -370,10 +233,8 @@ dest[0] = source[source.length - 2]; | ||
* | ||
* @param {Array} path1 JS path representation | ||
* @param {Array} path2 JS path representation | ||
* @return {Boolean} | ||
* @type {(path1: Array<PathDataItem>, path2: Array<PathDataItem>) => boolean} | ||
*/ | ||
exports.intersects = function (path1, path2) { | ||
// Collect points of every subpath. | ||
var points1 = relative2absolute(path1).reduce(gatherPoints, []), | ||
points2 = relative2absolute(path2).reduce(gatherPoints, []); | ||
const points1 = gatherPoints(convertRelativeToAbsolute(path1)); | ||
const points2 = gatherPoints(convertRelativeToAbsolute(path2)); | ||
@@ -386,9 +247,9 @@ // Axis-aligned bounding box check. | ||
points2.maxY <= points1.minY || | ||
points1.every(function (set1) { | ||
return points2.every(function (set2) { | ||
points1.list.every((set1) => { | ||
return points2.list.every((set2) => { | ||
return ( | ||
set1[set1.maxX][0] <= set2[set2.minX][0] || | ||
set2[set2.maxX][0] <= set1[set1.minX][0] || | ||
set1[set1.maxY][1] <= set2[set2.minY][1] || | ||
set2[set2.maxY][1] <= set1[set1.minY][1] | ||
set1.list[set1.maxX][0] <= set2.list[set2.minX][0] || | ||
set2.list[set2.maxX][0] <= set1.list[set1.minX][0] || | ||
set1.list[set1.maxY][1] <= set2.list[set2.minY][1] || | ||
set2.list[set2.maxY][1] <= set1.list[set1.minY][1] | ||
); | ||
@@ -401,11 +262,11 @@ }); | ||
// Get a convex hull from points of each subpath. Has the most complexity O(n·log n). | ||
var hullNest1 = points1.map(convexHull), | ||
hullNest2 = points2.map(convexHull); | ||
const hullNest1 = points1.list.map(convexHull); | ||
const hullNest2 = points2.list.map(convexHull); | ||
// Check intersection of every subpath of the first path with every subpath of the second. | ||
return hullNest1.some(function (hull1) { | ||
if (hull1.length < 3) return false; | ||
if (hull1.list.length < 3) return false; | ||
return hullNest2.some(function (hull2) { | ||
if (hull2.length < 3) return false; | ||
if (hull2.list.length < 3) return false; | ||
@@ -435,2 +296,5 @@ var simplex = [getSupport(hull1, hull2, [1, 0])], // create the initial simplex | ||
/** | ||
* @type {(a: Point, b: Point, direction: Array<number>) => Array<number>} | ||
*/ | ||
function getSupport(a, b, direction) { | ||
@@ -443,2 +307,5 @@ return sub(supportPoint(a, direction), supportPoint(b, minus(direction))); | ||
// 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>} | ||
*/ | ||
function supportPoint(polygon, direction) { | ||
@@ -455,10 +322,13 @@ var index = | ||
value; | ||
while ((value = dot(polygon[index], direction)) > max) { | ||
while ((value = dot(polygon.list[index], direction)) > max) { | ||
max = value; | ||
index = ++index % polygon.length; | ||
index = ++index % polygon.list.length; | ||
} | ||
return polygon[(index || polygon.length) - 1]; | ||
return polygon.list[(index || polygon.list.length) - 1]; | ||
} | ||
}; | ||
/** | ||
* @type {(simplex: Array<Array<number>>, direction: Array<number>) => boolean} | ||
*/ | ||
function processSimplex(simplex, direction) { | ||
@@ -518,2 +388,5 @@ // we only need to handle to 1-simplex and 2-simplex | ||
/** | ||
* @type {(v: Array<number>) => Array<number>} | ||
*/ | ||
function minus(v) { | ||
@@ -523,2 +396,5 @@ return [-v[0], -v[1]]; | ||
/** | ||
* @type {(v1: Array<number>, v2: Array<number>) => Array<number>} | ||
*/ | ||
function sub(v1, v2) { | ||
@@ -528,2 +404,5 @@ return [v1[0] - v2[0], v1[1] - v2[1]]; | ||
/** | ||
* @type {(v1: Array<number>, v2: Array<number>) => number} | ||
*/ | ||
function dot(v1, v2) { | ||
@@ -533,2 +412,5 @@ return v1[0] * v2[0] + v1[1] * v2[1]; | ||
/** | ||
* @type {(v1: Array<number>, v2: Array<number>) => Array<number>} | ||
*/ | ||
function orth(v, from) { | ||
@@ -539,109 +421,201 @@ var o = [-v[1], v[0]]; | ||
function gatherPoints(points, item, index, path) { | ||
var subPath = points.length && points[points.length - 1], | ||
prev = index && path[index - 1], | ||
basePoint = subPath.length && subPath[subPath.length - 1], | ||
data = item.data, | ||
ctrlPoint = basePoint; | ||
/** | ||
* @typedef {{ | ||
* list: Array<Array<number>>, | ||
* minX: number, | ||
* minY: number, | ||
* maxX: number, | ||
* maxY: number | ||
* }} Point | ||
*/ | ||
switch (item.instruction) { | ||
case 'M': | ||
points.push((subPath = [])); | ||
break; | ||
case 'H': | ||
addPoint(subPath, [data[0], basePoint[1]]); | ||
break; | ||
case 'V': | ||
addPoint(subPath, [basePoint[0], data[0]]); | ||
break; | ||
case 'Q': | ||
addPoint(subPath, data.slice(0, 2)); | ||
prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; // Save control point for shorthand | ||
break; | ||
case 'T': | ||
if (prev.instruction == 'Q' || prev.instruction == 'T') { | ||
ctrlPoint = [ | ||
basePoint[0] + prevCtrlPoint[0], | ||
basePoint[1] + prevCtrlPoint[1], | ||
]; | ||
addPoint(subPath, ctrlPoint); | ||
prevCtrlPoint = [data[0] - ctrlPoint[0], data[1] - ctrlPoint[1]]; | ||
} | ||
break; | ||
case 'C': | ||
// Approximate quibic Bezier curve with middle points between control points | ||
addPoint(subPath, [ | ||
0.5 * (basePoint[0] + data[0]), | ||
0.5 * (basePoint[1] + data[1]), | ||
]); | ||
addPoint(subPath, [0.5 * (data[0] + data[2]), 0.5 * (data[1] + data[3])]); | ||
addPoint(subPath, [0.5 * (data[2] + data[4]), 0.5 * (data[3] + data[5])]); | ||
prevCtrlPoint = [data[4] - data[2], data[5] - data[3]]; // Save control point for shorthand | ||
break; | ||
case 'S': | ||
if (prev.instruction == 'C' || prev.instruction == 'S') { | ||
/** | ||
* @typedef {{ | ||
* list: Array<Point>, | ||
* minX: number, | ||
* minY: number, | ||
* maxX: number, | ||
* maxY: number | ||
* }} Points | ||
*/ | ||
/** | ||
* @type {(pathData: Array<PathDataItem>) => Points} | ||
*/ | ||
function gatherPoints(pathData) { | ||
/** | ||
* @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} | ||
*/ | ||
const addPoint = (path, point) => { | ||
if (!path.list.length || point[1] > path.list[path.maxY][1]) { | ||
path.maxY = path.list.length; | ||
points.maxY = points.list.length | ||
? Math.max(point[1], points.maxY) | ||
: point[1]; | ||
} | ||
if (!path.list.length || point[0] > path.list[path.maxX][0]) { | ||
path.maxX = path.list.length; | ||
points.maxX = points.list.length | ||
? Math.max(point[0], points.maxX) | ||
: point[0]; | ||
} | ||
if (!path.list.length || point[1] < path.list[path.minY][1]) { | ||
path.minY = path.list.length; | ||
points.minY = points.list.length | ||
? Math.min(point[1], points.minY) | ||
: point[1]; | ||
} | ||
if (!path.list.length || point[0] < path.list[path.minX][0]) { | ||
path.minX = path.list.length; | ||
points.minX = points.list.length | ||
? Math.min(point[0], points.minX) | ||
: point[0]; | ||
} | ||
path.list.push(point); | ||
}; | ||
for (let i = 0; i < pathData.length; i += 1) { | ||
const pathDataItem = pathData[i]; | ||
let subPath = | ||
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]; | ||
let basePoint = | ||
subPath.list.length === 0 ? null : subPath.list[subPath.list.length - 1]; | ||
let data = pathDataItem.args; | ||
let ctrlPoint = basePoint; | ||
/** | ||
* @type {(n: number, i: number) => number} | ||
* TODO fix null hack | ||
*/ | ||
const toAbsolute = (n, i) => n + (basePoint == null ? 0 : basePoint[i % 2]); | ||
switch (pathDataItem.command) { | ||
case 'M': | ||
subPath = { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 }; | ||
points.list.push(subPath); | ||
break; | ||
case 'H': | ||
if (basePoint != null) { | ||
addPoint(subPath, [data[0], basePoint[1]]); | ||
} | ||
break; | ||
case 'V': | ||
if (basePoint != null) { | ||
addPoint(subPath, [basePoint[0], data[0]]); | ||
} | ||
break; | ||
case 'Q': | ||
addPoint(subPath, data.slice(0, 2)); | ||
prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; // Save control point for shorthand | ||
break; | ||
case 'T': | ||
if ( | ||
basePoint != null && | ||
prev != null && | ||
(prev.command == 'Q' || prev.command == 'T') | ||
) { | ||
ctrlPoint = [ | ||
basePoint[0] + prevCtrlPoint[0], | ||
basePoint[1] + prevCtrlPoint[1], | ||
]; | ||
addPoint(subPath, ctrlPoint); | ||
prevCtrlPoint = [data[0] - ctrlPoint[0], data[1] - ctrlPoint[1]]; | ||
} | ||
break; | ||
case 'C': | ||
if (basePoint != null) { | ||
// Approximate quibic Bezier curve with middle points between control points | ||
addPoint(subPath, [ | ||
0.5 * (basePoint[0] + data[0]), | ||
0.5 * (basePoint[1] + data[1]), | ||
]); | ||
} | ||
addPoint(subPath, [ | ||
basePoint[0] + 0.5 * prevCtrlPoint[0], | ||
basePoint[1] + 0.5 * prevCtrlPoint[1], | ||
0.5 * (data[0] + data[2]), | ||
0.5 * (data[1] + data[3]), | ||
]); | ||
ctrlPoint = [ | ||
basePoint[0] + prevCtrlPoint[0], | ||
basePoint[1] + prevCtrlPoint[1], | ||
]; | ||
} | ||
addPoint(subPath, [ | ||
0.5 * (ctrlPoint[0] + data[0]), | ||
0.5 * (ctrlPoint[1] + data[1]), | ||
]); | ||
addPoint(subPath, [0.5 * (data[0] + data[2]), 0.5 * (data[1] + data[3])]); | ||
prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; | ||
break; | ||
case 'A': | ||
// Convert the arc to bezier curves and use the same approximation | ||
var curves = a2c.apply(0, basePoint.concat(data)); | ||
for (var cData; (cData = curves.splice(0, 6).map(toAbsolute)).length; ) { | ||
addPoint(subPath, [ | ||
0.5 * (basePoint[0] + cData[0]), | ||
0.5 * (basePoint[1] + cData[1]), | ||
0.5 * (data[2] + data[4]), | ||
0.5 * (data[3] + data[5]), | ||
]); | ||
prevCtrlPoint = [data[4] - data[2], data[5] - data[3]]; // Save control point for shorthand | ||
break; | ||
case 'S': | ||
if ( | ||
basePoint != null && | ||
prev != null && | ||
(prev.command == 'C' || prev.command == 'S') | ||
) { | ||
addPoint(subPath, [ | ||
basePoint[0] + 0.5 * prevCtrlPoint[0], | ||
basePoint[1] + 0.5 * prevCtrlPoint[1], | ||
]); | ||
ctrlPoint = [ | ||
basePoint[0] + prevCtrlPoint[0], | ||
basePoint[1] + prevCtrlPoint[1], | ||
]; | ||
} | ||
if (ctrlPoint != null) { | ||
addPoint(subPath, [ | ||
0.5 * (ctrlPoint[0] + data[0]), | ||
0.5 * (ctrlPoint[1] + data[1]), | ||
]); | ||
} | ||
addPoint(subPath, [ | ||
0.5 * (cData[0] + cData[2]), | ||
0.5 * (cData[1] + cData[3]), | ||
0.5 * (data[0] + data[2]), | ||
0.5 * (data[1] + data[3]), | ||
]); | ||
addPoint(subPath, [ | ||
0.5 * (cData[2] + cData[4]), | ||
0.5 * (cData[3] + cData[5]), | ||
]); | ||
if (curves.length) addPoint(subPath, (basePoint = cData.slice(-2))); | ||
} | ||
break; | ||
} | ||
// Save final command coordinates | ||
if (data && data.length >= 2) addPoint(subPath, data.slice(-2)); | ||
return points; | ||
prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; | ||
break; | ||
function toAbsolute(n, i) { | ||
return n + basePoint[i % 2]; | ||
} | ||
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)); | ||
for ( | ||
var cData; | ||
(cData = curves.splice(0, 6).map(toAbsolute)).length; | ||
// Writes data about the extreme points on each axle | ||
function addPoint(path, point) { | ||
if (!path.length || point[1] > path[path.maxY][1]) { | ||
path.maxY = path.length; | ||
points.maxY = points.length ? Math.max(point[1], points.maxY) : point[1]; | ||
) { | ||
if (basePoint != null) { | ||
addPoint(subPath, [ | ||
0.5 * (basePoint[0] + cData[0]), | ||
0.5 * (basePoint[1] + cData[1]), | ||
]); | ||
} | ||
addPoint(subPath, [ | ||
0.5 * (cData[0] + cData[2]), | ||
0.5 * (cData[1] + cData[3]), | ||
]); | ||
addPoint(subPath, [ | ||
0.5 * (cData[2] + cData[4]), | ||
0.5 * (cData[3] + cData[5]), | ||
]); | ||
if (curves.length) addPoint(subPath, (basePoint = cData.slice(-2))); | ||
} | ||
} | ||
break; | ||
} | ||
if (!path.length || point[0] > path[path.maxX][0]) { | ||
path.maxX = path.length; | ||
points.maxX = points.length ? Math.max(point[0], points.maxX) : point[0]; | ||
} | ||
if (!path.length || point[1] < path[path.minY][1]) { | ||
path.minY = path.length; | ||
points.minY = points.length ? Math.min(point[1], points.minY) : point[1]; | ||
} | ||
if (!path.length || point[0] < path[path.minX][0]) { | ||
path.minX = path.length; | ||
points.minX = points.length ? Math.min(point[0], points.minX) : point[0]; | ||
} | ||
path.push(point); | ||
// Save final command coordinates | ||
if (data.length >= 2) addPoint(subPath, data.slice(-2)); | ||
} | ||
return points; | ||
} | ||
@@ -653,6 +627,6 @@ | ||
* | ||
* @param points An array of [X, Y] coordinates | ||
* @type {(points: Point) => Point} | ||
*/ | ||
function convexHull(points) { | ||
points.sort(function (a, b) { | ||
points.list.sort(function (a, b) { | ||
return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]; | ||
@@ -664,31 +638,33 @@ }); | ||
bottom = 0; | ||
for (let i = 0; i < points.length; i++) { | ||
for (let i = 0; i < points.list.length; i++) { | ||
while ( | ||
lower.length >= 2 && | ||
cross(lower[lower.length - 2], lower[lower.length - 1], points[i]) <= 0 | ||
cross(lower[lower.length - 2], lower[lower.length - 1], points.list[i]) <= | ||
0 | ||
) { | ||
lower.pop(); | ||
} | ||
if (points[i][1] < points[minY][1]) { | ||
if (points.list[i][1] < points.list[minY][1]) { | ||
minY = i; | ||
bottom = lower.length; | ||
} | ||
lower.push(points[i]); | ||
lower.push(points.list[i]); | ||
} | ||
var upper = [], | ||
maxY = points.length - 1, | ||
maxY = points.list.length - 1, | ||
top = 0; | ||
for (let i = points.length; i--; ) { | ||
for (let i = points.list.length; i--; ) { | ||
while ( | ||
upper.length >= 2 && | ||
cross(upper[upper.length - 2], upper[upper.length - 1], points[i]) <= 0 | ||
cross(upper[upper.length - 2], upper[upper.length - 1], points.list[i]) <= | ||
0 | ||
) { | ||
upper.pop(); | ||
} | ||
if (points[i][1] > points[maxY][1]) { | ||
if (points.list[i][1] > points.list[maxY][1]) { | ||
maxY = i; | ||
top = upper.length; | ||
} | ||
upper.push(points[i]); | ||
upper.push(points.list[i]); | ||
} | ||
@@ -700,8 +676,14 @@ | ||
var hull = lower.concat(upper); | ||
const hullList = lower.concat(upper); | ||
hull.minX = 0; // by sorting | ||
hull.maxX = lower.length; | ||
hull.minY = bottom; | ||
hull.maxY = (lower.length + top) % hull.length; | ||
/** | ||
* @type {Point} | ||
*/ | ||
const hull = { | ||
list: hullList, | ||
minX: 0, // by sorting | ||
maxX: lower.length, | ||
minY: bottom, | ||
maxY: (lower.length + top) % hullList.length, | ||
}; | ||
@@ -711,2 +693,5 @@ return hull; | ||
/** | ||
* @type {(o: Array<number>, a: Array<number>, b: Array<number>) => number} | ||
*/ | ||
function cross(o, a, b) { | ||
@@ -716,7 +701,20 @@ 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/ | ||
/** | ||
* Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/ | ||
* 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>} | ||
*/ | ||
function a2c( | ||
const a2c = ( | ||
x1, | ||
@@ -732,14 +730,23 @@ y1, | ||
recursive | ||
) { | ||
) => { | ||
// for more information of where this Math came from visit: | ||
// https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes | ||
var _120 = (Math.PI * 120) / 180, | ||
rad = (Math.PI / 180) * (+angle || 0), | ||
res = [], | ||
rotateX = function (x, y, rad) { | ||
return x * Math.cos(rad) - y * Math.sin(rad); | ||
}, | ||
rotateY = function (x, y, rad) { | ||
return x * Math.sin(rad) + y * Math.cos(rad); | ||
}; | ||
const _120 = (Math.PI * 120) / 180; | ||
const rad = (Math.PI / 180) * (+angle || 0); | ||
/** | ||
* @type {Array<number>} | ||
*/ | ||
let res = []; | ||
/** | ||
* @type {(x: number, y: number, rad: number) => number} | ||
*/ | ||
const rotateX = (x, y, rad) => { | ||
return x * Math.cos(rad) - y * Math.sin(rad); | ||
}; | ||
/** | ||
* @type {(x: number, y: number, rad: number) => number} | ||
*/ | ||
const rotateY = (x, y, rad) => { | ||
return x * Math.sin(rad) + y * Math.cos(rad); | ||
}; | ||
if (!recursive) { | ||
@@ -758,16 +765,15 @@ x1 = rotateX(x1, y1, -rad); | ||
} | ||
var rx2 = rx * rx, | ||
ry2 = ry * ry, | ||
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) | ||
) | ||
), | ||
cx = (k * rx * y) / ry + (x1 + x2) / 2, | ||
cy = (k * -ry * x) / rx + (y1 + y2) / 2, | ||
f1 = Math.asin(((y1 - cy) / ry).toFixed(9)), | ||
f2 = Math.asin(((y2 - cy) / ry).toFixed(9)); | ||
var rx2 = rx * rx; | ||
var ry2 = ry * ry; | ||
var 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) | ||
) | ||
); | ||
var cx = (k * rx * y) / ry + (x1 + x2) / 2; | ||
var cy = (k * -ry * x) / rx + (y1 + y2) / 2; | ||
var f1 = Math.asin(Number(((y1 - cy) / ry).toFixed(9))); | ||
var f2 = Math.asin(Number(((y2 - cy) / ry).toFixed(9))); | ||
@@ -834,2 +840,2 @@ f1 = x1 < cx ? Math.PI - f1 : f1; | ||
} | ||
} | ||
}; |
'use strict'; | ||
var regTransformTypes = /matrix|translate|scale|rotate|skewX|skewY/, | ||
regTransformSplit = /\s*(matrix|translate|scale|rotate|skewX|skewY)\s*\(\s*(.+?)\s*\)[\s,]*/, | ||
regNumericValues = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g; | ||
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. | ||
* | ||
* @param {String} transformString input string | ||
* @param {Object} params plugin params | ||
* @return {Array} output array | ||
* @type {(transformString: string) => Array<TransformItem>} | ||
*/ | ||
exports.transform2js = function (transformString) { | ||
exports.transform2js = (transformString) => { | ||
// JS representation of the transform data | ||
var transforms = [], | ||
// current transform context | ||
current; | ||
/** | ||
* @type {Array<TransformItem>} | ||
*/ | ||
const transforms = []; | ||
// current transform context | ||
/** | ||
* @type {null | TransformItem} | ||
*/ | ||
let current = null; | ||
// split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate', '-45', ''] | ||
transformString.split(regTransformSplit).forEach(function (item) { | ||
for (const item of transformString.split(regTransformSplit)) { | ||
var num; | ||
if (item) { | ||
@@ -28,3 +35,4 @@ // if item is a translate function | ||
// then collect it and change current context | ||
transforms.push((current = { name: item })); | ||
current = { name: item, data: [] }; | ||
transforms.push(current); | ||
// else if item is data | ||
@@ -36,11 +44,11 @@ } else { | ||
num = Number(num); | ||
if (current.data) current.data.push(num); | ||
else current.data = [num]; | ||
if (current != null) { | ||
current.data.push(num); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
// return empty array if broken transform (no data) | ||
return current && current.data ? transforms : []; | ||
return current == null || current.data.length == 0 ? [] : transforms; | ||
}; | ||
@@ -51,8 +59,7 @@ | ||
* | ||
* @param {Array} input transforms array | ||
* @return {Array} output matrix array | ||
* @type {(transforms: Array<TransformItem>) => TransformItem} | ||
*/ | ||
exports.transformsMultiply = function (transforms) { | ||
exports.transformsMultiply = (transforms) => { | ||
// convert transforms objects to the matrices | ||
transforms = transforms.map(function (transform) { | ||
const matrixData = transforms.map((transform) => { | ||
if (transform.name === 'matrix') { | ||
@@ -63,70 +70,109 @@ return transform.data; | ||
}); | ||
// multiply all matrices into one | ||
transforms = { | ||
const matrixTransform = { | ||
name: 'matrix', | ||
data: | ||
transforms.length > 0 ? transforms.reduce(multiplyTransformMatrices) : [], | ||
matrixData.length > 0 ? matrixData.reduce(multiplyTransformMatrices) : [], | ||
}; | ||
return transforms; | ||
return matrixTransform; | ||
}; | ||
/** | ||
* Do math like a schoolgirl. | ||
* | ||
* @type {Object} | ||
* math utilities in radians. | ||
*/ | ||
var mth = (exports.mth = { | ||
rad: function (deg) { | ||
const mth = { | ||
/** | ||
* @type {(deg: number) => number} | ||
*/ | ||
rad: (deg) => { | ||
return (deg * Math.PI) / 180; | ||
}, | ||
deg: function (rad) { | ||
/** | ||
* @type {(rad: number) => number} | ||
*/ | ||
deg: (rad) => { | ||
return (rad * 180) / Math.PI; | ||
}, | ||
cos: function (deg) { | ||
return Math.cos(this.rad(deg)); | ||
/** | ||
* @type {(deg: number) => number} | ||
*/ | ||
cos: (deg) => { | ||
return Math.cos(mth.rad(deg)); | ||
}, | ||
acos: function (val, floatPrecision) { | ||
return +this.deg(Math.acos(val)).toFixed(floatPrecision); | ||
/** | ||
* @type {(val: number, floatPrecision: number) => number} | ||
*/ | ||
acos: (val, floatPrecision) => { | ||
return Number(mth.deg(Math.acos(val)).toFixed(floatPrecision)); | ||
}, | ||
sin: function (deg) { | ||
return Math.sin(this.rad(deg)); | ||
/** | ||
* @type {(deg: number) => number} | ||
*/ | ||
sin: (deg) => { | ||
return Math.sin(mth.rad(deg)); | ||
}, | ||
asin: function (val, floatPrecision) { | ||
return +this.deg(Math.asin(val)).toFixed(floatPrecision); | ||
/** | ||
* @type {(val: number, floatPrecision: number) => number} | ||
*/ | ||
asin: (val, floatPrecision) => { | ||
return Number(mth.deg(Math.asin(val)).toFixed(floatPrecision)); | ||
}, | ||
tan: function (deg) { | ||
return Math.tan(this.rad(deg)); | ||
/** | ||
* @type {(deg: number) => number} | ||
*/ | ||
tan: (deg) => { | ||
return Math.tan(mth.rad(deg)); | ||
}, | ||
atan: function (val, floatPrecision) { | ||
return +this.deg(Math.atan(val)).toFixed(floatPrecision); | ||
/** | ||
* @type {(val: number, floatPrecision: number) => number} | ||
*/ | ||
atan: (val, floatPrecision) => { | ||
return Number(mth.deg(Math.atan(val)).toFixed(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 | ||
*/ | ||
/** | ||
* Decompose matrix into simple transforms. See | ||
* https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html | ||
* | ||
* @param {Object} data matrix transform object | ||
* @return {Object|Array} transforms array or original transform object | ||
* @type {(transform: TransformItem, params: TransformParams) => Array<TransformItem>} | ||
*/ | ||
exports.matrixToTransform = function (transform, params) { | ||
var floatPrecision = params.floatPrecision, | ||
data = transform.data, | ||
transforms = [], | ||
sx = +Math.hypot(data[0], data[1]).toFixed(params.transformPrecision), | ||
sy = +((data[0] * data[3] - data[1] * data[2]) / sx).toFixed( | ||
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 | ||
), | ||
colsSum = data[0] * data[2] + data[1] * data[3], | ||
rowsSum = data[0] * data[1] + data[2] * data[3], | ||
scaleBefore = rowsSum != 0 || sx == sy; | ||
) | ||
); | ||
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; | ||
@@ -184,6 +230,7 @@ // [..., ..., ..., ..., tx, ty] → translate(tx, ty) | ||
sin = data[1] / (scaleBefore ? sx : sy), | ||
x = data[4] * (scaleBefore || sy), | ||
y = data[5] * (scaleBefore || sx), | ||
x = data[4] * (scaleBefore ? 1 : sy), | ||
y = data[5] * (scaleBefore ? 1 : sx), | ||
denom = | ||
(Math.pow(1 - cos, 2) + Math.pow(sin, 2)) * (scaleBefore || sx * sy); | ||
(Math.pow(1 - cos, 2) + Math.pow(sin, 2)) * | ||
(scaleBefore ? 1 : sx * sy); | ||
rotate.push(((1 - cos) * x - sin * y) / denom); | ||
@@ -195,3 +242,3 @@ rotate.push(((1 - cos) * y + sin * x) / denom); | ||
} else if (data[1] || data[2]) { | ||
return transform; | ||
return [transform]; | ||
} | ||
@@ -211,18 +258,15 @@ | ||
* | ||
* @param {Object} transform transform object | ||
* @return {Array} matrix data | ||
* @type {(transform: TransformItem) => Array<number> } | ||
*/ | ||
function transformToMatrix(transform) { | ||
if (transform.name === 'matrix') return transform.data; | ||
var matrix; | ||
const transformToMatrix = (transform) => { | ||
if (transform.name === 'matrix') { | ||
return transform.data; | ||
} | ||
switch (transform.name) { | ||
case 'translate': | ||
// [1, 0, 0, 1, tx, ty] | ||
matrix = [1, 0, 0, 1, transform.data[0], transform.data[1] || 0]; | ||
break; | ||
return [1, 0, 0, 1, transform.data[0], transform.data[1] || 0]; | ||
case 'scale': | ||
// [sx, 0, 0, sy, 0, 0] | ||
matrix = [ | ||
return [ | ||
transform.data[0], | ||
@@ -235,3 +279,2 @@ 0, | ||
]; | ||
break; | ||
case 'rotate': | ||
@@ -243,4 +286,3 @@ // [cos(a), sin(a), -sin(a), cos(a), x, y] | ||
cy = transform.data[2] || 0; | ||
matrix = [ | ||
return [ | ||
cos, | ||
@@ -253,16 +295,13 @@ sin, | ||
]; | ||
break; | ||
case 'skewX': | ||
// [1, 0, tan(a), 1, 0, 0] | ||
matrix = [1, 0, mth.tan(transform.data[0]), 1, 0, 0]; | ||
break; | ||
return [1, 0, mth.tan(transform.data[0]), 1, 0, 0]; | ||
case 'skewY': | ||
// [1, tan(a), 0, 1, 0, 0] | ||
matrix = [1, mth.tan(transform.data[0]), 0, 1, 0, 0]; | ||
break; | ||
return [1, mth.tan(transform.data[0]), 0, 1, 0, 0]; | ||
default: | ||
throw Error(`Unknown transform ${transform.name}`); | ||
} | ||
}; | ||
return matrix; | ||
} | ||
/** | ||
@@ -274,31 +313,34 @@ * Applies transformation to an arc. To do so, we represent ellipse as a matrix, multiply it | ||
* | ||
* @param {Array} cursor [x, y] | ||
* @param {Array} arc [a, b, rotation in deg] | ||
* @param {Array} transform transformation matrix | ||
* @return {Array} arc transformed input arc | ||
* @type {( | ||
* cursor: [x: number, y: number], | ||
* arc: Array<number>, | ||
* transform: Array<number> | ||
* ) => Array<number>} | ||
*/ | ||
exports.transformArc = function (cursor, arc, transform) { | ||
exports.transformArc = (cursor, arc, transform) => { | ||
const x = arc[5] - cursor[0]; | ||
const y = arc[6] - cursor[1]; | ||
var a = arc[0], | ||
b = arc[1], | ||
rot = (arc[2] * Math.PI) / 180, | ||
cos = Math.cos(rot), | ||
sin = Math.sin(rot), | ||
h = | ||
let a = arc[0]; | ||
let b = arc[1]; | ||
const rot = (arc[2] * Math.PI) / 180; | ||
const cos = Math.cos(rot); | ||
const sin = Math.sin(rot); | ||
// skip if radius is 0 | ||
if (a > 0 && b > 0) { | ||
let h = | ||
Math.pow(x * cos + y * sin, 2) / (4 * a * a) + | ||
Math.pow(y * cos - x * sin, 2) / (4 * b * b); | ||
if (h > 1) { | ||
h = Math.sqrt(h); | ||
a *= h; | ||
b *= h; | ||
if (h > 1) { | ||
h = Math.sqrt(h); | ||
a *= h; | ||
b *= h; | ||
} | ||
} | ||
var ellipse = [a * cos, a * sin, -b * sin, b * cos, 0, 0], | ||
m = multiplyTransformMatrices(transform, ellipse), | ||
// Decompose the new ellipse matrix | ||
lastCol = m[2] * m[2] + m[3] * m[3], | ||
squareSum = m[0] * m[0] + m[1] * m[1] + lastCol, | ||
root = | ||
Math.hypot(m[0] - m[3], m[1] + m[2]) * | ||
Math.hypot(m[0] + m[3], m[1] - m[2]); | ||
const ellipse = [a * cos, a * sin, -b * sin, b * cos, 0, 0]; | ||
const m = multiplyTransformMatrices(transform, ellipse); | ||
// Decompose the new ellipse matrix | ||
const lastCol = m[2] * m[2] + m[3] * m[3]; | ||
const squareSum = m[0] * m[0] + m[1] * m[1] + lastCol; | ||
const root = | ||
Math.hypot(m[0] - m[3], m[1] + m[2]) * Math.hypot(m[0] + m[3], m[1] - m[2]); | ||
@@ -310,9 +352,9 @@ if (!root) { | ||
} else { | ||
var majorAxisSqr = (squareSum + root) / 2, | ||
minorAxisSqr = (squareSum - root) / 2, | ||
major = Math.abs(majorAxisSqr - lastCol) > 1e-6, | ||
sub = (major ? majorAxisSqr : minorAxisSqr) - lastCol, | ||
rowsSum = m[0] * m[2] + m[1] * m[3], | ||
term1 = m[0] * sub + m[2] * rowsSum, | ||
term2 = m[1] * sub + m[3] * rowsSum; | ||
const majorAxisSqr = (squareSum + root) / 2; | ||
const minorAxisSqr = (squareSum - root) / 2; | ||
const major = Math.abs(majorAxisSqr - lastCol) > 1e-6; | ||
const sub = (major ? majorAxisSqr : minorAxisSqr) - lastCol; | ||
const rowsSum = m[0] * m[2] + m[1] * m[3]; | ||
const term1 = m[0] * sub + m[2] * rowsSum; | ||
const term2 = m[1] * sub + m[3] * rowsSum; | ||
arc[0] = Math.sqrt(majorAxisSqr); | ||
@@ -338,7 +380,5 @@ arc[1] = Math.sqrt(minorAxisSqr); | ||
* | ||
* @param {Array} a matrix A data | ||
* @param {Array} b matrix B data | ||
* @return {Array} result | ||
* @type {(a: Array<number>, b: Array<number>) => Array<number>} | ||
*/ | ||
function multiplyTransformMatrices(a, b) { | ||
const multiplyTransformMatrices = (a, b) => { | ||
return [ | ||
@@ -352,2 +392,2 @@ a[0] * b[0] + a[2] * b[1], | ||
]; | ||
} | ||
}; |
'use strict'; | ||
const { closestByName } = require('../lib/xast.js'); | ||
exports.name = 'addAttributesToSVGElement'; | ||
exports.type = 'perItem'; | ||
exports.type = 'visitor'; | ||
exports.active = false; | ||
exports.description = 'adds attributes to an outer <svg> element'; | ||
@@ -56,31 +51,38 @@ | ||
* @author April Arcus | ||
* | ||
* @type {import('../lib/types').Plugin<{ | ||
* attribute?: string | Record<string, null | string>, | ||
* attributes?: Array<string | Record<string, null | string>> | ||
* }>} | ||
*/ | ||
exports.fn = (node, params) => { | ||
if ( | ||
node.type === 'element' && | ||
node.name === 'svg' && | ||
closestByName(node.parentNode, 'svg') == null | ||
) { | ||
if (!params || !(Array.isArray(params.attributes) || params.attribute)) { | ||
console.error(ENOCLS); | ||
return; | ||
} | ||
const attributes = params.attributes || [params.attribute]; | ||
for (const attribute of attributes) { | ||
if (typeof attribute === 'string') { | ||
if (node.attributes[attribute] == null) { | ||
node.attributes[attribute] = undefined; | ||
} | ||
} | ||
if (typeof attribute === 'object') { | ||
for (const key of Object.keys(attribute)) { | ||
if (node.attributes[key] == null) { | ||
node.attributes[key] = attribute[key]; | ||
exports.fn = (root, params) => { | ||
if (!Array.isArray(params.attributes) && !params.attribute) { | ||
console.error(ENOCLS); | ||
return null; | ||
} | ||
const attributes = params.attributes || [params.attribute]; | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
if (node.name === 'svg' && parentNode.type === 'root') { | ||
for (const attribute of attributes) { | ||
if (typeof attribute === 'string') { | ||
if (node.attributes[attribute] == null) { | ||
// @ts-ignore 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 | ||
node.attributes[key] = attribute[key]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
exports.name = 'addClassesToSVGElement'; | ||
exports.type = 'full'; | ||
exports.type = 'visitor'; | ||
exports.active = false; | ||
exports.description = 'adds classnames to an outer <svg> element'; | ||
@@ -15,9 +12,19 @@ | ||
plugins: | ||
- addClassesToSVGElement: | ||
className: "mySvg" | ||
plugins: [ | ||
{ | ||
name: "addClassesToSVGElement", | ||
params: { | ||
className: "mySvg" | ||
} | ||
} | ||
] | ||
plugins: | ||
- addClassesToSVGElement: | ||
classNames: ["mySvg", "size-big"] | ||
plugins: [ | ||
{ | ||
name: "addClassesToSVGElement", | ||
params: { | ||
classNames: ["mySvg", "size-big"] | ||
} | ||
} | ||
] | ||
`; | ||
@@ -28,32 +35,55 @@ | ||
* | ||
* plugins: | ||
* - addClassesToSVGElement: | ||
* className: 'mySvg' | ||
* plugins: [ | ||
* { | ||
* name: "addClassesToSVGElement", | ||
* params: { | ||
* className: "mySvg" | ||
* } | ||
* } | ||
* ] | ||
* | ||
* plugins: | ||
* - addClassesToSVGElement: | ||
* classNames: ['mySvg', 'size-big'] | ||
* plugins: [ | ||
* { | ||
* name: "addClassesToSVGElement", | ||
* params: { | ||
* classNames: ["mySvg", "size-big"] | ||
* } | ||
* } | ||
* ] | ||
* | ||
* @author April Arcus | ||
* | ||
* @type {import('../lib/types').Plugin<{ | ||
* className?: string, | ||
* classNames?: Array<string> | ||
* }>} | ||
*/ | ||
exports.fn = function (data, params) { | ||
exports.fn = (root, params) => { | ||
if ( | ||
!params || | ||
!( | ||
(Array.isArray(params.classNames) && params.classNames.some(String)) || | ||
params.className | ||
) | ||
!(Array.isArray(params.classNames) && params.classNames.some(String)) && | ||
!params.className | ||
) { | ||
console.error(ENOCLS); | ||
return data; | ||
return null; | ||
} | ||
var classNames = params.classNames || [params.className], | ||
svg = data.children[0]; | ||
if (svg.isElem('svg')) { | ||
svg.class.add.apply(svg.class, classNames); | ||
} | ||
return data; | ||
const classNames = params.classNames || [params.className]; | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
if (node.name === 'svg' && parentNode.type === 'root') { | ||
const classList = new Set( | ||
node.attributes.class == null | ||
? null | ||
: node.attributes.class.split(' ') | ||
); | ||
for (const className of classNames) { | ||
if (className != null) { | ||
classList.add(className); | ||
} | ||
} | ||
node.attributes.class = Array.from(classList).join(' '); | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -17,2 +17,8 @@ 'use strict'; | ||
* @author Kir Belevich | ||
* | ||
* @type {import('../lib/types').Plugin<{ | ||
* newlines?: boolean, | ||
* trim?: boolean, | ||
* spaces?: boolean | ||
* }>} | ||
*/ | ||
@@ -19,0 +25,0 @@ exports.fn = (root, params) => { |
'use strict'; | ||
const { traverse } = require('../lib/xast.js'); | ||
const { visit } = require('../lib/xast.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'cleanupEnableBackground'; | ||
exports.type = 'full'; | ||
exports.active = true; | ||
exports.description = | ||
@@ -24,53 +21,56 @@ 'remove or cleanup enable-background attribute when possible'; | ||
* | ||
* @param {Object} root current iteration item | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @author Kir Belevich | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
exports.fn = function (root) { | ||
const regEnableBackground = /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/; | ||
exports.fn = (root) => { | ||
const regEnableBackground = | ||
/^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/; | ||
let hasFilter = false; | ||
const elems = ['svg', 'mask', 'pattern']; | ||
visit(root, { | ||
element: { | ||
enter: (node) => { | ||
if (node.name === 'filter') { | ||
hasFilter = true; | ||
} | ||
}, | ||
}, | ||
}); | ||
traverse(root, (node) => { | ||
if (node.type === 'element') { | ||
if ( | ||
elems.includes(node.name) && | ||
node.attributes['enable-background'] != null && | ||
node.attributes.width != null && | ||
node.attributes.height != null | ||
) { | ||
const match = node.attributes['enable-background'].match( | ||
regEnableBackground | ||
); | ||
if (match) { | ||
return { | ||
element: { | ||
enter: (node) => { | ||
if (node.attributes['enable-background'] == null) { | ||
return; | ||
} | ||
if (hasFilter) { | ||
if ( | ||
node.attributes.width === match[1] && | ||
node.attributes.height === match[3] | ||
(node.name === 'svg' || | ||
node.name === 'mask' || | ||
node.name === 'pattern') && | ||
node.attributes.width != null && | ||
node.attributes.height != null | ||
) { | ||
if (node.name === 'svg') { | ||
delete node.attributes['enable-background']; | ||
} else { | ||
node.attributes['enable-background'] = 'new'; | ||
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'; | ||
} | ||
} | ||
} | ||
} else { | ||
//we don't need 'enable-background' if we have no filters | ||
delete node.attributes['enable-background']; | ||
} | ||
} | ||
if (node.name === 'filter') { | ||
hasFilter = true; | ||
} | ||
} | ||
}); | ||
if (hasFilter === false) { | ||
traverse(root, (node) => { | ||
if (node.type === 'element') { | ||
//we don't need 'enable-background' if we have no filters | ||
delete node.attributes['enable-background']; | ||
} | ||
}); | ||
} | ||
return root; | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -6,17 +6,8 @@ 'use strict'; | ||
exports.name = 'cleanupListOfValues'; | ||
exports.type = 'perItem'; | ||
exports.type = 'visitor'; | ||
exports.active = false; | ||
exports.description = 'rounds list of values to the fixed precision'; | ||
exports.params = { | ||
floatPrecision: 3, | ||
leadingZero: true, | ||
defaultPx: true, | ||
convertToPx: true, | ||
}; | ||
const regNumericValues = /^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/; | ||
const regNumericValues = | ||
/^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/; | ||
const regSeparator = /\s+,?\s*|,\s*/; | ||
@@ -30,2 +21,3 @@ const absoluteLengths = { | ||
pc: 16, | ||
px: 1, | ||
}; | ||
@@ -41,3 +33,2 @@ | ||
* | ||
* | ||
* <polygon points="208.250977 77.1308594 223.069336 ... "/> | ||
@@ -47,89 +38,68 @@ * ⬇ | ||
* | ||
* @author kiyopikko | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* | ||
* @author kiyopikko | ||
* @type {import('../lib/types').Plugin<{ | ||
* floatPrecision?: number, | ||
* leadingZero?: boolean, | ||
* defaultPx?: boolean, | ||
* convertToPx?: boolean | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
if (item.type !== 'element') { | ||
return; | ||
} | ||
exports.fn = (_root, params) => { | ||
const { | ||
floatPrecision = 3, | ||
leadingZero = true, | ||
defaultPx = true, | ||
convertToPx = true, | ||
} = params; | ||
if (item.attributes.points != null) { | ||
item.attributes.points = roundValues(item.attributes.points); | ||
} | ||
/** | ||
* @type {(lists: string) => string} | ||
*/ | ||
const roundValues = (lists) => { | ||
const roundedList = []; | ||
if (item.attributes['enable-background'] != null) { | ||
item.attributes['enable-background'] = roundValues( | ||
item.attributes['enable-background'] | ||
); | ||
} | ||
for (const elem of lists.split(regSeparator)) { | ||
const match = elem.match(regNumericValues); | ||
const matchNew = elem.match(/new/); | ||
if (item.attributes.viewBox != null) { | ||
item.attributes.viewBox = roundValues(item.attributes.viewBox); | ||
} | ||
if (item.attributes['stroke-dasharray'] != null) { | ||
item.attributes['stroke-dasharray'] = roundValues( | ||
item.attributes['stroke-dasharray'] | ||
); | ||
} | ||
if (item.attributes.dx != null) { | ||
item.attributes.dx = roundValues(item.attributes.dx); | ||
} | ||
if (item.attributes.dy != null) { | ||
item.attributes.dy = roundValues(item.attributes.dy); | ||
} | ||
if (item.attributes.x != null) { | ||
item.attributes.x = roundValues(item.attributes.x); | ||
} | ||
if (item.attributes.y != null) { | ||
item.attributes.y = roundValues(item.attributes.y); | ||
} | ||
function roundValues(lists) { | ||
var num, | ||
units, | ||
match, | ||
matchNew, | ||
listsArr = lists.split(regSeparator), | ||
roundedList = []; | ||
for (const elem of listsArr) { | ||
match = elem.match(regNumericValues); | ||
matchNew = elem.match(/new/); | ||
// if attribute value matches regNumericValues | ||
if (match) { | ||
// round it to the fixed precision | ||
(num = +(+match[1]).toFixed(params.floatPrecision)), | ||
(units = match[3] || ''); | ||
let num = Number(Number(match[1]).toFixed(floatPrecision)); | ||
/** | ||
* @type {any} | ||
*/ | ||
let matchedUnit = match[3] || ''; | ||
/** | ||
* @type{'' | keyof typeof absoluteLengths} | ||
*/ | ||
let units = matchedUnit; | ||
// convert absolute values to pixels | ||
if (params.convertToPx && units && units in absoluteLengths) { | ||
var pxNum = +(absoluteLengths[units] * match[1]).toFixed( | ||
params.floatPrecision | ||
if (convertToPx && units && units in absoluteLengths) { | ||
const pxNum = Number( | ||
(absoluteLengths[units] * Number(match[1])).toFixed(floatPrecision) | ||
); | ||
if (String(pxNum).length < match[0].length) | ||
(num = pxNum), (units = 'px'); | ||
if (pxNum.toString().length < match[0].length) { | ||
num = pxNum; | ||
units = 'px'; | ||
} | ||
} | ||
// and remove leading zero | ||
if (params.leadingZero) { | ||
num = removeLeadingZero(num); | ||
let str; | ||
if (leadingZero) { | ||
str = removeLeadingZero(num); | ||
} else { | ||
str = num.toString(); | ||
} | ||
// remove default 'px' units | ||
if (params.defaultPx && units === 'px') { | ||
if (defaultPx && units === 'px') { | ||
units = ''; | ||
} | ||
roundedList.push(num + units); | ||
roundedList.push(str + units); | ||
} | ||
@@ -145,3 +115,45 @@ // if attribute value is "new"(only enable-background). | ||
return roundedList.join(' '); | ||
} | ||
}; | ||
return { | ||
element: { | ||
enter: (node) => { | ||
if (node.attributes.points != null) { | ||
node.attributes.points = roundValues(node.attributes.points); | ||
} | ||
if (node.attributes['enable-background'] != null) { | ||
node.attributes['enable-background'] = roundValues( | ||
node.attributes['enable-background'] | ||
); | ||
} | ||
if (node.attributes.viewBox != null) { | ||
node.attributes.viewBox = roundValues(node.attributes.viewBox); | ||
} | ||
if (node.attributes['stroke-dasharray'] != null) { | ||
node.attributes['stroke-dasharray'] = roundValues( | ||
node.attributes['stroke-dasharray'] | ||
); | ||
} | ||
if (node.attributes.dx != null) { | ||
node.attributes.dx = roundValues(node.attributes.dx); | ||
} | ||
if (node.attributes.dy != null) { | ||
node.attributes.dy = roundValues(node.attributes.dy); | ||
} | ||
if (node.attributes.x != null) { | ||
node.attributes.x = roundValues(node.attributes.x); | ||
} | ||
if (node.attributes.y != null) { | ||
node.attributes.y = roundValues(node.attributes.y); | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
const { removeLeadingZero } = require('../lib/svgo/tools'); | ||
exports.name = 'cleanupNumericValues'; | ||
exports.type = 'perItem'; | ||
exports.type = 'visitor'; | ||
exports.active = true; | ||
exports.description = | ||
'rounds numeric values to the fixed precision, removes default ‘px’ units'; | ||
exports.params = { | ||
floatPrecision: 3, | ||
leadingZero: true, | ||
defaultPx: true, | ||
convertToPx: true, | ||
const regNumericValues = | ||
/^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/; | ||
const absoluteLengths = { | ||
// relative to px | ||
cm: 96 / 2.54, | ||
mm: 96 / 25.4, | ||
in: 96, | ||
pt: 4 / 3, | ||
pc: 16, | ||
px: 1, | ||
}; | ||
var regNumericValues = /^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/, | ||
removeLeadingZero = require('../lib/svgo/tools').removeLeadingZero, | ||
absoluteLengths = { | ||
// relative to px | ||
cm: 96 / 2.54, | ||
mm: 96 / 25.4, | ||
in: 96, | ||
pt: 4 / 3, | ||
pc: 16, | ||
}; | ||
/** | ||
@@ -34,62 +28,87 @@ * Round numeric values to the fixed precision, | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @author Kir Belevich | ||
* @type {import('../lib/types').Plugin<{ | ||
* floatPrecision?: number, | ||
* leadingZero?: boolean, | ||
* defaultPx?: boolean, | ||
* convertToPx?: boolean | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
if (item.type === 'element') { | ||
var floatPrecision = params.floatPrecision; | ||
exports.fn = (_root, params) => { | ||
const { | ||
floatPrecision = 3, | ||
leadingZero = true, | ||
defaultPx = true, | ||
convertToPx = true, | ||
} = params; | ||
if (item.attributes.viewBox != null) { | ||
var nums = item.attributes.viewBox.split(/\s,?\s*|,\s*/g); | ||
item.attributes.viewBox = nums | ||
.map(function (value) { | ||
var num = +value; | ||
return isNaN(num) ? value : +num.toFixed(floatPrecision); | ||
}) | ||
.join(' '); | ||
} | ||
return { | ||
element: { | ||
enter: (node) => { | ||
if (node.attributes.viewBox != null) { | ||
const nums = node.attributes.viewBox.split(/\s,?\s*|,\s*/g); | ||
node.attributes.viewBox = nums | ||
.map((value) => { | ||
const num = Number(value); | ||
return Number.isNaN(num) | ||
? value | ||
: Number(num.toFixed(floatPrecision)); | ||
}) | ||
.join(' '); | ||
} | ||
for (const [name, value] of Object.entries(item.attributes)) { | ||
// The `version` attribute is a text string and cannot be rounded | ||
if (name === 'version') { | ||
continue; | ||
} | ||
for (const [name, value] of Object.entries(node.attributes)) { | ||
// The `version` attribute is a text string and cannot be rounded | ||
if (name === 'version') { | ||
continue; | ||
} | ||
var match = value.match(regNumericValues); | ||
const match = value.match(regNumericValues); | ||
// if attribute value matches regNumericValues | ||
if (match) { | ||
// round it to the fixed precision | ||
var num = +(+match[1]).toFixed(floatPrecision), | ||
units = match[3] || ''; | ||
// 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} | ||
*/ | ||
let units = matchedUnit; | ||
// convert absolute values to pixels | ||
if (params.convertToPx && units && units in absoluteLengths) { | ||
var pxNum = +(absoluteLengths[units] * match[1]).toFixed( | ||
floatPrecision | ||
); | ||
// convert absolute values to pixels | ||
if (convertToPx && units !== '' && units in absoluteLengths) { | ||
const pxNum = Number( | ||
(absoluteLengths[units] * Number(match[1])).toFixed( | ||
floatPrecision | ||
) | ||
); | ||
if (pxNum.toString().length < match[0].length) { | ||
num = pxNum; | ||
units = 'px'; | ||
} | ||
} | ||
if (String(pxNum).length < match[0].length) { | ||
num = pxNum; | ||
units = 'px'; | ||
} | ||
} | ||
// and remove leading zero | ||
let str; | ||
if (leadingZero) { | ||
str = removeLeadingZero(num); | ||
} else { | ||
str = num.toString(); | ||
} | ||
// and remove leading zero | ||
if (params.leadingZero) { | ||
num = removeLeadingZero(num); | ||
} | ||
// remove default 'px' units | ||
if (defaultPx && units === 'px') { | ||
units = ''; | ||
} | ||
// remove default 'px' units | ||
if (params.defaultPx && units === 'px') { | ||
units = ''; | ||
node.attributes[name] = str + units; | ||
} | ||
} | ||
item.attributes[name] = num + units; | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
const collections = require('./_collections.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'convertColors'; | ||
exports.type = 'perItem'; | ||
exports.active = true; | ||
exports.description = 'converts colors: rgb() to #rrggbb and #rrggbb to #rgb'; | ||
exports.params = { | ||
currentColor: false, | ||
names2hex: true, | ||
rgb2hex: true, | ||
shorthex: true, | ||
shortname: true, | ||
const rNumber = '([+-]?(?:\\d*\\.\\d+|\\d+\\.?)%?)'; | ||
const rComma = '\\s*,\\s*'; | ||
const regRGB = new RegExp( | ||
'^rgb\\(\\s*' + rNumber + rComma + rNumber + rComma + rNumber + '\\s*\\)$' | ||
); | ||
const regHEX = /^#(([a-fA-F0-9])\2){3}$/; | ||
/** | ||
* Convert [r, g, b] to #rrggbb. | ||
* | ||
* @see https://gist.github.com/983535 | ||
* | ||
* @example | ||
* rgb2hex([255, 255, 255]) // '#ffffff' | ||
* | ||
* @author Jed Schmidt | ||
* | ||
* @type {(rgb: Array<number>) => string} | ||
*/ | ||
const convertRgbToHex = ([r, g, b]) => { | ||
// combine the octets into a 32-bit integer as: [1][r][g][b] | ||
const hexNumber = | ||
// operator precedence is (+) > (<<) > (|) | ||
((((256 + // [1][0] | ||
r) << // [1][r] | ||
8) | // [1][r][0] | ||
g) << // [1][r][g] | ||
8) | // [1][r][g][0] | ||
b; | ||
// serialize [1][r][g][b] to a hex string, and | ||
// remove the 1 to get the number with 0s intact | ||
return '#' + hexNumber.toString(16).slice(1).toUpperCase(); | ||
}; | ||
var collections = require('./_collections'), | ||
rNumber = '([+-]?(?:\\d*\\.\\d+|\\d+\\.?)%?)', | ||
rComma = '\\s*,\\s*', | ||
regRGB = new RegExp( | ||
'^rgb\\(\\s*' + rNumber + rComma + rNumber + rComma + rNumber + '\\s*\\)$' | ||
), | ||
regHEX = /^#(([a-fA-F0-9])\2){3}$/, | ||
none = /\bnone\b/i; | ||
/** | ||
@@ -48,84 +64,90 @@ * Convert different colors formats in element attributes to hex. | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @author Kir Belevich | ||
* @type {import('../lib/types').Plugin<{ | ||
* currentColor?: boolean | string | RegExp, | ||
* names2hex?: boolean, | ||
* rgb2hex?: boolean, | ||
* shorthex?: boolean, | ||
* shortname?: boolean, | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
if (item.type === 'element') { | ||
for (const [name, value] of Object.entries(item.attributes)) { | ||
if (collections.colorsProps.includes(name)) { | ||
let val = value; | ||
let match; | ||
exports.fn = (_root, params) => { | ||
const { | ||
currentColor = false, | ||
names2hex = true, | ||
rgb2hex = true, | ||
shorthex = true, | ||
shortname = true, | ||
} = params; | ||
// Convert colors to currentColor | ||
if (params.currentColor) { | ||
if (typeof params.currentColor === 'string') { | ||
match = val === params.currentColor; | ||
} else if (params.currentColor.exec) { | ||
match = params.currentColor.exec(val); | ||
} else { | ||
match = !val.match(none); | ||
} | ||
if (match) { | ||
val = 'currentColor'; | ||
} | ||
} | ||
return { | ||
element: { | ||
enter: (node) => { | ||
for (const [name, value] of Object.entries(node.attributes)) { | ||
if (collections.colorsProps.includes(name)) { | ||
let val = value; | ||
// Convert color name keyword to long hex | ||
if (params.names2hex && val.toLowerCase() in collections.colorsNames) { | ||
val = collections.colorsNames[val.toLowerCase()]; | ||
} | ||
// convert colors to currentColor | ||
if (currentColor) { | ||
let matched; | ||
if (typeof currentColor === 'string') { | ||
matched = val === currentColor; | ||
} else if (currentColor instanceof RegExp) { | ||
matched = currentColor.exec(val) != null; | ||
} else { | ||
matched = val !== 'none'; | ||
} | ||
if (matched) { | ||
val = 'currentColor'; | ||
} | ||
} | ||
// Convert rgb() to long hex | ||
if (params.rgb2hex && (match = val.match(regRGB))) { | ||
match = match.slice(1, 4).map(function (m) { | ||
if (m.indexOf('%') > -1) m = Math.round(parseFloat(m) * 2.55); | ||
// convert color name keyword to long hex | ||
if (names2hex) { | ||
const colorName = val.toLowerCase(); | ||
if (collections.colorsNames[colorName] != null) { | ||
val = collections.colorsNames[colorName]; | ||
} | ||
} | ||
return Math.max(0, Math.min(m, 255)); | ||
}); | ||
// convert rgb() to long hex | ||
if (rgb2hex) { | ||
let match = val.match(regRGB); | ||
if (match != null) { | ||
let nums = match.slice(1, 4).map((m) => { | ||
let n; | ||
if (m.indexOf('%') > -1) { | ||
n = Math.round(parseFloat(m) * 2.55); | ||
} else { | ||
n = Number(m); | ||
} | ||
return Math.max(0, Math.min(n, 255)); | ||
}); | ||
val = convertRgbToHex(nums); | ||
} | ||
} | ||
val = rgb2hex(match); | ||
} | ||
// convert long hex to short hex | ||
if (shorthex) { | ||
let match = val.match(regHEX); | ||
if (match != null) { | ||
val = '#' + match[0][1] + match[0][3] + match[0][5]; | ||
} | ||
} | ||
// Convert long hex to short hex | ||
if (params.shorthex && (match = val.match(regHEX))) { | ||
val = '#' + match[0][1] + match[0][3] + match[0][5]; | ||
} | ||
// convert hex to short name | ||
if (shortname) { | ||
const colorName = val.toLowerCase(); | ||
if (collections.colorsShortNames[colorName] != null) { | ||
val = collections.colorsShortNames[colorName]; | ||
} | ||
} | ||
// Convert hex to short name | ||
if (params.shortname) { | ||
var lowerVal = val.toLowerCase(); | ||
if (lowerVal in collections.colorsShortNames) { | ||
val = collections.colorsShortNames[lowerVal]; | ||
node.attributes[name] = val; | ||
} | ||
} | ||
item.attributes[name] = val; | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; | ||
/** | ||
* Convert [r, g, b] to #rrggbb. | ||
* | ||
* @see https://gist.github.com/983535 | ||
* | ||
* @example | ||
* rgb2hex([255, 255, 255]) // '#ffffff' | ||
* | ||
* @param {Array} rgb [r, g, b] | ||
* @return {String} #rrggbb | ||
* | ||
* @author Jed Schmidt | ||
*/ | ||
function rgb2hex(rgb) { | ||
return ( | ||
'#' + | ||
('00000' + ((rgb[0] << 16) | (rgb[1] << 8) | rgb[2]).toString(16)) | ||
.slice(-6) | ||
.toUpperCase() | ||
); | ||
} |
@@ -14,2 +14,4 @@ 'use strict'; | ||
* @author Taylor Hunt | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -21,4 +23,4 @@ exports.fn = () => { | ||
if (node.name === 'ellipse') { | ||
const rx = node.attributes.rx || 0; | ||
const ry = node.attributes.ry || 0; | ||
const rx = node.attributes.rx || '0'; | ||
const ry = node.attributes.ry || '0'; | ||
if ( | ||
@@ -25,0 +27,0 @@ rx === ry || |
@@ -128,3 +128,3 @@ 'use strict'; | ||
const pathItem = pathData[i]; | ||
let { instruction: command, data: args } = pathItem; | ||
let { command, args } = pathItem; | ||
@@ -275,4 +275,4 @@ // moveto (x y) | ||
pathItem.instruction = command; | ||
pathItem.data = args; | ||
pathItem.command = command; | ||
pathItem.args = args; | ||
// store absolute coordinates for later use | ||
@@ -302,15 +302,15 @@ // base should preserve reference from other element | ||
path = path.filter(function (item, index, path) { | ||
var instruction = item.instruction, | ||
data = item.data, | ||
next = path[index + 1]; | ||
let command = item.command; | ||
let data = item.args; | ||
let next = path[index + 1]; | ||
if (data) { | ||
if (command !== 'Z' && command !== 'z') { | ||
var sdata = data, | ||
circle; | ||
if (instruction === 's') { | ||
if (command === 's') { | ||
sdata = [0, 0].concat(data); | ||
if ('cs'.indexOf(prev.instruction) > -1) { | ||
var pdata = prev.data, | ||
if (command === 'c' || command === 's') { | ||
var pdata = prev.args, | ||
n = pdata.length; | ||
@@ -327,3 +327,3 @@ | ||
params.makeArcs && | ||
(instruction == 'c' || instruction == 's') && | ||
(command == 'c' || command == 's') && | ||
isConvex(sdata) && | ||
@@ -336,4 +336,4 @@ (circle = findCircle(sdata)) | ||
arc = { | ||
instruction: 'a', | ||
data: [r, r, 0, 0, sweep, sdata[4], sdata[5]], | ||
command: 'a', | ||
args: [r, r, 0, 0, sweep, sdata[4], sdata[5]], | ||
coords: item.coords.slice(), | ||
@@ -355,14 +355,12 @@ base: item.base, | ||
if ( | ||
(prev.instruction == 'c' && | ||
isConvex(prev.data) && | ||
isArcPrev(prev.data, circle)) || | ||
(prev.instruction == 'a' && | ||
prev.sdata && | ||
isArcPrev(prev.sdata, circle)) | ||
(prev.command == 'c' && | ||
isConvex(prev.args) && | ||
isArcPrev(prev.args, circle)) || | ||
(prev.command == 'a' && prev.sdata && isArcPrev(prev.sdata, circle)) | ||
) { | ||
arcCurves.unshift(prev); | ||
arc.base = prev.base; | ||
arc.data[5] = arc.coords[0] - arc.base[0]; | ||
arc.data[6] = arc.coords[1] - arc.base[1]; | ||
var prevData = prev.instruction == 'a' ? prev.sdata : prev.data; | ||
arc.args[5] = arc.coords[0] - arc.base[0]; | ||
arc.args[6] = arc.coords[1] - arc.base[1]; | ||
var prevData = prev.command == 'a' ? prev.sdata : prev.args; | ||
var prevAngle = findArcAngle(prevData, { | ||
@@ -376,3 +374,3 @@ center: [ | ||
angle += prevAngle; | ||
if (angle > Math.PI) arc.data[3] = 1; | ||
if (angle > Math.PI) arc.args[3] = 1; | ||
hasPrev = 1; | ||
@@ -384,13 +382,13 @@ } | ||
var j = index; | ||
(next = path[++j]) && ~'cs'.indexOf(next.instruction); | ||
(next = path[++j]) && ~'cs'.indexOf(next.command); | ||
) { | ||
var nextData = next.data; | ||
if (next.instruction == 's') { | ||
var nextData = next.args; | ||
if (next.command == 's') { | ||
nextLonghand = makeLonghand( | ||
{ instruction: 's', data: next.data.slice() }, | ||
path[j - 1].data | ||
{ command: 's', args: next.args.slice() }, | ||
path[j - 1].args | ||
); | ||
nextData = nextLonghand.data; | ||
nextLonghand.data = nextData.slice(0, 2); | ||
nextData = nextLonghand.args; | ||
nextLonghand.args = nextData.slice(0, 2); | ||
suffix = stringify([nextLonghand]); | ||
@@ -401,3 +399,3 @@ } | ||
if (angle - 2 * Math.PI > 1e-3) break; // more than 360° | ||
if (angle > Math.PI) arc.data[3] = 1; | ||
if (angle > Math.PI) arc.args[3] = 1; | ||
arcCurves.push(next); | ||
@@ -407,15 +405,15 @@ if (2 * Math.PI - angle > 1e-3) { | ||
arc.coords = next.coords; | ||
arc.data[5] = arc.coords[0] - arc.base[0]; | ||
arc.data[6] = arc.coords[1] - arc.base[1]; | ||
arc.args[5] = arc.coords[0] - arc.base[0]; | ||
arc.args[6] = arc.coords[1] - arc.base[1]; | ||
} else { | ||
// full circle, make a half-circle arc and add a second one | ||
arc.data[5] = 2 * (relCircle.center[0] - nextData[4]); | ||
arc.data[6] = 2 * (relCircle.center[1] - nextData[5]); | ||
arc.args[5] = 2 * (relCircle.center[0] - nextData[4]); | ||
arc.args[6] = 2 * (relCircle.center[1] - nextData[5]); | ||
arc.coords = [ | ||
arc.base[0] + arc.data[5], | ||
arc.base[1] + arc.data[6], | ||
arc.base[0] + arc.args[5], | ||
arc.base[1] + arc.args[6], | ||
]; | ||
arc = { | ||
instruction: 'a', | ||
data: [ | ||
command: 'a', | ||
args: [ | ||
r, | ||
@@ -442,12 +440,12 @@ r, | ||
if ((stringify(output) + suffix).length < stringify(arcCurves).length) { | ||
if (path[j] && path[j].instruction == 's') { | ||
makeLonghand(path[j], path[j - 1].data); | ||
if (path[j] && path[j].command == 's') { | ||
makeLonghand(path[j], path[j - 1].args); | ||
} | ||
if (hasPrev) { | ||
var prevArc = output.shift(); | ||
roundData(prevArc.data); | ||
relSubpoint[0] += prevArc.data[5] - prev.data[prev.data.length - 2]; | ||
relSubpoint[1] += prevArc.data[6] - prev.data[prev.data.length - 1]; | ||
prev.instruction = 'a'; | ||
prev.data = prevArc.data; | ||
roundData(prevArc.args); | ||
relSubpoint[0] += prevArc.args[5] - prev.args[prev.args.length - 2]; | ||
relSubpoint[1] += prevArc.args[6] - prev.args[prev.args.length - 1]; | ||
prev.command = 'a'; | ||
prev.args = prevArc.args; | ||
item.base = prev.coords = prevArc.coords; | ||
@@ -466,4 +464,4 @@ } | ||
if (!arc) return false; | ||
instruction = 'a'; | ||
data = arc.data; | ||
command = 'a'; | ||
data = arc.args; | ||
item.coords = arc.coords; | ||
@@ -477,11 +475,18 @@ } | ||
if (precision !== false) { | ||
if ('mltqsc'.indexOf(instruction) > -1) { | ||
if ( | ||
command === 'm' || | ||
command === 'l' || | ||
command === 't' || | ||
command === 'q' || | ||
command === 's' || | ||
command === 'c' | ||
) { | ||
for (var i = data.length; i--; ) { | ||
data[i] += item.base[i % 2] - relSubpoint[i % 2]; | ||
} | ||
} else if (instruction == 'h') { | ||
} else if (command == 'h') { | ||
data[0] += item.base[0] - relSubpoint[0]; | ||
} else if (instruction == 'v') { | ||
} else if (command == 'v') { | ||
data[0] += item.base[1] - relSubpoint[1]; | ||
} else if (instruction == 'a') { | ||
} else if (command == 'a') { | ||
data[5] += item.base[0] - relSubpoint[0]; | ||
@@ -492,4 +497,4 @@ data[6] += item.base[1] - relSubpoint[1]; | ||
if (instruction == 'h') relSubpoint[0] += data[0]; | ||
else if (instruction == 'v') relSubpoint[1] += data[0]; | ||
if (command == 'h') relSubpoint[0] += data[0]; | ||
else if (command == 'v') relSubpoint[1] += data[0]; | ||
else { | ||
@@ -501,3 +506,3 @@ relSubpoint[0] += data[data.length - 2]; | ||
if (instruction.toLowerCase() == 'm') { | ||
if (command === 'M' || command === 'm') { | ||
pathBase[0] = relSubpoint[0]; | ||
@@ -511,21 +516,21 @@ pathBase[1] = relSubpoint[1]; | ||
if ( | ||
(instruction === 'c' && isCurveStraightLine(data)) || | ||
(instruction === 's' && isCurveStraightLine(sdata)) | ||
(command === 'c' && isCurveStraightLine(data)) || | ||
(command === 's' && isCurveStraightLine(sdata)) | ||
) { | ||
if (next && next.instruction == 's') makeLonghand(next, data); // fix up next curve | ||
instruction = 'l'; | ||
if (next && next.command == 's') makeLonghand(next, data); // fix up next curve | ||
command = 'l'; | ||
data = data.slice(-2); | ||
} else if (instruction === 'q' && isCurveStraightLine(data)) { | ||
if (next && next.instruction == 't') makeLonghand(next, data); // fix up next curve | ||
instruction = 'l'; | ||
} else if (command === 'q' && isCurveStraightLine(data)) { | ||
if (next && next.command == 't') makeLonghand(next, data); // fix up next curve | ||
command = 'l'; | ||
data = data.slice(-2); | ||
} else if ( | ||
instruction === 't' && | ||
prev.instruction !== 'q' && | ||
prev.instruction !== 't' | ||
command === 't' && | ||
prev.command !== 'q' && | ||
prev.command !== 't' | ||
) { | ||
instruction = 'l'; | ||
command = 'l'; | ||
data = data.slice(-2); | ||
} else if (instruction === 'a' && (data[0] === 0 || data[1] === 0)) { | ||
instruction = 'l'; | ||
} else if (command === 'a' && (data[0] === 0 || data[1] === 0)) { | ||
command = 'l'; | ||
data = data.slice(-2); | ||
@@ -538,8 +543,8 @@ } | ||
// l 0 50 → v 50 | ||
if (params.lineShorthands && instruction === 'l') { | ||
if (params.lineShorthands && command === 'l') { | ||
if (data[1] === 0) { | ||
instruction = 'h'; | ||
command = 'h'; | ||
data.pop(); | ||
} else if (data[0] === 0) { | ||
instruction = 'v'; | ||
command = 'v'; | ||
data.shift(); | ||
@@ -554,11 +559,11 @@ } | ||
hasMarkerMid === false && | ||
'mhv'.indexOf(instruction) > -1 && | ||
prev.instruction && | ||
instruction == prev.instruction.toLowerCase() && | ||
((instruction != 'h' && instruction != 'v') || | ||
prev.data[0] >= 0 == data[0] >= 0) | ||
(command === 'm' || command === 'h' || command === 'v') && | ||
prev.command && | ||
command == prev.command.toLowerCase() && | ||
((command != 'h' && command != 'v') || | ||
prev.args[0] >= 0 == data[0] >= 0) | ||
) { | ||
prev.data[0] += data[0]; | ||
if (instruction != 'h' && instruction != 'v') { | ||
prev.data[1] += data[1]; | ||
prev.args[0] += data[0]; | ||
if (command != 'h' && command != 'v') { | ||
prev.args[1] += data[1]; | ||
} | ||
@@ -571,12 +576,12 @@ prev.coords = item.coords; | ||
// convert curves into smooth shorthands | ||
if (params.curveSmoothShorthands && prev.instruction) { | ||
if (params.curveSmoothShorthands && prev.command) { | ||
// curveto | ||
if (instruction === 'c') { | ||
if (command === 'c') { | ||
// c + c → c + s | ||
if ( | ||
prev.instruction === 'c' && | ||
data[0] === -(prev.data[2] - prev.data[4]) && | ||
data[1] === -(prev.data[3] - prev.data[5]) | ||
prev.command === 'c' && | ||
data[0] === -(prev.args[2] - prev.args[4]) && | ||
data[1] === -(prev.args[3] - prev.args[5]) | ||
) { | ||
instruction = 's'; | ||
command = 's'; | ||
data = data.slice(2); | ||
@@ -587,7 +592,7 @@ } | ||
else if ( | ||
prev.instruction === 's' && | ||
data[0] === -(prev.data[0] - prev.data[2]) && | ||
data[1] === -(prev.data[1] - prev.data[3]) | ||
prev.command === 's' && | ||
data[0] === -(prev.args[0] - prev.args[2]) && | ||
data[1] === -(prev.args[1] - prev.args[3]) | ||
) { | ||
instruction = 's'; | ||
command = 's'; | ||
data = data.slice(2); | ||
@@ -598,7 +603,8 @@ } | ||
else if ( | ||
'cs'.indexOf(prev.instruction) === -1 && | ||
prev.command !== 'c' && | ||
prev.command !== 's' && | ||
data[0] === 0 && | ||
data[1] === 0 | ||
) { | ||
instruction = 's'; | ||
command = 's'; | ||
data = data.slice(2); | ||
@@ -609,10 +615,10 @@ } | ||
// quadratic Bézier curveto | ||
else if (instruction === 'q') { | ||
else if (command === 'q') { | ||
// q + q → q + t | ||
if ( | ||
prev.instruction === 'q' && | ||
data[0] === prev.data[2] - prev.data[0] && | ||
data[1] === prev.data[3] - prev.data[1] | ||
prev.command === 'q' && | ||
data[0] === prev.args[2] - prev.args[0] && | ||
data[1] === prev.args[3] - prev.args[1] | ||
) { | ||
instruction = 't'; | ||
command = 't'; | ||
data = data.slice(2); | ||
@@ -623,7 +629,7 @@ } | ||
else if ( | ||
prev.instruction === 't' && | ||
data[2] === prev.data[0] && | ||
data[3] === prev.data[1] | ||
prev.command === 't' && | ||
data[2] === prev.args[0] && | ||
data[3] === prev.args[1] | ||
) { | ||
instruction = 't'; | ||
command = 't'; | ||
data = data.slice(2); | ||
@@ -638,3 +644,9 @@ } | ||
if ( | ||
'lhvqtcs'.indexOf(instruction) > -1 && | ||
(command === 'l' || | ||
command === 'h' || | ||
command === 'v' || | ||
command === 'q' || | ||
command === 't' || | ||
command === 'c' || | ||
command === 's') && | ||
data.every(function (i) { | ||
@@ -649,3 +661,3 @@ return i === 0; | ||
// a 25,25 -30 0,1 0,0 | ||
if (instruction === 'a' && data[5] === 0 && data[6] === 0) { | ||
if (command === 'a' && data[5] === 0 && data[6] === 0) { | ||
path[index] = prev; | ||
@@ -656,4 +668,4 @@ return false; | ||
item.instruction = instruction; | ||
item.data = data; | ||
item.command = command; | ||
item.args = data; | ||
@@ -665,3 +677,3 @@ prev = item; | ||
relSubpoint[1] = pathBase[1]; | ||
if (prev.instruction == 'z') return false; | ||
if (prev.command === 'Z' || prev.command === 'z') return false; | ||
prev = item; | ||
@@ -687,3 +699,3 @@ } | ||
if (index == 0) return true; | ||
if (!item.data) { | ||
if (item.command === 'Z' || item.command === 'z') { | ||
prev = item; | ||
@@ -693,15 +705,22 @@ return true; | ||
var instruction = item.instruction, | ||
data = item.data, | ||
adata = data && data.slice(0); | ||
var command = item.command, | ||
data = item.args, | ||
adata = data.slice(); | ||
if ('mltqsc'.indexOf(instruction) > -1) { | ||
if ( | ||
command === 'm' || | ||
command === 'l' || | ||
command === 't' || | ||
command === 'q' || | ||
command === 's' || | ||
command === 'c' | ||
) { | ||
for (var i = adata.length; i--; ) { | ||
adata[i] += item.base[i % 2]; | ||
} | ||
} else if (instruction == 'h') { | ||
} else if (command == 'h') { | ||
adata[0] += item.base[0]; | ||
} else if (instruction == 'v') { | ||
} else if (command == 'v') { | ||
adata[0] += item.base[1]; | ||
} else if (instruction == 'a') { | ||
} else if (command == 'a') { | ||
adata[5] += item.base[0]; | ||
@@ -718,3 +737,3 @@ adata[6] += item.base[1]; | ||
// v-20 -> V0 | ||
// Don't convert if it fits following previous instruction. | ||
// Don't convert if it fits following previous command. | ||
// l20 30-10-50 instead of l20 30L20 30 | ||
@@ -726,11 +745,11 @@ if ( | ||
params.negativeExtraSpace && | ||
instruction == prev.instruction && | ||
prev.instruction.charCodeAt(0) > 96 && | ||
command == prev.command && | ||
prev.command.charCodeAt(0) > 96 && | ||
absoluteDataStr.length == relativeDataStr.length - 1 && | ||
(data[0] < 0 || | ||
(/^0\./.test(data[0]) && prev.data[prev.data.length - 1] % 1)) | ||
(/^0\./.test(data[0]) && prev.args[prev.args.length - 1] % 1)) | ||
)) | ||
) { | ||
item.instruction = instruction.toUpperCase(); | ||
item.data = adata; | ||
item.command = command.toUpperCase(); | ||
item.args = adata; | ||
} | ||
@@ -874,11 +893,11 @@ | ||
function makeLonghand(item, data) { | ||
switch (item.instruction) { | ||
switch (item.command) { | ||
case 's': | ||
item.instruction = 'c'; | ||
item.command = 'c'; | ||
break; | ||
case 't': | ||
item.instruction = 'q'; | ||
item.command = 'q'; | ||
break; | ||
} | ||
item.data.unshift( | ||
item.args.unshift( | ||
data[data.length - 2] - data[data.length - 4], | ||
@@ -1031,7 +1050,7 @@ data[data.length - 1] - data[data.length - 3] | ||
var strData = ''; | ||
if (item.data) { | ||
strData = cleanupOutData(roundData(item.data.slice()), params); | ||
if (item.args) { | ||
strData = cleanupOutData(roundData(item.args.slice()), params); | ||
} | ||
return pathString + item.instruction + strData; | ||
return pathString + item.command + strData; | ||
}, ''); | ||
} |
'use strict'; | ||
/** | ||
* @typedef {import('../lib/types').PathDataItem} PathDataItem | ||
*/ | ||
const { stringifyPathData } = require('../lib/path.js'); | ||
@@ -21,5 +25,10 @@ const { detachNodeFromParent } = require('../lib/xast.js'); | ||
* @author Lev Solntsev | ||
* | ||
* @type {import('../lib/types').Plugin<{ | ||
* convertArcs?: boolean, | ||
* floatPrecision?: number | ||
* }>} | ||
*/ | ||
exports.fn = (root, params) => { | ||
const { convertArcs = false, floatPrecision: precision = null } = params; | ||
const { convertArcs = false, floatPrecision: precision } = params; | ||
@@ -45,2 +54,5 @@ return { | ||
if (Number.isNaN(x - y + width - height)) return; | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const pathData = [ | ||
@@ -68,2 +80,5 @@ { command: 'M', args: [x, y] }, | ||
if (Number.isNaN(x1 - y1 + x2 - y2)) return; | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const pathData = [ | ||
@@ -93,2 +108,5 @@ { command: 'M', args: [x1, y1] }, | ||
} | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const pathData = []; | ||
@@ -117,2 +135,5 @@ for (let i = 0; i < coords.length; i += 2) { | ||
} | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const pathData = [ | ||
@@ -140,2 +161,5 @@ { command: 'M', args: [cx, cy - r] }, | ||
} | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const pathData = [ | ||
@@ -142,0 +166,0 @@ { command: 'M', args: [ecx, ecy - ry] }, |
'use strict'; | ||
exports.name = 'convertTransform'; | ||
/** | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
*/ | ||
exports.type = 'perItem'; | ||
const { cleanupOutData } = require('../lib/svgo/tools.js'); | ||
const { | ||
transform2js, | ||
transformsMultiply, | ||
matrixToTransform, | ||
} = require('./_transforms.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'convertTransform'; | ||
exports.active = true; | ||
exports.description = 'collapses multiple transformations and optimizes it'; | ||
exports.params = { | ||
convertToShorts: true, | ||
// degPrecision: 3, // transformPrecision (or matrix precision) - 2 by default | ||
floatPrecision: 3, | ||
transformPrecision: 5, | ||
matrixToTransform: true, | ||
shortTranslate: true, | ||
shortScale: true, | ||
shortRotate: true, | ||
removeUseless: true, | ||
collapseIntoOne: true, | ||
leadingZero: true, | ||
negativeExtraSpace: false, | ||
}; | ||
var cleanupOutData = require('../lib/svgo/tools').cleanupOutData, | ||
transform2js = require('./_transforms.js').transform2js, | ||
transformsMultiply = require('./_transforms.js').transformsMultiply, | ||
matrixToTransform = require('./_transforms.js').matrixToTransform, | ||
degRound, | ||
floatRound, | ||
transformRound; | ||
/** | ||
@@ -42,26 +27,89 @@ * Convert matrices to the short aliases, | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @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, | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
if (item.type === 'element') { | ||
// transform | ||
if (item.attributes.transform != null) { | ||
convertTransform(item, 'transform', params); | ||
} | ||
exports.fn = (_root, params) => { | ||
const { | ||
convertToShorts = true, | ||
// degPrecision = 3, // transformPrecision (or matrix precision) - 2 by default | ||
degPrecision, | ||
floatPrecision = 3, | ||
transformPrecision = 5, | ||
matrixToTransform = true, | ||
shortTranslate = true, | ||
shortScale = true, | ||
shortRotate = true, | ||
removeUseless = true, | ||
collapseIntoOne = true, | ||
leadingZero = true, | ||
negativeExtraSpace = false, | ||
} = params; | ||
const newParams = { | ||
convertToShorts, | ||
degPrecision, | ||
floatPrecision, | ||
transformPrecision, | ||
matrixToTransform, | ||
shortTranslate, | ||
shortScale, | ||
shortRotate, | ||
removeUseless, | ||
collapseIntoOne, | ||
leadingZero, | ||
negativeExtraSpace, | ||
}; | ||
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); | ||
} | ||
}, | ||
}, | ||
}; | ||
}; | ||
// gradientTransform | ||
if (item.attributes.gradientTransform != null) { | ||
convertTransform(item, 'gradientTransform', 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 | ||
*/ | ||
// patternTransform | ||
if (item.attributes.patternTransform != null) { | ||
convertTransform(item, 'patternTransform', params); | ||
} | ||
} | ||
}; | ||
/** | ||
* @typedef {{ name: string, data: Array<number> }} TransformItem | ||
*/ | ||
@@ -71,7 +119,5 @@ /** | ||
* | ||
* @param {Object} item input item | ||
* @param {String} attrName attribute name | ||
* @param {Object} params plugin params | ||
* @type {(item: XastElement, attrName: string, params: TransformParams) => void} | ||
*/ | ||
function convertTransform(item, attrName, params) { | ||
const convertTransform = (item, attrName, params) => { | ||
let data = transform2js(item.attributes[attrName]); | ||
@@ -87,3 +133,3 @@ params = definePrecision(data, params); | ||
} else { | ||
data.forEach(roundTransform); | ||
data.forEach((item) => roundTransform(item, params)); | ||
} | ||
@@ -100,3 +146,3 @@ | ||
} | ||
} | ||
}; | ||
@@ -110,69 +156,83 @@ /** | ||
* | ||
* @param {Array} transforms input array | ||
* @param {Object} params plugin params | ||
* @return {Array} output array | ||
* @type {(data: Array<TransformItem>, params: TransformParams) => TransformParams} | ||
* | ||
* clone params so it don't affect other elements transformations. | ||
*/ | ||
function definePrecision(data, params) { | ||
var matrixData = data.reduce(getMatrixData, []), | ||
significantDigits = params.transformPrecision; | ||
// Clone params so it don't affect other elements transformations. | ||
params = Object.assign({}, params); | ||
const definePrecision = (data, { ...newParams }) => { | ||
const matrixData = []; | ||
for (const item of data) { | ||
if (item.name == 'matrix') { | ||
matrixData.push(...item.data.slice(0, 4)); | ||
} | ||
} | ||
let significantDigits = newParams.transformPrecision; | ||
// Limit transform precision with matrix one. Calculating with larger precision doesn't add any value. | ||
if (matrixData.length) { | ||
params.transformPrecision = Math.min( | ||
params.transformPrecision, | ||
newParams.transformPrecision = Math.min( | ||
newParams.transformPrecision, | ||
Math.max.apply(Math, matrixData.map(floatDigits)) || | ||
params.transformPrecision | ||
newParams.transformPrecision | ||
); | ||
significantDigits = Math.max.apply( | ||
Math, | ||
matrixData.map(function (n) { | ||
return String(n).replace(/\D+/g, '').length; // Number of digits in a number. 123.45 → 5 | ||
}) | ||
matrixData.map( | ||
(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. | ||
if (!('degPrecision' in params)) { | ||
params.degPrecision = Math.max( | ||
if (newParams.degPrecision == null) { | ||
newParams.degPrecision = Math.max( | ||
0, | ||
Math.min(params.floatPrecision, significantDigits - 2) | ||
Math.min(newParams.floatPrecision, significantDigits - 2) | ||
); | ||
} | ||
return newParams; | ||
}; | ||
floatRound = | ||
params.floatPrecision >= 1 && params.floatPrecision < 20 | ||
? smartRound.bind(this, params.floatPrecision) | ||
: round; | ||
degRound = | ||
params.degPrecision >= 1 && params.floatPrecision < 20 | ||
? smartRound.bind(this, params.degPrecision) | ||
: round; | ||
transformRound = | ||
params.transformPrecision >= 1 && params.floatPrecision < 20 | ||
? smartRound.bind(this, params.transformPrecision) | ||
: round; | ||
/** | ||
* @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); | ||
} | ||
}; | ||
return params; | ||
} | ||
/** | ||
* Gathers four first matrix parameters. | ||
* | ||
* @param {Array} a array of data | ||
* @param {Object} transform | ||
* @return {Array} output array | ||
* @type {(data: Array<number>, params: TransformParams) => Array<number>} | ||
*/ | ||
function getMatrixData(a, b) { | ||
return b.name == 'matrix' ? a.concat(b.data.slice(0, 4)) : a; | ||
} | ||
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 | ||
* | ||
* @type {(n: number) => number} | ||
*/ | ||
function floatDigits(n) { | ||
return (n = String(n)).slice(n.indexOf('.')).length - 1; | ||
} | ||
const floatDigits = (n) => { | ||
const str = n.toString(); | ||
return str.slice(str.indexOf('.')).length - 1; | ||
}; | ||
@@ -182,7 +242,5 @@ /** | ||
* | ||
* @param {Array} transforms input array | ||
* @param {Object} params plugin params | ||
* @return {Array} output array | ||
* @type {(transforms: Array<TransformItem>, params: TransformParams) => Array<TransformItem>} | ||
*/ | ||
function convertToShorts(transforms, params) { | ||
const convertToShorts = (transforms, params) => { | ||
for (var i = 0; i < transforms.length; i++) { | ||
@@ -195,7 +253,6 @@ var transform = transforms[i]; | ||
if ( | ||
decomposed != transform && | ||
js2transform(decomposed, params).length <= | ||
js2transform([transform], params).length | ||
js2transform([transform], params).length | ||
) { | ||
transforms.splice.apply(transforms, [i, 1].concat(decomposed)); | ||
transforms.splice(i, 1, ...decomposed); | ||
} | ||
@@ -207,3 +264,3 @@ transform = transforms[i]; | ||
// 12.754997 → 12.755 | ||
roundTransform(transform); | ||
roundTransform(transform, params); | ||
@@ -258,3 +315,3 @@ // convert long translate transform notation to the shorts one | ||
return transforms; | ||
} | ||
}; | ||
@@ -264,7 +321,6 @@ /** | ||
* | ||
* @param {Array} transforms input array | ||
* @return {Array} output array | ||
* @type {(trasforms: Array<TransformItem>) => Array<TransformItem>} | ||
*/ | ||
function removeUseless(transforms) { | ||
return transforms.filter(function (transform) { | ||
const removeUseless = (transforms) => { | ||
return transforms.filter((transform) => { | ||
// translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0) | ||
@@ -299,3 +355,3 @@ if ( | ||
}); | ||
} | ||
}; | ||
@@ -305,12 +361,10 @@ /** | ||
* | ||
* @param {Array} transformJS JS representation array | ||
* @param {Object} params plugin params | ||
* @return {String} output string | ||
* @type {(transformJS: Array<TransformItem>, params: TransformParams) => string} | ||
*/ | ||
function js2transform(transformJS, params) { | ||
const js2transform = (transformJS, params) => { | ||
var transformString = ''; | ||
// collect output value string | ||
transformJS.forEach(function (transform) { | ||
roundTransform(transform); | ||
transformJS.forEach((transform) => { | ||
roundTransform(transform, params); | ||
transformString += | ||
@@ -325,29 +379,34 @@ (transformString && ' ') + | ||
return transformString; | ||
} | ||
}; | ||
function roundTransform(transform) { | ||
/** | ||
* @type {(transform: TransformItem, params: TransformParams) => TransformItem} | ||
*/ | ||
const roundTransform = (transform, params) => { | ||
switch (transform.name) { | ||
case 'translate': | ||
transform.data = floatRound(transform.data); | ||
transform.data = floatRound(transform.data, params); | ||
break; | ||
case 'rotate': | ||
transform.data = degRound(transform.data.slice(0, 1)).concat( | ||
floatRound(transform.data.slice(1)) | ||
); | ||
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); | ||
transform.data = degRound(transform.data, params); | ||
break; | ||
case 'scale': | ||
transform.data = transformRound(transform.data); | ||
transform.data = transformRound(transform.data, params); | ||
break; | ||
case 'matrix': | ||
transform.data = transformRound(transform.data.slice(0, 4)).concat( | ||
floatRound(transform.data.slice(4)) | ||
); | ||
transform.data = [ | ||
...transformRound(transform.data.slice(0, 4), params), | ||
...floatRound(transform.data.slice(4), params), | ||
]; | ||
break; | ||
} | ||
return transform; | ||
} | ||
}; | ||
@@ -357,8 +416,7 @@ /** | ||
* | ||
* @param {Array} data input data array | ||
* @return {Array} output data array | ||
* @type {(data: Array<number>) => Array<number>} | ||
*/ | ||
function round(data) { | ||
const round = (data) => { | ||
return data.map(Math.round); | ||
} | ||
}; | ||
@@ -370,7 +428,5 @@ /** | ||
* | ||
* @param {Number} fixed number of decimals | ||
* @param {Array} data input data array | ||
* @return {Array} output data array | ||
* @type {(precision: number, data: Array<number>) => Array<number>} | ||
*/ | ||
function smartRound(precision, data) { | ||
const smartRound = (precision, data) => { | ||
for ( | ||
@@ -382,3 +438,3 @@ var i = data.length, | ||
) { | ||
if (data[i].toFixed(precision) != data[i]) { | ||
if (Number(data[i].toFixed(precision)) !== data[i]) { | ||
var rounded = +data[i].toFixed(precision - 1); | ||
@@ -392,2 +448,2 @@ data[i] = | ||
return data; | ||
} | ||
}; |
@@ -151,7 +151,20 @@ 'use strict'; | ||
} | ||
const styleDeclarationList = csstree.parse( | ||
selectedEl.attributes.style == null ? '' : selectedEl.attributes.style, | ||
{ | ||
context: 'declarationList', | ||
parseValue: false, | ||
} | ||
); | ||
const styleDeclarationItems = new Map(); | ||
csstree.walk(styleDeclarationList, { | ||
visit: 'Declaration', | ||
enter(node, item) { | ||
styleDeclarationItems.set(node.property, item); | ||
}, | ||
}); | ||
// merge declarations | ||
csstree.walk(selector.rule, { | ||
visit: 'Declaration', | ||
enter: function (styleCsstreeDeclaration) { | ||
enter(ruleDeclaration) { | ||
// existing inline styles have higher priority | ||
@@ -161,19 +174,25 @@ // no inline styles, external styles, external styles used | ||
// inline styles, external styles higher priority than inline styles, external styles used | ||
var styleDeclaration = cssTools.csstreeToStyleDeclaration( | ||
styleCsstreeDeclaration | ||
const matchedItem = styleDeclarationItems.get( | ||
ruleDeclaration.property | ||
); | ||
if ( | ||
selectedEl.style.getPropertyValue(styleDeclaration.name) !== null && | ||
selectedEl.style.getPropertyPriority(styleDeclaration.name) >= | ||
styleDeclaration.priority | ||
const ruleDeclarationItem = | ||
styleDeclarationList.children.createItem(ruleDeclaration); | ||
if (matchedItem == null) { | ||
styleDeclarationList.children.append(ruleDeclarationItem); | ||
} else if ( | ||
matchedItem.data.important !== true && | ||
ruleDeclaration.important === true | ||
) { | ||
return; | ||
styleDeclarationList.children.replace( | ||
matchedItem, | ||
ruleDeclarationItem | ||
); | ||
styleDeclarationItems.set( | ||
ruleDeclaration.property, | ||
ruleDeclarationItem | ||
); | ||
} | ||
selectedEl.style.setProperty( | ||
styleDeclaration.name, | ||
styleDeclaration.value, | ||
styleDeclaration.priority | ||
); | ||
}, | ||
}); | ||
selectedEl.attributes.style = csstree.generate(styleDeclarationList); | ||
} | ||
@@ -212,9 +231,15 @@ | ||
// class | ||
var firstSubSelector = selector.item.data.children.first(); | ||
const classList = new Set( | ||
selectedEl.attributes.class == null | ||
? null | ||
: selectedEl.attributes.class.split(' ') | ||
); | ||
const firstSubSelector = selector.item.data.children.first(); | ||
if (firstSubSelector.type === 'ClassSelector') { | ||
selectedEl.class.remove(firstSubSelector.name); | ||
classList.delete(firstSubSelector.name); | ||
} | ||
// clean up now empty class attributes | ||
if (typeof selectedEl.class.item(0) === 'undefined') { | ||
if (classList.size === 0) { | ||
delete selectedEl.attributes.class; | ||
} else { | ||
selectedEl.attributes.class = Array.from(classList).join(' '); | ||
} | ||
@@ -221,0 +246,0 @@ |
@@ -7,4 +7,4 @@ 'use strict'; | ||
exports.type = 'visitor'; | ||
exports.name = 'mergePaths'; | ||
exports.type = 'visitor'; | ||
exports.active = true; | ||
@@ -16,6 +16,9 @@ exports.description = 'merges multiple paths in one if possible'; | ||
* | ||
* @param {Object} root | ||
* @param {Object} params | ||
* @author Kir Belevich, Lev Solntsev | ||
* | ||
* @author Kir Belevich, Lev Solntsev | ||
* @type {import('../lib/types').Plugin<{ | ||
* force?: boolean, | ||
* floatPrecision?: number, | ||
* noSpaceAfterFlags?: boolean | ||
* }>} | ||
*/ | ||
@@ -22,0 +25,0 @@ exports.fn = (root, params) => { |
@@ -67,2 +67,4 @@ 'use strict'; | ||
} | ||
return false; | ||
}), | ||
@@ -69,0 +71,0 @@ allPath = item.children.every(function (inner) { |
'use strict'; | ||
const { querySelectorAll } = require('../lib/xast.js'); | ||
exports.name = 'removeAttributesBySelector'; | ||
exports.type = 'perItem'; | ||
exports.type = 'visitor'; | ||
exports.active = false; | ||
exports.description = | ||
@@ -15,12 +14,13 @@ 'removes attributes of elements that match a css selector'; | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* | ||
* @example | ||
* <caption>A selector removing a single attribute</caption> | ||
* plugins: | ||
* - removeAttributesBySelector: | ||
* plugins: [ | ||
* { | ||
* name: "removeAttributesBySelector", | ||
* params: { | ||
* selector: "[fill='#00ff00']" | ||
* attributes: "fill" | ||
* } | ||
* } | ||
* ] | ||
* | ||
@@ -32,8 +32,14 @@ * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> | ||
* <caption>A selector removing multiple attributes</caption> | ||
* plugins: | ||
* - removeAttributesBySelector: | ||
* selector: "[fill='#00ff00']" | ||
* attributes: | ||
* - fill | ||
* - stroke | ||
* plugins: [ | ||
* { | ||
* name: "removeAttributesBySelector", | ||
* params: { | ||
* selector: "[fill='#00ff00']", | ||
* attributes: [ | ||
* "fill", | ||
* "stroke" | ||
* ] | ||
* } | ||
* } | ||
* ] | ||
* | ||
@@ -45,13 +51,23 @@ * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> | ||
* <caption>Multiple selectors removing attributes</caption> | ||
* plugins: | ||
* - removeAttributesBySelector: | ||
* selectors: | ||
* - selector: "[fill='#00ff00']" | ||
* plugins: [ | ||
* { | ||
* name: "removeAttributesBySelector", | ||
* params: { | ||
* selectors: [ | ||
* { | ||
* selector: "[fill='#00ff00']", | ||
* attributes: "fill" | ||
* }, | ||
* { | ||
* selector: "#remove", | ||
* attributes: [ | ||
* "stroke", | ||
* "id" | ||
* ] | ||
* } | ||
* ] | ||
* } | ||
* } | ||
* ] | ||
* | ||
* - selector: "#remove" | ||
* attributes: | ||
* - stroke | ||
* - id | ||
* | ||
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> | ||
@@ -61,20 +77,27 @@ * ↓ | ||
* | ||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors} | ||
* @link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors | ||
* | ||
* @author Bradley Mease | ||
* | ||
* @type {import('../lib/types').Plugin<any>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
var selectors = Array.isArray(params.selectors) ? params.selectors : [params]; | ||
selectors.map(({ selector, attributes }) => { | ||
if (item.matches(selector)) { | ||
if (Array.isArray(attributes)) { | ||
for (const name of attributes) { | ||
delete item.attributes[name]; | ||
exports.fn = (root, params) => { | ||
const selectors = Array.isArray(params.selectors) | ||
? params.selectors | ||
: [params]; | ||
for (const { selector, attributes } of selectors) { | ||
const nodes = querySelectorAll(root, selector); | ||
for (const node of nodes) { | ||
if (node.type === 'element') { | ||
if (Array.isArray(attributes)) { | ||
for (const name of attributes) { | ||
delete node.attributes[name]; | ||
} | ||
} else { | ||
delete node.attributes[attributes]; | ||
} | ||
} else { | ||
delete item.attributes[attributes]; | ||
} | ||
} | ||
}); | ||
} | ||
return {}; | ||
}; |
'use strict'; | ||
var DEFAULT_SEPARATOR = ':'; | ||
exports.name = 'removeAttrs'; | ||
exports.type = 'perItem'; | ||
exports.type = 'visitor'; | ||
exports.active = false; | ||
exports.description = 'removes specified attributes'; | ||
exports.params = { | ||
elemSeparator: DEFAULT_SEPARATOR, | ||
preserveCurrentColor: false, | ||
attrs: [], | ||
}; | ||
const DEFAULT_SEPARATOR = ':'; | ||
@@ -22,9 +13,9 @@ /** | ||
* | ||
* @param elemSeparator | ||
* @example elemSeparator | ||
* format: string | ||
* | ||
* @param preserveCurrentColor | ||
* @example preserveCurrentColor | ||
* format: boolean | ||
* | ||
* @param attrs: | ||
* @example attrs: | ||
* | ||
@@ -81,63 +72,66 @@ * format: [ element* : attribute* : value* ] | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Benny Schudel | ||
* | ||
* @author Benny Schudel | ||
* @type {import('../lib/types').Plugin<{ | ||
* elemSeparator?: string, | ||
* preserveCurrentColor?: boolean, | ||
* attrs: string | Array<string> | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
exports.fn = (root, params) => { | ||
// wrap into an array if params is not | ||
if (!Array.isArray(params.attrs)) { | ||
params.attrs = [params.attrs]; | ||
} | ||
const elemSeparator = | ||
typeof params.elemSeparator == 'string' | ||
? params.elemSeparator | ||
: DEFAULT_SEPARATOR; | ||
const preserveCurrentColor = | ||
typeof params.preserveCurrentColor == 'boolean' | ||
? params.preserveCurrentColor | ||
: false; | ||
const attrs = Array.isArray(params.attrs) ? params.attrs : [params.attrs]; | ||
if (item.type === 'element') { | ||
var elemSeparator = | ||
typeof params.elemSeparator == 'string' | ||
? params.elemSeparator | ||
: DEFAULT_SEPARATOR; | ||
var preserveCurrentColor = | ||
typeof params.preserveCurrentColor == 'boolean' | ||
? params.preserveCurrentColor | ||
: false; | ||
return { | ||
element: { | ||
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 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(''); | ||
} | ||
// prepare patterns | ||
var patterns = params.attrs.map(function (pattern) { | ||
// if no element separators (:), assume it's attribute name, and apply to all elements *regardless of value* | ||
if (pattern.indexOf(elemSeparator) === -1) { | ||
pattern = ['.*', elemSeparator, pattern, elemSeparator, '.*'].join(''); | ||
// create regexps for element, attribute name, and attribute value | ||
const list = pattern.split(elemSeparator).map((value) => { | ||
// adjust single * to match anything | ||
if (value === '*') { | ||
value = '.*'; | ||
} | ||
return new RegExp(['^', value, '$'].join(''), 'i'); | ||
}); | ||
// 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(''); | ||
} | ||
// create regexps for element, attribute name, and attribute value | ||
return pattern.split(elemSeparator).map(function (value) { | ||
// adjust single * to match anything | ||
if (value === '*') { | ||
value = '.*'; | ||
} | ||
return new RegExp(['^', value, '$'].join(''), 'i'); | ||
}); | ||
}); | ||
// loop patterns | ||
patterns.forEach(function (pattern) { | ||
// matches element | ||
if (pattern[0].test(item.name)) { | ||
// loop attributes | ||
for (const [name, value] of Object.entries(item.attributes)) { | ||
var isFillCurrentColor = | ||
preserveCurrentColor && name == 'fill' && value == 'currentColor'; | ||
var isStrokeCurrentColor = | ||
preserveCurrentColor && name == 'stroke' && value == 'currentColor'; | ||
if (!(isFillCurrentColor || isStrokeCurrentColor)) { | ||
// matches attribute name | ||
if (pattern[1].test(name)) { | ||
// matches attribute value | ||
if (pattern[2].test(value)) { | ||
delete item.attributes[name]; | ||
// matches element | ||
if (list[0].test(node.name)) { | ||
// loop attributes | ||
for (const [name, value] of Object.entries(node.attributes)) { | ||
const isFillCurrentColor = | ||
preserveCurrentColor && | ||
name == 'fill' && | ||
value == 'currentColor'; | ||
const isStrokeCurrentColor = | ||
preserveCurrentColor && | ||
name == 'stroke' && | ||
value == 'currentColor'; | ||
if ( | ||
!isFillCurrentColor && | ||
!isStrokeCurrentColor && | ||
// matches attribute name | ||
list[1].test(name) && | ||
// matches attribute value | ||
list[2].test(value) | ||
) { | ||
delete node.attributes[name]; | ||
} | ||
@@ -147,5 +141,5 @@ } | ||
} | ||
} | ||
}); | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -18,2 +18,4 @@ 'use strict'; | ||
* @author Kir Belevich | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -20,0 +22,0 @@ exports.fn = () => { |
@@ -20,2 +20,4 @@ 'use strict'; | ||
* @author Daniel Wabyick | ||
* | ||
* @type {import('../lib/types').Plugin<{ removeAny?: boolean }>} | ||
*/ | ||
@@ -22,0 +24,0 @@ exports.fn = (root, params) => { |
@@ -31,2 +31,4 @@ 'use strict'; | ||
* @author Kir Belevich | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -33,0 +35,0 @@ exports.fn = () => { |
'use strict'; | ||
const { detachNodeFromParent } = require('../lib/xast.js'); | ||
exports.name = 'removeElementsByAttr'; | ||
exports.type = 'perItem'; | ||
exports.type = 'visitor'; | ||
exports.active = false; | ||
exports.description = | ||
'removes arbitrary elements by ID or className (disabled by default)'; | ||
exports.params = { | ||
id: [], | ||
class: [], | ||
}; | ||
/** | ||
* Remove arbitrary SVG elements by ID or className. | ||
* | ||
* @param id | ||
* examples: | ||
* | ||
* @example id | ||
* > single: remove element with ID of `elementID` | ||
@@ -35,5 +27,3 @@ * --- | ||
* | ||
* @param class | ||
* examples: | ||
* | ||
* @example class | ||
* > single: remove all elements with class of `elementClass` | ||
@@ -51,31 +41,40 @@ * --- | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Eli Dupuis (@elidupuis) | ||
* | ||
* @author Eli Dupuis (@elidupuis) | ||
* @type {import('../lib/types').Plugin<{ | ||
* id?: string | Array<string>, | ||
* class?: string | Array<string> | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
// wrap params in an array if not already | ||
['id', 'class'].forEach(function (key) { | ||
if (!Array.isArray(params[key])) { | ||
params[key] = [params[key]]; | ||
} | ||
}); | ||
// abort if current item is no an element | ||
if (item.type !== 'element') { | ||
return; | ||
} | ||
// remove element if it's `id` matches configured `id` params | ||
if (item.attributes.id != null && params.id.length !== 0) { | ||
return params.id.includes(item.attributes.id) === false; | ||
} | ||
// remove element if it's `class` contains any of the configured `class` params | ||
if (item.attributes.class && params.class.length !== 0) { | ||
const classList = item.attributes.class.split(' '); | ||
return params.class.some((item) => classList.includes(item)) === false; | ||
} | ||
exports.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]; | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
// remove element if it's `id` matches configured `id` params | ||
if (node.attributes.id != null && ids.length !== 0) { | ||
if (ids.includes(node.attributes.id)) { | ||
detachNodeFromParent(node, parentNode); | ||
} | ||
} | ||
// remove element if it's `class` contains any of the configured `class` params | ||
if (node.attributes.class && classes.length !== 0) { | ||
const classList = node.attributes.class.split(' '); | ||
for (const item of classes) { | ||
if (classList.includes(item)) { | ||
detachNodeFromParent(node, parentNode); | ||
break; | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -26,2 +26,8 @@ 'use strict'; | ||
* @author Kir Belevich | ||
* | ||
* @type {import('../lib/types').Plugin<{ | ||
* text?: boolean, | ||
* tspan?: boolean, | ||
* tref?: boolean | ||
* }>} | ||
*/ | ||
@@ -28,0 +34,0 @@ exports.fn = (root, params) => { |
@@ -30,6 +30,21 @@ 'use strict'; | ||
* | ||
* @param {Object} root | ||
* @param {Object} params | ||
* @author Kir Belevich | ||
* | ||
* @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, | ||
* }>} | ||
*/ | ||
@@ -36,0 +51,0 @@ exports.fn = (root, params) => { |
@@ -16,2 +16,4 @@ 'use strict'; | ||
* @author Kir Belevich | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -18,0 +20,0 @@ exports.fn = () => { |
'use strict'; | ||
exports.name = 'removeOffCanvasPaths'; | ||
/** | ||
* @typedef {import('../lib/types').PathDataItem} PathDataItem | ||
*/ | ||
exports.type = 'perItem'; | ||
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)'; | ||
const JSAPI = require('../lib/svgo/jsAPI.js'); | ||
var _path = require('./_path.js'), | ||
intersects = _path.intersects, | ||
path2js = _path.path2js, | ||
viewBox, | ||
viewBoxJS; | ||
/** | ||
* Remove elements that are drawn outside of the viewbox. | ||
* | ||
* @param {Object} item current iteration item | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author JoshyPHP | ||
* | ||
* @author JoshyPHP | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
exports.fn = function (item) { | ||
if ( | ||
item.type === 'element' && | ||
item.name === 'path' && | ||
item.attributes.d != null && | ||
typeof viewBox !== 'undefined' | ||
) { | ||
// Consider that any item with a transform attribute or a M instruction | ||
// within the viewBox is visible | ||
if (hasTransform(item) || pathMovesWithinViewBox(item.attributes.d)) { | ||
return true; | ||
} | ||
exports.fn = () => { | ||
/** | ||
* @type {null | { | ||
* top: number, | ||
* right: number, | ||
* bottom: number, | ||
* left: number, | ||
* width: number, | ||
* height: number | ||
* }} | ||
*/ | ||
let viewBoxData = null; | ||
var pathJS = path2js(item); | ||
if (pathJS.length === 2) { | ||
// Use a closed clone of the path if it's too short for intersects() | ||
pathJS = JSON.parse(JSON.stringify(pathJS)); | ||
pathJS.push({ instruction: 'z' }); | ||
} | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
if (node.name === 'svg' && parentNode.type === 'root') { | ||
let viewBox = ''; | ||
// find viewbox | ||
if (node.attributes.viewBox != null) { | ||
// remove commas and plus signs, normalize and trim whitespace | ||
viewBox = node.attributes.viewBox; | ||
} else if ( | ||
node.attributes.height != null && | ||
node.attributes.width != null | ||
) { | ||
viewBox = `0 0 ${node.attributes.width} ${node.attributes.height}`; | ||
} | ||
return intersects(viewBoxJS, pathJS); | ||
} | ||
if (item.type === 'element' && item.name === 'svg') { | ||
parseViewBox(item); | ||
} | ||
// parse viewbox | ||
// remove commas and plus signs, normalize and trim whitespace | ||
viewBox = viewBox | ||
.replace(/[,+]|px/g, ' ') | ||
.replace(/\s+/g, ' ') | ||
.replace(/^\s*|\s*$/g, ''); | ||
// ensure that the dimensions are 4 values separated by space | ||
const m = | ||
/^(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+)$/.exec( | ||
viewBox | ||
); | ||
if (m == null) { | ||
return; | ||
} | ||
const left = Number.parseFloat(m[1]); | ||
const top = Number.parseFloat(m[2]); | ||
const width = Number.parseFloat(m[3]); | ||
const height = Number.parseFloat(m[4]); | ||
return true; | ||
}; | ||
// store the viewBox boundaries | ||
viewBoxData = { | ||
left, | ||
top, | ||
right: left + width, | ||
bottom: top + height, | ||
width, | ||
height, | ||
}; | ||
} | ||
/** | ||
* Test whether given item or any of its ancestors has a transform attribute. | ||
* | ||
* @param {String} path | ||
* @return {Boolean} | ||
*/ | ||
function hasTransform(item) { | ||
return ( | ||
item.attributes.transform != null || | ||
(item.parentNode && | ||
item.parentNode.type === 'element' && | ||
hasTransform(item.parentNode)) | ||
); | ||
} | ||
// consider that any item with a transform attribute is visible | ||
if (node.attributes.transform != null) { | ||
return visitSkip; | ||
} | ||
/** | ||
* Parse the viewBox coordinates and compute the JS representation of its path. | ||
* | ||
* @param {Object} svg svg element item | ||
*/ | ||
function parseViewBox(svg) { | ||
var viewBoxData = ''; | ||
if (svg.attributes.viewBox != null) { | ||
// Remove commas and plus signs, normalize and trim whitespace | ||
viewBoxData = svg.attributes.viewBox; | ||
} else if (svg.attributes.height != null && svg.attributes.width != null) { | ||
viewBoxData = `0 0 ${svg.attributes.width} ${svg.attributes.height}`; | ||
} | ||
if ( | ||
node.name === 'path' && | ||
node.attributes.d != null && | ||
viewBoxData != null | ||
) { | ||
const pathData = parsePathData(node.attributes.d); | ||
// Remove commas and plus signs, normalize and trim whitespace | ||
viewBoxData = viewBoxData | ||
.replace(/[,+]|px/g, ' ') | ||
.replace(/\s+/g, ' ') | ||
.replace(/^\s*|\s*$/g, ''); | ||
// consider that a M command within the viewBox is visible | ||
let visible = false; | ||
for (const pathDataItem of pathData) { | ||
if (pathDataItem.command === 'M') { | ||
const [x, y] = pathDataItem.args; | ||
if ( | ||
x >= viewBoxData.left && | ||
x <= viewBoxData.right && | ||
y >= viewBoxData.top && | ||
y <= viewBoxData.bottom | ||
) { | ||
visible = true; | ||
} | ||
} | ||
} | ||
if (visible) { | ||
return; | ||
} | ||
// Ensure that the dimensions are 4 values separated by space | ||
var m = /^(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+)$/.exec( | ||
viewBoxData | ||
); | ||
if (!m) { | ||
return; | ||
} | ||
if (pathData.length === 2) { | ||
// close the path too short for intersects() | ||
pathData.push({ command: 'z', args: [] }); | ||
} | ||
// Store the viewBox boundaries | ||
viewBox = { | ||
left: parseFloat(m[1]), | ||
top: parseFloat(m[2]), | ||
right: parseFloat(m[1]) + parseFloat(m[3]), | ||
bottom: parseFloat(m[2]) + parseFloat(m[4]), | ||
}; | ||
const { left, top, width, height } = viewBoxData; | ||
/** | ||
* @type {Array<PathDataItem>} | ||
*/ | ||
const viewBoxPathData = [ | ||
{ command: 'M', args: [left, top] }, | ||
{ command: 'h', args: [width] }, | ||
{ command: 'v', args: [height] }, | ||
{ command: 'H', args: [left] }, | ||
{ command: 'z', args: [] }, | ||
]; | ||
var path = new JSAPI({ | ||
type: 'element', | ||
name: 'path', | ||
attributes: { | ||
d: 'M' + m[1] + ' ' + m[2] + 'h' + m[3] + 'v' + m[4] + 'H' + m[1] + 'z', | ||
if (intersects(viewBoxPathData, pathData) === false) { | ||
detachNodeFromParent(node, parentNode); | ||
} | ||
} | ||
}, | ||
}, | ||
content: [], | ||
}); | ||
viewBoxJS = path2js(path); | ||
} | ||
/** | ||
* Test whether given path has a M instruction with coordinates within the viewBox. | ||
* | ||
* @param {String} path | ||
* @return {Boolean} | ||
*/ | ||
function pathMovesWithinViewBox(path) { | ||
var regexp = /M\s*(-?\d*\.?\d+)(?!\d)\s*(-?\d*\.?\d+)/g, | ||
m; | ||
while (null !== (m = regexp.exec(path))) { | ||
if ( | ||
m[1] >= viewBox.left && | ||
m[1] <= viewBox.right && | ||
m[2] >= viewBox.top && | ||
m[2] <= viewBox.bottom | ||
) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
}; | ||
}; |
@@ -16,2 +16,4 @@ 'use strict'; | ||
* @author Kir Belevich | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -18,0 +20,0 @@ exports.fn = () => { |
@@ -15,4 +15,5 @@ 'use strict'; | ||
* | ||
* @author Patrick Klingemann | ||
* | ||
* @author Patrick Klingemann | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -19,0 +20,0 @@ exports.fn = () => { |
@@ -16,2 +16,4 @@ 'use strict'; | ||
* @author Betsy Dupuis | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -18,0 +20,0 @@ exports.fn = () => { |
@@ -16,2 +16,4 @@ 'use strict'; | ||
* @author Igor Kalashnikov | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -18,0 +20,0 @@ exports.fn = () => { |
'use strict'; | ||
const { parseName } = require('../lib/svgo/tools.js'); | ||
const { visitSkip, detachNodeFromParent } = require('../lib/xast.js'); | ||
const { collectStylesheet, computeStyle } = require('../lib/style.js'); | ||
const { | ||
elems, | ||
attrsGroups, | ||
elemsGroups, | ||
attrsGroupsDefaults, | ||
presentationNonInheritableGroupAttrs, | ||
} = require('./_collections'); | ||
exports.type = 'visitor'; | ||
exports.name = 'removeUnknownsAndDefaults'; | ||
exports.type = 'perItem'; | ||
exports.active = true; | ||
exports.description = | ||
'removes unknown elements content and attributes, removes attrs with default values'; | ||
exports.params = { | ||
unknownContent: true, | ||
unknownAttrs: true, | ||
defaultAttrs: true, | ||
uselessOverrides: true, | ||
keepDataAttrs: true, | ||
keepAriaAttrs: true, | ||
keepRoleAttr: false, | ||
}; | ||
// resolve all groups references | ||
var collections = require('./_collections'), | ||
elems = collections.elems, | ||
attrsGroups = collections.attrsGroups, | ||
elemsGroups = collections.elemsGroups, | ||
attrsGroupsDefaults = collections.attrsGroupsDefaults, | ||
attrsInheritable = collections.inheritableAttrs, | ||
applyGroups = collections.presentationNonInheritableGroupAttrs; | ||
/** | ||
* @type {Map<string, Set<string>>} | ||
*/ | ||
const allowedChildrenPerElement = new Map(); | ||
/** | ||
* @type {Map<string, Set<string>>} | ||
*/ | ||
const allowedAttributesPerElement = new Map(); | ||
/** | ||
* @type {Map<string, Map<string, string>>} | ||
*/ | ||
const attributesDefaultsPerElement = new Map(); | ||
// collect and extend all references | ||
for (const elem of Object.values(elems)) { | ||
if (elem.attrsGroups) { | ||
elem.attrs = elem.attrs || []; | ||
elem.attrsGroups.forEach(function (attrsGroupName) { | ||
elem.attrs = elem.attrs.concat(attrsGroups[attrsGroupName]); | ||
var groupDefaults = attrsGroupsDefaults[attrsGroupName]; | ||
if (groupDefaults) { | ||
elem.defaults = elem.defaults || {}; | ||
for (const [attrName, attr] of Object.entries(groupDefaults)) { | ||
elem.defaults[attrName] = attr; | ||
for (const [name, config] of Object.entries(elems)) { | ||
/** | ||
* @type {Set<string>} | ||
*/ | ||
const allowedChildren = new Set(); | ||
if (config.content) { | ||
for (const elementName of config.content) { | ||
allowedChildren.add(elementName); | ||
} | ||
} | ||
if (config.contentGroups) { | ||
for (const contentGroupName of config.contentGroups) { | ||
const elemsGroup = elemsGroups[contentGroupName]; | ||
if (elemsGroup) { | ||
for (const elementName of elemsGroup) { | ||
allowedChildren.add(elementName); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
if (elem.contentGroups) { | ||
elem.content = elem.content || []; | ||
elem.contentGroups.forEach(function (contentGroupName) { | ||
elem.content = elem.content.concat(elemsGroups[contentGroupName]); | ||
}); | ||
/** | ||
* @type {Set<string>} | ||
*/ | ||
const allowedAttributes = new Set(); | ||
if (config.attrs) { | ||
for (const attrName of config.attrs) { | ||
allowedAttributes.add(attrName); | ||
} | ||
} | ||
/** | ||
* @type {Map<string, string>} | ||
*/ | ||
const attributesDefaults = new Map(); | ||
if (config.defaults) { | ||
for (const [attrName, defaultValue] of Object.entries(config.defaults)) { | ||
attributesDefaults.set(attrName, defaultValue); | ||
} | ||
} | ||
for (const attrsGroupName of config.attrsGroups) { | ||
const attrsGroup = attrsGroups[attrsGroupName]; | ||
if (attrsGroup) { | ||
for (const attrName of attrsGroup) { | ||
allowedAttributes.add(attrName); | ||
} | ||
} | ||
const groupDefaults = attrsGroupsDefaults[attrsGroupName]; | ||
if (groupDefaults) { | ||
for (const [attrName, defaultValue] of Object.entries(groupDefaults)) { | ||
attributesDefaults.set(attrName, defaultValue); | ||
} | ||
} | ||
} | ||
allowedChildrenPerElement.set(name, allowedChildren); | ||
allowedAttributesPerElement.set(name, allowedAttributes); | ||
attributesDefaultsPerElement.set(name, attributesDefaults); | ||
} | ||
@@ -65,67 +95,125 @@ | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @author Kir Belevich | ||
* @type {import('../lib/types').Plugin<{ | ||
* unknownContent?: boolean, | ||
* unknownAttrs?: boolean, | ||
* defaultAttrs?: boolean, | ||
* uselessOverrides?: boolean, | ||
* keepDataAttrs?: boolean, | ||
* keepAriaAttrs?: boolean, | ||
* keepRoleAttr?: boolean, | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
// elems w/o namespace prefix | ||
if (item.type === 'element' && !parseName(item.name).prefix) { | ||
var elem = item.name; | ||
exports.fn = (root, params) => { | ||
const { | ||
unknownContent = true, | ||
unknownAttrs = true, | ||
defaultAttrs = true, | ||
uselessOverrides = true, | ||
keepDataAttrs = true, | ||
keepAriaAttrs = true, | ||
keepRoleAttr = false, | ||
} = params; | ||
const stylesheet = collectStylesheet(root); | ||
// remove unknown element's content | ||
if ( | ||
params.unknownContent && | ||
elems[elem] && // make sure we know of this element before checking its children | ||
elem !== 'foreignObject' // Don't check foreignObject | ||
) { | ||
item.children.forEach(function (content, i) { | ||
if ( | ||
content.type === 'element' && | ||
!parseName(content.name).prefix && | ||
((elems[elem].content && // Do we have a record of its permitted content? | ||
elems[elem].content.indexOf(content.name) === -1) || | ||
(!elems[elem].content && // we dont know about its permitted content | ||
!elems[content.name])) // check that we know about the element at all | ||
) { | ||
item.children.splice(i, 1); | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
// skip namespaced elements | ||
if (node.name.includes(':')) { | ||
return; | ||
} | ||
}); | ||
} | ||
// skip visiting foreignObject subtree | ||
if (node.name === 'foreignObject') { | ||
return visitSkip; | ||
} | ||
// remove element's unknown attrs and attrs with default values | ||
if (elems[elem] && elems[elem].attrs) { | ||
for (const [name, value] of Object.entries(item.attributes)) { | ||
const { prefix } = parseName(name); | ||
if ( | ||
name !== 'xmlns' && | ||
(prefix === 'xml' || !prefix) && | ||
(!params.keepDataAttrs || name.indexOf('data-') != 0) && | ||
(!params.keepAriaAttrs || name.indexOf('aria-') != 0) && | ||
(!params.keepRoleAttr || name != 'role') | ||
) { | ||
// remove unknown element's content | ||
if (unknownContent && parentNode.type === 'element') { | ||
const allowedChildren = allowedChildrenPerElement.get( | ||
parentNode.name | ||
); | ||
if (allowedChildren == null || allowedChildren.size === 0) { | ||
// remove unknown elements | ||
if (allowedChildrenPerElement.get(node.name) == null) { | ||
detachNodeFromParent(node, parentNode); | ||
return; | ||
} | ||
} else { | ||
// remove not allowed children | ||
if (allowedChildren.has(node.name) === false) { | ||
detachNodeFromParent(node, parentNode); | ||
return; | ||
} | ||
} | ||
} | ||
const allowedAttributes = allowedAttributesPerElement.get(node.name); | ||
const attributesDefaults = attributesDefaultsPerElement.get(node.name); | ||
const computedParentStyle = | ||
parentNode.type === 'element' | ||
? computeStyle(stylesheet, parentNode) | ||
: null; | ||
// remove element's unknown attrs and attrs with default values | ||
for (const [name, value] of Object.entries(node.attributes)) { | ||
if (keepDataAttrs && name.startsWith('data-')) { | ||
continue; | ||
} | ||
if (keepAriaAttrs && name.startsWith('aria-')) { | ||
continue; | ||
} | ||
if (keepRoleAttr && name === 'role') { | ||
continue; | ||
} | ||
// skip xmlns attribute | ||
if (name === 'xmlns') { | ||
continue; | ||
} | ||
// skip namespaced attributes except xml:* and xlink:* | ||
if (name.includes(':')) { | ||
const [prefix] = name.split(':'); | ||
if (prefix !== 'xml' && prefix !== 'xlink') { | ||
continue; | ||
} | ||
} | ||
if ( | ||
// unknown attrs | ||
(params.unknownAttrs && elems[elem].attrs.indexOf(name) === -1) || | ||
// attrs with default values | ||
(params.defaultAttrs && | ||
item.attributes.id == null && | ||
elems[elem].defaults && | ||
elems[elem].defaults[name] === value && | ||
(attrsInheritable.includes(name) === false || | ||
!item.parentNode.computedAttr(name))) || | ||
// useless overrides | ||
(params.uselessOverrides && | ||
item.attributes.id == null && | ||
applyGroups.includes(name) === false && | ||
attrsInheritable.includes(name) === true && | ||
item.parentNode.computedAttr(name, value)) | ||
unknownAttrs && | ||
allowedAttributes && | ||
allowedAttributes.has(name) === false | ||
) { | ||
delete item.attributes[name]; | ||
delete node.attributes[name]; | ||
} | ||
if ( | ||
defaultAttrs && | ||
node.attributes.id == null && | ||
attributesDefaults && | ||
attributesDefaults.get(name) === value | ||
) { | ||
// keep defaults if parent has own or inherited style | ||
if ( | ||
computedParentStyle == null || | ||
computedParentStyle[name] == null | ||
) { | ||
delete node.attributes[name]; | ||
} | ||
} | ||
if (uselessOverrides && node.attributes.id == null) { | ||
const style = | ||
computedParentStyle == null ? null : computedParentStyle[name]; | ||
if ( | ||
presentationNonInheritableGroupAttrs.includes(name) === false && | ||
style != null && | ||
style.type === 'static' && | ||
style.value === value | ||
) { | ||
delete node.attributes[name]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
const { elemsGroups } = require('./_collections'); | ||
/** | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
*/ | ||
const { detachNodeFromParent } = require('../lib/xast.js'); | ||
const { elemsGroups } = require('./_collections.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'removeUselessDefs'; | ||
exports.type = 'perItem'; | ||
exports.active = true; | ||
exports.description = 'removes elements in <defs> without id'; | ||
@@ -16,36 +18,49 @@ | ||
* | ||
* @param {Object} item current iteration item | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Lev Solntsev | ||
* | ||
* @author Lev Solntsev | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
exports.fn = function (item) { | ||
if (item.type === 'element') { | ||
if (item.name === 'defs') { | ||
item.children = getUsefulItems(item, []); | ||
if (item.children.length === 0) { | ||
return false; | ||
} | ||
} else if ( | ||
elemsGroups.nonRendering.includes(item.name) && | ||
item.attributes.id == null | ||
) { | ||
return false; | ||
} | ||
} | ||
exports.fn = () => { | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
if (node.name === 'defs') { | ||
/** | ||
* @type {Array<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); | ||
} | ||
}, | ||
}, | ||
}; | ||
}; | ||
function getUsefulItems(item, usefulItems) { | ||
for (const child of item.children) { | ||
/** | ||
* @type {(node: XastElement, usefulNodes: Array<XastElement>) => void} | ||
*/ | ||
const collectUsefulNodes = (node, usefulNodes) => { | ||
for (const child of node.children) { | ||
if (child.type === 'element') { | ||
if (child.attributes.id != null || child.name === 'style') { | ||
usefulItems.push(child); | ||
child.parentNode = item; | ||
usefulNodes.push(child); | ||
} else { | ||
child.children = getUsefulItems(child, usefulItems); | ||
collectUsefulNodes(child, usefulNodes); | ||
} | ||
} | ||
} | ||
return usefulItems; | ||
} | ||
}; |
@@ -19,4 +19,2 @@ 'use strict'; | ||
var shape = require('./_collections').elemsGroups.shape, | ||
regStrokeProps = /^stroke/, | ||
regFillProps = /^fill-/, | ||
styleOrScript = ['style', 'script']; | ||
@@ -63,3 +61,3 @@ | ||
for (const name of Object.keys(item.attributes)) { | ||
if (regStrokeProps.test(name)) { | ||
if (name.startsWith('stroke')) { | ||
delete item.attributes[name]; | ||
@@ -78,3 +76,3 @@ } | ||
for (const name of Object.keys(item.attributes)) { | ||
if (regFillProps.test(name)) { | ||
if (name.startsWith('fill-')) { | ||
delete item.attributes[name]; | ||
@@ -81,0 +79,0 @@ } |
'use strict'; | ||
const { closestByName } = require('../lib/xast.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'removeViewBox'; | ||
exports.type = 'perItem'; | ||
exports.active = true; | ||
exports.description = 'removes viewBox attribute when possible'; | ||
@@ -25,31 +20,33 @@ | ||
* | ||
* @param {Object} item current iteration item | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @author Kir Belevich | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
exports.fn = function (item) { | ||
if ( | ||
item.type === 'element' && | ||
viewBoxElems.includes(item.name) && | ||
item.attributes.viewBox != null && | ||
item.attributes.width != null && | ||
item.attributes.height != null | ||
) { | ||
// TODO remove width/height for such case instead | ||
if (item.name === 'svg' && closestByName(item.parentNode, 'svg')) { | ||
return; | ||
} | ||
const nums = item.attributes.viewBox.split(/[ ,]+/g); | ||
if ( | ||
nums[0] === '0' && | ||
nums[1] === '0' && | ||
item.attributes.width.replace(/px$/, '') === nums[2] && // could use parseFloat too | ||
item.attributes.height.replace(/px$/, '') === nums[3] | ||
) { | ||
delete item.attributes.viewBox; | ||
} | ||
} | ||
exports.fn = () => { | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
if ( | ||
viewBoxElems.includes(node.name) && | ||
node.attributes.viewBox != null && | ||
node.attributes.width != null && | ||
node.attributes.height != null | ||
) { | ||
// TODO remove width/height for such case instead | ||
if (node.name === 'svg' && parentNode.type !== 'root') { | ||
return; | ||
} | ||
const nums = node.attributes.viewBox.split(/[ ,]+/g); | ||
if ( | ||
nums[0] === '0' && | ||
nums[1] === '0' && | ||
node.attributes.width.replace(/px$/, '') === nums[2] && // could use parseFloat too | ||
node.attributes.height.replace(/px$/, '') === nums[3] | ||
) { | ||
delete node.attributes.viewBox; | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -17,2 +17,4 @@ 'use strict'; | ||
* @author Kir Belevich | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
@@ -19,0 +21,0 @@ exports.fn = () => { |
@@ -65,12 +65,3 @@ 'use strict'; | ||
for (let def of defs) { | ||
// Remove class and style before copying to avoid circular refs in | ||
// JSON.stringify. This is fine because we don't actually want class or | ||
// style information to be copied. | ||
const style = def.style; | ||
const defClass = def.class; | ||
delete def.style; | ||
delete def.class; | ||
const defClone = def.clone(); | ||
def.style = style; | ||
def.class = defClass; | ||
delete defClone.attributes.transform; | ||
@@ -77,0 +68,0 @@ defsTag.spliceContent(0, 0, defClone); |
'use strict'; | ||
exports.type = 'visitor'; | ||
exports.name = 'sortDefsChildren'; | ||
exports.type = 'perItem'; | ||
exports.active = true; | ||
exports.description = 'Sorts children of <defs> to improve compression'; | ||
@@ -15,30 +12,50 @@ | ||
* | ||
* @param {Object} item current iteration item | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author David Leston | ||
* | ||
* @author David Leston | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
exports.fn = function (item) { | ||
if (item.isElem('defs')) { | ||
var frequency = item.children.reduce(function (frequency, child) { | ||
if (child.name in frequency) { | ||
frequency[child.name]++; | ||
} else { | ||
frequency[child.name] = 1; | ||
} | ||
return frequency; | ||
}, {}); | ||
item.children.sort(function (a, b) { | ||
var frequencyComparison = frequency[b.name] - frequency[a.name]; | ||
if (frequencyComparison !== 0) { | ||
return frequencyComparison; | ||
} | ||
var lengthComparison = b.name.length - a.name.length; | ||
if (lengthComparison !== 0) { | ||
return lengthComparison; | ||
} | ||
return a.name != b.name ? (a.name > b.name ? -1 : 1) : 0; | ||
}); | ||
return true; | ||
} | ||
exports.fn = () => { | ||
return { | ||
element: { | ||
enter: (node) => { | ||
if (node.name === 'defs') { | ||
/** | ||
* @type {Map<string, number>} | ||
*/ | ||
const frequencies = new Map(); | ||
for (const child of node.children) { | ||
if (child.type === 'element') { | ||
const frequency = frequencies.get(child.name); | ||
if (frequency == null) { | ||
frequencies.set(child.name, 1); | ||
} else { | ||
frequencies.set(child.name, frequency + 1); | ||
} | ||
} | ||
} | ||
node.children.sort((a, b) => { | ||
if (a.type !== 'element' || b.type !== 'element') { | ||
return 0; | ||
} | ||
const aFrequency = frequencies.get(a.name); | ||
const bFrequency = frequencies.get(b.name); | ||
if (aFrequency != null && bFrequency != null) { | ||
const frequencyComparison = bFrequency - aFrequency; | ||
if (frequencyComparison !== 0) { | ||
return frequencyComparison; | ||
} | ||
} | ||
const lengthComparison = b.name.length - a.name.length; | ||
if (lengthComparison !== 0) { | ||
return lengthComparison; | ||
} | ||
if (a.name !== b.name) { | ||
return a.name > b.name ? -1 : 1; | ||
} | ||
return 0; | ||
}); | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -92,3 +92,3 @@ <div align="center"> | ||
overrides: { | ||
// customize options | ||
// customize options for plugins included in preset | ||
builtinPluginName: { | ||
@@ -102,2 +102,11 @@ optionName: 'optionValue', | ||
}, | ||
// Enable builtin plugin not included in preset | ||
'moreBuiltinPlugin', | ||
// Enable and configure builtin plugin not included in preset | ||
{ | ||
name: 'manyBuiltInPlugin', | ||
params: { | ||
optionName: 'value', | ||
}, | ||
}, | ||
], | ||
@@ -165,3 +174,3 @@ }; | ||
SVGO provides a few low level utilities. `extendDefaultPlugins` is described above. | ||
SVGO provides a few low level utilities. | ||
@@ -168,0 +177,0 @@ ### optimize |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
945509
79
14567
290
19
Updatedcolorette@^1.3.0
Updatedcommander@^7.2.0
Updatedcss-tree@^1.1.3