• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nfroidure / svg-pathdata / 23649555530

27 Mar 2026 01:52PM UTC coverage: 96.617% (+0.1%) from 96.498%
23649555530

push

github

nfroidure
9.0.0

665 of 700 branches covered (95.0%)

Branch coverage included in aggregate %.

763 of 778 relevant lines covered (98.07%)

684.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.92
/src/SVGPathDataParser.ts
1
// Parse SVG PathData
2
// http://www.w3.org/TR/SVG/paths.html#PathDataBNF
3
import { COMMAND_ARG_COUNTS, SVGPathData } from './SVGPathData.js';
4
import { TransformableSVG } from './TransformableSVG.js';
5
import type { SVGCommand, TransformFunction } from './types.js';
6
// Private consts : Char groups
7
const isWhiteSpace = (c: string) =>
32✔
8
  ' ' === c || '\t' === c || '\r' === c || '\n' === c;
9,877✔
9
const isDigit = (c: string) =>
32✔
10
  '0'.charCodeAt(0) <= c.charCodeAt(0) && c.charCodeAt(0) <= '9'.charCodeAt(0);
57,680✔
11

12
export class SVGPathDataParser extends TransformableSVG {
13
  private curNumber = '';
432✔
14
  private curCommandType: SVGCommand['type'] | -1 = -1;
432✔
15
  private curCommandRelative = false;
432✔
16
  private canParseCommandOrComma = true;
432✔
17
  private curNumberHasExp = false;
432✔
18
  private curNumberHasExpDigits = false;
432✔
19
  private curNumberHasDecimal = false;
432✔
20
  private curArgs: number[] = [];
432✔
21

22
  finish(commands: SVGCommand[] = []) {
1✔
23
    this.parse(' ', commands);
421✔
24
    // Adding residual command
25
    if (0 !== this.curArgs.length || !this.canParseCommandOrComma) {
420✔
26
      throw new SyntaxError('Unterminated command at the path end.');
31✔
27
    }
28
    return commands;
389✔
29
  }
30

31
  parse(str: string, commands: SVGCommand[] = []) {
4✔
32
    const finishCommand = (command: SVGCommand) => {
855✔
33
      commands.push(command);
2,061✔
34
      this.curArgs.length = 0;
2,061✔
35
      this.canParseCommandOrComma = true;
2,061✔
36
    };
37

38
    for (let i = 0; i < str.length; i++) {
855✔
39
      const c = str[i];
28,840✔
40
      // White spaces parsing
41
      const isAArcFlag =
42
        this.curCommandType === SVGPathData.ARC &&
28,840✔
43
        (this.curArgs.length === 3 || this.curArgs.length === 4) &&
44
        this.curNumber.length === 1 &&
45
        (this.curNumber === '0' || this.curNumber === '1');
46
      const isEndingDigit =
47
        isDigit(c) && ((this.curNumber === '0' && c === '0') || isAArcFlag);
28,840✔
48

49
      if (isDigit(c) && !isEndingDigit) {
28,840✔
50
        this.curNumber += c;
16,676✔
51
        this.curNumberHasExpDigits = this.curNumberHasExp;
16,676✔
52
        continue;
16,676✔
53
      }
54
      if ('e' === c || 'E' === c) {
12,164✔
55
        this.curNumber += c;
229✔
56
        this.curNumberHasExp = true;
229✔
57
        continue;
229✔
58
      }
59
      if (
11,935✔
60
        ('-' === c || '+' === c) &&
24,102✔
61
        this.curNumberHasExp &&
62
        !this.curNumberHasExpDigits
63
      ) {
64
        this.curNumber += c;
225✔
65
        continue;
225✔
66
      }
67
      // if we already have a ".", it means we are starting a new number
68
      if (
11,710✔
69
        '.' === c &&
17,652✔
70
        !this.curNumberHasExp &&
71
        !this.curNumberHasDecimal &&
72
        !isAArcFlag
73
      ) {
74
        this.curNumber += c;
1,829✔
75
        this.curNumberHasDecimal = true;
1,829✔
76
        continue;
1,829✔
77
      }
78

79
      // New number
80
      if (this.curNumber && -1 !== this.curCommandType) {
9,881✔
81
        const val = Number(this.curNumber);
6,714✔
82
        if (isNaN(val)) {
6,714✔
83
          throw new SyntaxError(`Invalid number ending at ${i}`);
1✔
84
        }
85
        if (this.curCommandType === SVGPathData.ARC) {
6,713✔
86
          if (0 === this.curArgs.length || 1 === this.curArgs.length) {
2,569✔
87
            if (0 > val) {
743✔
88
              throw new SyntaxError(
2✔
89
                `Expected positive number, got "${val}" at index "${i}"`,
90
              );
91
            }
92
          } else if (3 === this.curArgs.length || 4 === this.curArgs.length) {
1,826✔
93
            if ('0' !== this.curNumber && '1' !== this.curNumber) {
733✔
94
              throw new SyntaxError(
1✔
95
                `Expected a flag, got "${this.curNumber}" at index "${i}"`,
96
              );
97
            }
98
          }
99
        }
100
        this.curArgs.push(val);
6,710✔
101
        if (this.curArgs.length === COMMAND_ARG_COUNTS[this.curCommandType]) {
6,710✔
102
          if (SVGPathData.HORIZ_LINE_TO === this.curCommandType) {
2,061✔
103
            finishCommand({
257✔
104
              type: SVGPathData.HORIZ_LINE_TO,
105
              relative: this.curCommandRelative,
106
              x: val,
107
            });
108
          } else if (SVGPathData.VERT_LINE_TO === this.curCommandType) {
1,804✔
109
            finishCommand({
221✔
110
              type: SVGPathData.VERT_LINE_TO,
111
              relative: this.curCommandRelative,
112
              y: val,
113
            });
114
            // Move to / line to / smooth quadratic curve to commands (x, y)
115
          } else if (
1,583✔
116
            this.curCommandType === SVGPathData.MOVE_TO ||
3,549✔
117
            this.curCommandType === SVGPathData.LINE_TO ||
118
            this.curCommandType === SVGPathData.SMOOTH_QUAD_TO
119
          ) {
120
            finishCommand({
869✔
121
              type: this.curCommandType,
122
              relative: this.curCommandRelative,
123
              x: this.curArgs[0],
124
              y: this.curArgs[1],
125
            } as SVGCommand);
126
            // Switch to line to state
127
            if (SVGPathData.MOVE_TO === this.curCommandType) {
869✔
128
              this.curCommandType = SVGPathData.LINE_TO;
398✔
129
            }
130
          } else if (this.curCommandType === SVGPathData.CURVE_TO) {
714✔
131
            finishCommand({
235✔
132
              type: SVGPathData.CURVE_TO,
133
              relative: this.curCommandRelative,
134
              x1: this.curArgs[0],
135
              y1: this.curArgs[1],
136
              x2: this.curArgs[2],
137
              y2: this.curArgs[3],
138
              x: this.curArgs[4],
139
              y: this.curArgs[5],
140
            });
141
          } else if (this.curCommandType === SVGPathData.SMOOTH_CURVE_TO) {
479✔
142
            finishCommand({
59✔
143
              type: SVGPathData.SMOOTH_CURVE_TO,
144
              relative: this.curCommandRelative,
145
              x2: this.curArgs[0],
146
              y2: this.curArgs[1],
147
              x: this.curArgs[2],
148
              y: this.curArgs[3],
149
            });
150
          } else if (this.curCommandType === SVGPathData.QUAD_TO) {
420✔
151
            finishCommand({
58✔
152
              type: SVGPathData.QUAD_TO,
153
              relative: this.curCommandRelative,
154
              x1: this.curArgs[0],
155
              y1: this.curArgs[1],
156
              x: this.curArgs[2],
157
              y: this.curArgs[3],
158
            });
159
          } else if (this.curCommandType === SVGPathData.ARC) {
362!
160
            finishCommand({
362✔
161
              type: SVGPathData.ARC,
162
              relative: this.curCommandRelative,
163
              rX: this.curArgs[0],
164
              rY: this.curArgs[1],
165
              xRot: this.curArgs[2],
166
              lArcFlag: this.curArgs[3] as 0 | 1,
167
              sweepFlag: this.curArgs[4] as 0 | 1,
168
              x: this.curArgs[5],
169
              y: this.curArgs[6],
170
            });
171
          }
172
        }
173
        this.curNumber = '';
6,710✔
174
        this.curNumberHasExpDigits = false;
6,710✔
175
        this.curNumberHasExp = false;
6,710✔
176
        this.curNumberHasDecimal = false;
6,710✔
177
        this.canParseCommandOrComma = true;
6,710✔
178
      }
179
      // Continue if a white space or a comma was detected
180
      if (isWhiteSpace(c)) {
9,877✔
181
        continue;
5,309✔
182
      }
183
      if (',' === c && this.canParseCommandOrComma) {
4,568✔
184
        // L 0,0, H is not valid:
185
        this.canParseCommandOrComma = false;
800✔
186
        continue;
800✔
187
      }
188
      // if a sign is detected, then parse the new number
189
      if ('+' === c || '-' === c || '.' === c) {
3,768✔
190
        this.curNumber = c;
1,256✔
191
        this.curNumberHasDecimal = '.' === c;
1,256✔
192
        continue;
1,256✔
193
      }
194
      // if a 0 is detected, then parse the new number
195
      if (isEndingDigit) {
2,512✔
196
        this.curNumber = c;
400✔
197
        this.curNumberHasDecimal = false;
400✔
198
        continue;
400✔
199
      }
200

201
      // Adding residual command
202
      if (0 !== this.curArgs.length) {
2,112✔
203
        throw new SyntaxError(`Unterminated command at index ${i}.`);
6✔
204
      }
205
      if (!this.canParseCommandOrComma) {
2,106✔
206
        throw new SyntaxError(
1✔
207
          `Unexpected character "${c}" at index ${i}. Command cannot follow comma`,
208
        );
209
      }
210
      this.canParseCommandOrComma = false;
2,105✔
211
      // Detecting the next command
212
      if ('z' === c || 'Z' === c) {
2,105✔
213
        commands.push({
189✔
214
          type: SVGPathData.CLOSE_PATH,
215
        });
216
        this.canParseCommandOrComma = true;
189✔
217
        this.curCommandType = -1;
189✔
218
        continue;
189✔
219
        // Horizontal move to command
220
      } else if ('h' === c || 'H' === c) {
1,916✔
221
        this.curCommandType = SVGPathData.HORIZ_LINE_TO;
243✔
222
        this.curCommandRelative = 'h' === c;
243✔
223
        // Vertical move to command
224
      } else if ('v' === c || 'V' === c) {
1,673✔
225
        this.curCommandType = SVGPathData.VERT_LINE_TO;
206✔
226
        this.curCommandRelative = 'v' === c;
206✔
227
        // Move to command
228
      } else if ('m' === c || 'M' === c) {
1,467✔
229
        this.curCommandType = SVGPathData.MOVE_TO;
401✔
230
        this.curCommandRelative = 'm' === c;
401✔
231
        // Line to command
232
      } else if ('l' === c || 'L' === c) {
1,066✔
233
        this.curCommandType = SVGPathData.LINE_TO;
350✔
234
        this.curCommandRelative = 'l' === c;
350✔
235
        // Curve to command
236
      } else if ('c' === c || 'C' === c) {
716✔
237
        this.curCommandType = SVGPathData.CURVE_TO;
201✔
238
        this.curCommandRelative = 'c' === c;
201✔
239
        // Smooth curve to command
240
      } else if ('s' === c || 'S' === c) {
515✔
241
        this.curCommandType = SVGPathData.SMOOTH_CURVE_TO;
62✔
242
        this.curCommandRelative = 's' === c;
62✔
243
        // Quadratic bezier curve to command
244
      } else if ('q' === c || 'Q' === c) {
453✔
245
        this.curCommandType = SVGPathData.QUAD_TO;
61✔
246
        this.curCommandRelative = 'q' === c;
61✔
247
        // Smooth quadratic bezier curve to command
248
      } else if ('t' === c || 'T' === c) {
392✔
249
        this.curCommandType = SVGPathData.SMOOTH_QUAD_TO;
62✔
250
        this.curCommandRelative = 't' === c;
62✔
251
        // Elliptic arc command
252
      } else if ('a' === c || 'A' === c) {
330✔
253
        this.curCommandType = SVGPathData.ARC;
329✔
254
        this.curCommandRelative = 'a' === c;
329✔
255
      } else {
256
        throw new SyntaxError(`Unexpected character "${c}" at index ${i}.`);
1✔
257
      }
258
    }
259
    return commands;
843✔
260
  }
261
  /**
262
   * Return a wrapper around this parser which applies the transformation on parsed commands.
263
   */
264
  transform(transform: TransformFunction) {
265
    const result = Object.create(this, {
1✔
266
      parse: {
267
        value(chunk: string, commands: SVGCommand[] = []) {
3✔
268
          const parsedCommands = Object.getPrototypeOf(this).parse.call(
4✔
269
            this,
270
            chunk,
271
          );
272
          for (const c of parsedCommands) {
4✔
273
            const cT = transform(c);
2✔
274
            if (Array.isArray(cT)) {
2!
275
              commands.push(...cT);
×
276
            } else {
277
              commands.push(cT);
2✔
278
            }
279
          }
280
          return commands;
4✔
281
        },
282
      },
283
    });
284
    return result as this;
1✔
285
  }
286
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc