You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
309 lines
6.7 KiB
309 lines
6.7 KiB
2 years ago
|
'use strict';
|
||
|
|
||
|
|
||
|
var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0 };
|
||
|
|
||
|
var SPECIAL_SPACES = [
|
||
|
0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
|
||
|
0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF
|
||
|
];
|
||
|
|
||
|
function isSpace(ch) {
|
||
|
return (ch === 0x0A) || (ch === 0x0D) || (ch === 0x2028) || (ch === 0x2029) || // Line terminators
|
||
|
// White spaces
|
||
|
(ch === 0x20) || (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0) ||
|
||
|
(ch >= 0x1680 && SPECIAL_SPACES.indexOf(ch) >= 0);
|
||
|
}
|
||
|
|
||
|
function isCommand(code) {
|
||
|
/*eslint-disable no-bitwise*/
|
||
|
switch (code | 0x20) {
|
||
|
case 0x6D/* m */:
|
||
|
case 0x7A/* z */:
|
||
|
case 0x6C/* l */:
|
||
|
case 0x68/* h */:
|
||
|
case 0x76/* v */:
|
||
|
case 0x63/* c */:
|
||
|
case 0x73/* s */:
|
||
|
case 0x71/* q */:
|
||
|
case 0x74/* t */:
|
||
|
case 0x61/* a */:
|
||
|
case 0x72/* r */:
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function isArc(code) {
|
||
|
return (code | 0x20) === 0x61;
|
||
|
}
|
||
|
|
||
|
function isDigit(code) {
|
||
|
return (code >= 48 && code <= 57); // 0..9
|
||
|
}
|
||
|
|
||
|
function isDigitStart(code) {
|
||
|
return (code >= 48 && code <= 57) || /* 0..9 */
|
||
|
code === 0x2B || /* + */
|
||
|
code === 0x2D || /* - */
|
||
|
code === 0x2E; /* . */
|
||
|
}
|
||
|
|
||
|
|
||
|
function State(path) {
|
||
|
this.index = 0;
|
||
|
this.path = path;
|
||
|
this.max = path.length;
|
||
|
this.result = [];
|
||
|
this.param = 0.0;
|
||
|
this.err = '';
|
||
|
this.segmentStart = 0;
|
||
|
this.data = [];
|
||
|
}
|
||
|
|
||
|
function skipSpaces(state) {
|
||
|
while (state.index < state.max && isSpace(state.path.charCodeAt(state.index))) {
|
||
|
state.index++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function scanFlag(state) {
|
||
|
var ch = state.path.charCodeAt(state.index);
|
||
|
|
||
|
if (ch === 0x30/* 0 */) {
|
||
|
state.param = 0;
|
||
|
state.index++;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (ch === 0x31/* 1 */) {
|
||
|
state.param = 1;
|
||
|
state.index++;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
state.err = 'SvgPath: arc flag can be 0 or 1 only (at pos ' + state.index + ')';
|
||
|
}
|
||
|
|
||
|
|
||
|
function scanParam(state) {
|
||
|
var start = state.index,
|
||
|
index = start,
|
||
|
max = state.max,
|
||
|
zeroFirst = false,
|
||
|
hasCeiling = false,
|
||
|
hasDecimal = false,
|
||
|
hasDot = false,
|
||
|
ch;
|
||
|
|
||
|
if (index >= max) {
|
||
|
state.err = 'SvgPath: missed param (at pos ' + index + ')';
|
||
|
return;
|
||
|
}
|
||
|
ch = state.path.charCodeAt(index);
|
||
|
|
||
|
if (ch === 0x2B/* + */ || ch === 0x2D/* - */) {
|
||
|
index++;
|
||
|
ch = (index < max) ? state.path.charCodeAt(index) : 0;
|
||
|
}
|
||
|
|
||
|
// This logic is shamelessly borrowed from Esprima
|
||
|
// https://github.com/ariya/esprimas
|
||
|
//
|
||
|
if (!isDigit(ch) && ch !== 0x2E/* . */) {
|
||
|
state.err = 'SvgPath: param should start with 0..9 or `.` (at pos ' + index + ')';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (ch !== 0x2E/* . */) {
|
||
|
zeroFirst = (ch === 0x30/* 0 */);
|
||
|
index++;
|
||
|
|
||
|
ch = (index < max) ? state.path.charCodeAt(index) : 0;
|
||
|
|
||
|
if (zeroFirst && index < max) {
|
||
|
// decimal number starts with '0' such as '09' is illegal.
|
||
|
if (ch && isDigit(ch)) {
|
||
|
state.err = 'SvgPath: numbers started with `0` such as `09` are illegal (at pos ' + start + ')';
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
while (index < max && isDigit(state.path.charCodeAt(index))) {
|
||
|
index++;
|
||
|
hasCeiling = true;
|
||
|
}
|
||
|
ch = (index < max) ? state.path.charCodeAt(index) : 0;
|
||
|
}
|
||
|
|
||
|
if (ch === 0x2E/* . */) {
|
||
|
hasDot = true;
|
||
|
index++;
|
||
|
while (isDigit(state.path.charCodeAt(index))) {
|
||
|
index++;
|
||
|
hasDecimal = true;
|
||
|
}
|
||
|
ch = (index < max) ? state.path.charCodeAt(index) : 0;
|
||
|
}
|
||
|
|
||
|
if (ch === 0x65/* e */ || ch === 0x45/* E */) {
|
||
|
if (hasDot && !hasCeiling && !hasDecimal) {
|
||
|
state.err = 'SvgPath: invalid float exponent (at pos ' + index + ')';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
index++;
|
||
|
|
||
|
ch = (index < max) ? state.path.charCodeAt(index) : 0;
|
||
|
if (ch === 0x2B/* + */ || ch === 0x2D/* - */) {
|
||
|
index++;
|
||
|
}
|
||
|
if (index < max && isDigit(state.path.charCodeAt(index))) {
|
||
|
while (index < max && isDigit(state.path.charCodeAt(index))) {
|
||
|
index++;
|
||
|
}
|
||
|
} else {
|
||
|
state.err = 'SvgPath: invalid float exponent (at pos ' + index + ')';
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
state.index = index;
|
||
|
state.param = parseFloat(state.path.slice(start, index)) + 0.0;
|
||
|
}
|
||
|
|
||
|
|
||
|
function finalizeSegment(state) {
|
||
|
var cmd, cmdLC;
|
||
|
|
||
|
// Process duplicated commands (without comand name)
|
||
|
|
||
|
// This logic is shamelessly borrowed from Raphael
|
||
|
// https://github.com/DmitryBaranovskiy/raphael/
|
||
|
//
|
||
|
cmd = state.path[state.segmentStart];
|
||
|
cmdLC = cmd.toLowerCase();
|
||
|
|
||
|
var params = state.data;
|
||
|
|
||
|
if (cmdLC === 'm' && params.length > 2) {
|
||
|
state.result.push([ cmd, params[0], params[1] ]);
|
||
|
params = params.slice(2);
|
||
|
cmdLC = 'l';
|
||
|
cmd = (cmd === 'm') ? 'l' : 'L';
|
||
|
}
|
||
|
|
||
|
if (cmdLC === 'r') {
|
||
|
state.result.push([ cmd ].concat(params));
|
||
|
} else {
|
||
|
|
||
|
while (params.length >= paramCounts[cmdLC]) {
|
||
|
state.result.push([ cmd ].concat(params.splice(0, paramCounts[cmdLC])));
|
||
|
if (!paramCounts[cmdLC]) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function scanSegment(state) {
|
||
|
var max = state.max,
|
||
|
cmdCode, is_arc, comma_found, need_params, i;
|
||
|
|
||
|
state.segmentStart = state.index;
|
||
|
cmdCode = state.path.charCodeAt(state.index);
|
||
|
is_arc = isArc(cmdCode);
|
||
|
|
||
|
if (!isCommand(cmdCode)) {
|
||
|
state.err = 'SvgPath: bad command ' + state.path[state.index] + ' (at pos ' + state.index + ')';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
need_params = paramCounts[state.path[state.index].toLowerCase()];
|
||
|
|
||
|
state.index++;
|
||
|
skipSpaces(state);
|
||
|
|
||
|
state.data = [];
|
||
|
|
||
|
if (!need_params) {
|
||
|
// Z
|
||
|
finalizeSegment(state);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
comma_found = false;
|
||
|
|
||
|
for (;;) {
|
||
|
for (i = need_params; i > 0; i--) {
|
||
|
if (is_arc && (i === 3 || i === 4)) scanFlag(state);
|
||
|
else scanParam(state);
|
||
|
|
||
|
if (state.err.length) {
|
||
|
finalizeSegment(state);
|
||
|
return;
|
||
|
}
|
||
|
state.data.push(state.param);
|
||
|
|
||
|
skipSpaces(state);
|
||
|
comma_found = false;
|
||
|
|
||
|
if (state.index < max && state.path.charCodeAt(state.index) === 0x2C/* , */) {
|
||
|
state.index++;
|
||
|
skipSpaces(state);
|
||
|
comma_found = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// after ',' param is mandatory
|
||
|
if (comma_found) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (state.index >= state.max) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Stop on next segment
|
||
|
if (!isDigitStart(state.path.charCodeAt(state.index))) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
finalizeSegment(state);
|
||
|
}
|
||
|
|
||
|
|
||
|
/* Returns array of segments:
|
||
|
*
|
||
|
* [
|
||
|
* [ command, coord1, coord2, ... ]
|
||
|
* ]
|
||
|
*/
|
||
|
module.exports = function pathParse(svgPath) {
|
||
|
var state = new State(svgPath);
|
||
|
var max = state.max;
|
||
|
|
||
|
skipSpaces(state);
|
||
|
|
||
|
while (state.index < max && !state.err.length) {
|
||
|
scanSegment(state);
|
||
|
}
|
||
|
|
||
|
if (state.result.length) {
|
||
|
if ('mM'.indexOf(state.result[0][0]) < 0) {
|
||
|
state.err = 'SvgPath: string should start with `M` or `m`';
|
||
|
state.result = [];
|
||
|
} else {
|
||
|
state.result[0][0] = 'M';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
err: state.err,
|
||
|
segments: state.result
|
||
|
};
|
||
|
};
|