Skip to content

Commit

Permalink
refactor: improve performance of stringifyPathData (#1900)
Browse files Browse the repository at this point in the history
  • Loading branch information
SethFalco authored Dec 24, 2023
1 parent a3da8b3 commit 6996fca
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 60 deletions.
130 changes: 81 additions & 49 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};

/**
Expand All @@ -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;
};

Expand All @@ -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;
23 changes: 12 additions & 11 deletions lib/svgo/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 6996fca

Please sign in to comment.