From d0177756f8d8731d0bba12bb27ffc3b3591bf3c9 Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Sun, 24 Dec 2023 15:48:45 +0000 Subject: [PATCH] refactor: improve performance of stringifyPathData --- lib/path.js | 130 +++++++++++++++++++++++++++++----------------- lib/svgo/tools.js | 23 ++++---- 2 files changed, 93 insertions(+), 60 deletions(-) diff --git a/lib/path.js b/lib/path.js index 1e383dd98..a5f7d5bfc 100644 --- a/lib/path.js +++ b/lib/path.js @@ -243,15 +243,21 @@ const parsePathData = (string) => { exports.parsePathData = parsePathData; /** - * @type {(number: number, precision?: number) => string} + * @type {(number: number, precision?: number) => { + * roundedStr: string, + * rounded: number + * }} */ -const stringifyNumber = (number, precision) => { +const roundAndStringify = (number, precision) => { if (precision != null) { const ratio = 10 ** precision; number = Math.round(number * ratio) / ratio; } - // remove zero whole from decimal number - return removeLeadingZero(number); + + return { + roundedStr: removeLeadingZero(number), + rounded: number, + }; }; /** @@ -267,29 +273,35 @@ const stringifyNumber = (number, precision) => { */ const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => { let result = ''; - let prev = ''; - for (let i = 0; i < args.length; i += 1) { - const number = args[i]; - const numberString = stringifyNumber(number, precision); + let previous; + + for (let i = 0; i < args.length; i++) { + const { roundedStr, rounded } = roundAndStringify(args[i], precision); if ( disableSpaceAfterFlags && (command === 'A' || command === 'a') && // consider combined arcs (i % 7 === 4 || i % 7 === 5) ) { - result += numberString; - } else if (i === 0 || numberString.startsWith('-')) { + result += roundedStr; + } else if (i === 0 || rounded < 0) { // avoid space before first and negative numbers - result += numberString; - } else if (prev.includes('.') && numberString.startsWith('.')) { + result += roundedStr; + } else if ( + !Number.isInteger(previous) && + rounded != 0 && + rounded < 1 && + rounded > -1 + ) { // remove space before decimal with zero whole // only when previous number is also decimal - result += numberString; + result += roundedStr; } else { - result += ` ${numberString}`; + result += ` ${roundedStr}`; } - prev = numberString; + previous = rounded; } + return result; }; @@ -302,48 +314,68 @@ const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => { */ /** - * @type {(options: StringifyPathDataOptions) => string} + * @param {StringifyPathDataOptions} options + * @returns {string} */ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => { - // combine sequence of the same commands - let combined = []; - for (let i = 0; i < pathData.length; i += 1) { + if (pathData.length === 1) { + const { command, args } = pathData[0]; + return ( + command + stringifyArgs(command, args, precision, disableSpaceAfterFlags) + ); + } + + let result = ''; + let prev = { ...pathData[0] }; + + // match leading moveto with following lineto + if (pathData[1].command === 'L') { + prev.command = 'M'; + } else if (pathData[1].command === 'l') { + prev.command = 'm'; + } + + for (let i = 1; i < pathData.length; i++) { const { command, args } = pathData[i]; - if (i === 0) { - combined.push({ command, args }); - } else { - /** - * @type {PathDataItem} - */ - const last = combined[combined.length - 1]; - // match leading moveto with following lineto - if (i === 1) { - if (command === 'L') { - last.command = 'M'; - } - if (command === 'l') { - last.command = 'm'; - } + if ( + (prev.command === command && + prev.command !== 'M' && + prev.command !== 'm') || + // combine matching moveto and lineto sequences + (prev.command === 'M' && command === 'L') || + (prev.command === 'm' && command === 'l') + ) { + prev.args = [...prev.args, ...args]; + if (i === pathData.length - 1) { + result += + prev.command + + stringifyArgs( + prev.command, + prev.args, + precision, + disableSpaceAfterFlags, + ); } - if ( - (last.command === command && - last.command !== 'M' && - last.command !== 'm') || - // combine matching moveto and lineto sequences - (last.command === 'M' && command === 'L') || - (last.command === 'm' && command === 'l') - ) { - last.args = [...last.args, ...args]; + } else { + result += + prev.command + + stringifyArgs( + prev.command, + prev.args, + precision, + disableSpaceAfterFlags, + ); + + if (i === pathData.length - 1) { + result += + command + + stringifyArgs(command, args, precision, disableSpaceAfterFlags); } else { - combined.push({ command, args }); + prev = { command, args }; } } } - let result = ''; - for (const { command, args } of combined) { - result += - command + stringifyArgs(command, args, precision, disableSpaceAfterFlags); - } + return result; }; exports.stringifyPathData = stringifyPathData; diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index e778034dc..afbda50c0 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -124,23 +124,24 @@ exports.cleanupOutData = (data, params, command) => { /** * Remove floating-point numbers leading zero. * + * @param {number} value + * @returns {string} * @example * 0.5 → .5 - * - * @example * -0.5 → -.5 - * - * @type {(num: number) => string} */ -const removeLeadingZero = (num) => { - var strNum = num.toString(); +const removeLeadingZero = (value) => { + const strValue = value.toString(); - if (0 < num && num < 1 && strNum.charAt(0) === '0') { - strNum = strNum.slice(1); - } else if (-1 < num && num < 0 && strNum.charAt(1) === '0') { - strNum = strNum.charAt(0) + strNum.slice(2); + if (0 < value && value < 1 && strValue.startsWith('0')) { + return strValue.slice(1); } - return strNum; + + if (-1 < value && value < 0 && strValue[1] === '0') { + return strValue[0] + strValue.slice(2); + } + + return strValue; }; exports.removeLeadingZero = removeLeadingZero;