254 lines
6.4 KiB
JavaScript
254 lines
6.4 KiB
JavaScript
import { parseSVG } from '../svg/parse.mjs';
|
|
|
|
const CLOSE_PATH = 1;
|
|
const MOVE_TO = 2;
|
|
const HORIZ_LINE_TO = 4;
|
|
const VERT_LINE_TO = 8;
|
|
const LINE_TO = 16;
|
|
const CURVE_TO = 32;
|
|
const SMOOTH_CURVE_TO = 64;
|
|
const QUAD_TO = 128;
|
|
const SMOOTH_QUAD_TO = 256;
|
|
const ARC = 512;
|
|
const argCount = {
|
|
[MOVE_TO]: 2,
|
|
[LINE_TO]: 2,
|
|
[HORIZ_LINE_TO]: 1,
|
|
[VERT_LINE_TO]: 1,
|
|
[CLOSE_PATH]: 0,
|
|
[QUAD_TO]: 4,
|
|
[SMOOTH_QUAD_TO]: 2,
|
|
[CURVE_TO]: 6,
|
|
[SMOOTH_CURVE_TO]: 4,
|
|
[ARC]: 7
|
|
};
|
|
const isDigit = (num) => num >= 48 && num <= 57;
|
|
const isWhiteSpace = (num) => num === 32 || num === 9 || num === 13 || num === 10;
|
|
function cleanPath(path) {
|
|
const commands = [];
|
|
const length = path.length;
|
|
let currentNumber = "";
|
|
let currentNumberHasExp = false;
|
|
let currentNumberHasExpDigits = false;
|
|
let currentNumberHasDecimal = false;
|
|
let canParseCommandOrComma = true;
|
|
let currentCommand = null;
|
|
let currentCommandType = null;
|
|
let currentArgs = [];
|
|
let i = 0;
|
|
const finishCommand = () => {
|
|
if (currentCommand !== null) {
|
|
commands.push({
|
|
command: currentCommand,
|
|
params: currentArgs.slice(0)
|
|
});
|
|
currentArgs = [];
|
|
canParseCommandOrComma = true;
|
|
switch (currentCommand) {
|
|
case "M":
|
|
currentCommand = "L";
|
|
break;
|
|
case "m":
|
|
currentCommand = "l";
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
const parseNumber = () => {
|
|
if (currentNumber !== "" && currentCommandType) {
|
|
let value = Number(currentNumber);
|
|
if (isNaN(value)) {
|
|
throw new Error(`Invalid number "${currentNumber}" at ${i}`);
|
|
}
|
|
if (currentCommandType === ARC) {
|
|
if (currentArgs.length < 2 && value <= 0) {
|
|
throw new Error(
|
|
`Expected positive number, got "${value}" at ${i}`
|
|
);
|
|
}
|
|
while (true) {
|
|
if (currentArgs.length < 3 || currentArgs.length > 4) {
|
|
break;
|
|
}
|
|
if (currentNumber === "0" || currentNumber === "1") {
|
|
break;
|
|
}
|
|
const slice = currentNumber.slice(0, 1);
|
|
const newNumber = currentNumber.slice(1);
|
|
const newValue = Number(newNumber);
|
|
if (slice === "0" || slice === "1") {
|
|
if (isNaN(newValue)) {
|
|
throw new Error(
|
|
`Invalid number "${currentNumber}" at ${i}`
|
|
);
|
|
}
|
|
currentArgs.push(slice);
|
|
currentNumber = newNumber;
|
|
value = newValue;
|
|
continue;
|
|
}
|
|
throw new Error(
|
|
`Expected a flag, got "${currentNumber}" at ${i}`
|
|
);
|
|
}
|
|
}
|
|
currentArgs.push(currentNumber);
|
|
if (currentArgs.length === argCount[currentCommandType]) {
|
|
finishCommand();
|
|
}
|
|
currentNumber = "";
|
|
currentNumberHasExpDigits = false;
|
|
currentNumberHasExp = false;
|
|
currentNumberHasDecimal = false;
|
|
canParseCommandOrComma = true;
|
|
}
|
|
};
|
|
for (i = 0; i < length; i++) {
|
|
const char = path[i];
|
|
const num = char.charCodeAt(0);
|
|
if (isDigit(num)) {
|
|
currentNumber += char;
|
|
currentNumberHasExpDigits = currentNumberHasExp;
|
|
continue;
|
|
}
|
|
if (char === "e" || char === "E") {
|
|
currentNumber += char;
|
|
currentNumberHasExp = true;
|
|
continue;
|
|
}
|
|
if ((char === "-" || char === "+") && currentNumberHasExp && !currentNumberHasExpDigits) {
|
|
currentNumber += char;
|
|
continue;
|
|
}
|
|
if (char === "." && !currentNumberHasExp && !currentNumberHasDecimal) {
|
|
currentNumber += char;
|
|
currentNumberHasDecimal = true;
|
|
continue;
|
|
}
|
|
parseNumber();
|
|
if (isWhiteSpace(num)) {
|
|
continue;
|
|
}
|
|
if (canParseCommandOrComma && char === ",") {
|
|
canParseCommandOrComma = false;
|
|
continue;
|
|
}
|
|
if (char === "+" || char === "-" || char === ".") {
|
|
currentNumber = char;
|
|
currentNumberHasDecimal = char === ".";
|
|
continue;
|
|
}
|
|
if (currentArgs.length > 0) {
|
|
throw new Error(`Unexpected command at ${i}`);
|
|
}
|
|
if (!canParseCommandOrComma) {
|
|
throw new Error(`Command cannot follow comma at ${i}`);
|
|
}
|
|
canParseCommandOrComma = false;
|
|
currentCommand = char;
|
|
switch (char) {
|
|
case "z":
|
|
case "Z":
|
|
commands.push({
|
|
command: char,
|
|
params: []
|
|
});
|
|
canParseCommandOrComma = true;
|
|
currentCommandType = null;
|
|
currentCommand = null;
|
|
break;
|
|
case "h":
|
|
case "H":
|
|
currentCommandType = HORIZ_LINE_TO;
|
|
break;
|
|
case "v":
|
|
case "V":
|
|
currentCommandType = VERT_LINE_TO;
|
|
break;
|
|
case "m":
|
|
case "M":
|
|
currentCommandType = MOVE_TO;
|
|
break;
|
|
case "l":
|
|
case "L":
|
|
currentCommandType = LINE_TO;
|
|
break;
|
|
case "c":
|
|
case "C":
|
|
currentCommandType = CURVE_TO;
|
|
break;
|
|
case "s":
|
|
case "S":
|
|
currentCommandType = SMOOTH_CURVE_TO;
|
|
break;
|
|
case "q":
|
|
case "Q":
|
|
currentCommandType = QUAD_TO;
|
|
break;
|
|
case "t":
|
|
case "T":
|
|
currentCommandType = SMOOTH_QUAD_TO;
|
|
break;
|
|
case "a":
|
|
case "A":
|
|
currentCommandType = ARC;
|
|
break;
|
|
default:
|
|
throw new Error(`Unexpected character "${char}" at ${i}`);
|
|
}
|
|
}
|
|
parseNumber();
|
|
if (currentArgs.length) {
|
|
if (!currentCommandType) {
|
|
throw new Error("Empty path");
|
|
}
|
|
if (currentArgs.length !== argCount[currentCommandType]) {
|
|
throw new Error(`Unexpected end of path at ${i}`);
|
|
}
|
|
finishCommand();
|
|
}
|
|
let output = "";
|
|
commands.forEach((item) => {
|
|
output += item.command;
|
|
item.params.forEach((value, index) => {
|
|
if (index > 0) {
|
|
const char = value[0];
|
|
switch (char) {
|
|
case "-":
|
|
case "+":
|
|
break;
|
|
case ".":
|
|
if (index < 1) {
|
|
break;
|
|
}
|
|
if (item.params[index - 1].includes(".")) {
|
|
break;
|
|
}
|
|
default:
|
|
output += " ";
|
|
}
|
|
}
|
|
output += value;
|
|
});
|
|
});
|
|
return output;
|
|
}
|
|
function deOptimisePaths(svg) {
|
|
parseSVG(svg, (item) => {
|
|
if (item.tagName !== "path") {
|
|
return;
|
|
}
|
|
const d = item.element.attribs.d;
|
|
if (typeof d === "string") {
|
|
try {
|
|
const optimised = cleanPath(d);
|
|
if (optimised !== d) {
|
|
item.$element.attr("d", optimised);
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
export { deOptimisePaths };
|