we (web engine): Experimental web browser project to understand the limits of Claude
1//! SVG path data parser.
2//!
3//! Parses the `d` attribute of `<path>` elements into a sequence of
4//! path commands per the SVG specification.
5
6/// A single SVG path command.
7#[derive(Debug, Clone, PartialEq)]
8pub enum PathCommand {
9 /// Move to (x, y). Absolute.
10 MoveTo(f32, f32),
11 /// Line to (x, y). Absolute.
12 LineTo(f32, f32),
13 /// Horizontal line to x. Absolute.
14 HorizontalLineTo(f32),
15 /// Vertical line to y. Absolute.
16 VerticalLineTo(f32),
17 /// Cubic bezier to (x, y) with control points (x1, y1) and (x2, y2). Absolute.
18 CubicTo(f32, f32, f32, f32, f32, f32),
19 /// Smooth cubic bezier to (x, y) with control point (x2, y2). Absolute.
20 SmoothCubicTo(f32, f32, f32, f32),
21 /// Quadratic bezier to (x, y) with control point (x1, y1). Absolute.
22 QuadraticTo(f32, f32, f32, f32),
23 /// Smooth quadratic bezier to (x, y). Absolute.
24 SmoothQuadraticTo(f32, f32),
25 /// Arc to (x, y) with radii (rx, ry), x-axis rotation, large-arc flag, sweep flag. Absolute.
26 ArcTo(f32, f32, f32, bool, bool, f32, f32),
27 /// Close path.
28 Close,
29}
30
31/// Parse an SVG path data string into a list of absolute path commands.
32pub fn parse_path_data(d: &str) -> Vec<PathCommand> {
33 let mut commands = Vec::new();
34 let mut parser = PathParser::new(d);
35 parser.parse(&mut commands);
36 commands
37}
38
39struct PathParser<'a> {
40 input: &'a [u8],
41 pos: usize,
42 // Current point (for relative → absolute conversion).
43 cx: f32,
44 cy: f32,
45 // Start of current subpath (for close).
46 sx: f32,
47 sy: f32,
48 // Last control point (for smooth curves).
49 last_cubic_cp: Option<(f32, f32)>,
50 last_quad_cp: Option<(f32, f32)>,
51}
52
53impl<'a> PathParser<'a> {
54 fn new(input: &'a str) -> Self {
55 PathParser {
56 input: input.as_bytes(),
57 pos: 0,
58 cx: 0.0,
59 cy: 0.0,
60 sx: 0.0,
61 sy: 0.0,
62 last_cubic_cp: None,
63 last_quad_cp: None,
64 }
65 }
66
67 fn parse(&mut self, commands: &mut Vec<PathCommand>) {
68 while self.pos < self.input.len() {
69 self.skip_whitespace_and_commas();
70 if self.pos >= self.input.len() {
71 break;
72 }
73
74 let cmd = self.input[self.pos];
75 if cmd.is_ascii_alphabetic() {
76 self.pos += 1;
77 self.parse_command(cmd, commands);
78 } else {
79 // Implicit repeat of previous command.
80 break;
81 }
82 }
83 }
84
85 fn parse_command(&mut self, cmd: u8, commands: &mut Vec<PathCommand>) {
86 let relative = cmd.is_ascii_lowercase();
87
88 match cmd.to_ascii_uppercase() {
89 b'M' => self.parse_move_to(relative, commands),
90 b'L' => self.parse_line_to(relative, commands),
91 b'H' => self.parse_horizontal(relative, commands),
92 b'V' => self.parse_vertical(relative, commands),
93 b'C' => self.parse_cubic(relative, commands),
94 b'S' => self.parse_smooth_cubic(relative, commands),
95 b'Q' => self.parse_quadratic(relative, commands),
96 b'T' => self.parse_smooth_quadratic(relative, commands),
97 b'A' => self.parse_arc(relative, commands),
98 b'Z' => {
99 commands.push(PathCommand::Close);
100 self.cx = self.sx;
101 self.cy = self.sy;
102 self.last_cubic_cp = None;
103 self.last_quad_cp = None;
104 }
105 _ => {}
106 }
107 }
108
109 fn parse_move_to(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
110 let mut first = true;
111 while let Some((x, y)) = self.try_parse_coordinate_pair() {
112 let (ax, ay) = if relative {
113 (self.cx + x, self.cy + y)
114 } else {
115 (x, y)
116 };
117 if first {
118 commands.push(PathCommand::MoveTo(ax, ay));
119 self.sx = ax;
120 self.sy = ay;
121 first = false;
122 } else {
123 // Subsequent coordinate pairs are treated as LineTo.
124 commands.push(PathCommand::LineTo(ax, ay));
125 }
126 self.cx = ax;
127 self.cy = ay;
128 self.last_cubic_cp = None;
129 self.last_quad_cp = None;
130 }
131 }
132
133 fn parse_line_to(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
134 while let Some((x, y)) = self.try_parse_coordinate_pair() {
135 let (ax, ay) = if relative {
136 (self.cx + x, self.cy + y)
137 } else {
138 (x, y)
139 };
140 commands.push(PathCommand::LineTo(ax, ay));
141 self.cx = ax;
142 self.cy = ay;
143 self.last_cubic_cp = None;
144 self.last_quad_cp = None;
145 }
146 }
147
148 fn parse_horizontal(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
149 while let Some(x) = self.try_parse_number() {
150 let ax = if relative { self.cx + x } else { x };
151 commands.push(PathCommand::HorizontalLineTo(ax));
152 self.cx = ax;
153 self.last_cubic_cp = None;
154 self.last_quad_cp = None;
155 }
156 }
157
158 fn parse_vertical(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
159 while let Some(y) = self.try_parse_number() {
160 let ay = if relative { self.cy + y } else { y };
161 commands.push(PathCommand::VerticalLineTo(ay));
162 self.cy = ay;
163 self.last_cubic_cp = None;
164 self.last_quad_cp = None;
165 }
166 }
167
168 fn parse_cubic(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
169 while let Some(coords) = self.try_parse_n_numbers(6) {
170 let (x1, y1, x2, y2, x, y) = if relative {
171 (
172 self.cx + coords[0],
173 self.cy + coords[1],
174 self.cx + coords[2],
175 self.cy + coords[3],
176 self.cx + coords[4],
177 self.cy + coords[5],
178 )
179 } else {
180 (
181 coords[0], coords[1], coords[2], coords[3], coords[4], coords[5],
182 )
183 };
184 commands.push(PathCommand::CubicTo(x1, y1, x2, y2, x, y));
185 self.cx = x;
186 self.cy = y;
187 self.last_cubic_cp = Some((x2, y2));
188 self.last_quad_cp = None;
189 }
190 }
191
192 fn parse_smooth_cubic(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
193 while let Some(coords) = self.try_parse_n_numbers(4) {
194 let (x2, y2, x, y) = if relative {
195 (
196 self.cx + coords[0],
197 self.cy + coords[1],
198 self.cx + coords[2],
199 self.cy + coords[3],
200 )
201 } else {
202 (coords[0], coords[1], coords[2], coords[3])
203 };
204 commands.push(PathCommand::SmoothCubicTo(x2, y2, x, y));
205 // Reflect the last cubic control point.
206 self.last_cubic_cp = Some((x2, y2));
207 self.cx = x;
208 self.cy = y;
209 self.last_quad_cp = None;
210 }
211 }
212
213 fn parse_quadratic(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
214 while let Some(coords) = self.try_parse_n_numbers(4) {
215 let (x1, y1, x, y) = if relative {
216 (
217 self.cx + coords[0],
218 self.cy + coords[1],
219 self.cx + coords[2],
220 self.cy + coords[3],
221 )
222 } else {
223 (coords[0], coords[1], coords[2], coords[3])
224 };
225 commands.push(PathCommand::QuadraticTo(x1, y1, x, y));
226 self.cx = x;
227 self.cy = y;
228 self.last_quad_cp = Some((x1, y1));
229 self.last_cubic_cp = None;
230 }
231 }
232
233 fn parse_smooth_quadratic(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
234 while let Some((x, y)) = self.try_parse_coordinate_pair() {
235 let (ax, ay) = if relative {
236 (self.cx + x, self.cy + y)
237 } else {
238 (x, y)
239 };
240 commands.push(PathCommand::SmoothQuadraticTo(ax, ay));
241 // Reflect the last quadratic control point.
242 let cp = self
243 .last_quad_cp
244 .map(|(cpx, cpy)| (2.0 * self.cx - cpx, 2.0 * self.cy - cpy))
245 .unwrap_or((self.cx, self.cy));
246 self.last_quad_cp = Some(cp);
247 self.cx = ax;
248 self.cy = ay;
249 self.last_cubic_cp = None;
250 }
251 }
252
253 fn parse_arc(&mut self, relative: bool, commands: &mut Vec<PathCommand>) {
254 loop {
255 self.skip_whitespace_and_commas();
256 let Some(rx) = self.try_parse_number() else {
257 break;
258 };
259 let Some(ry) = self.try_parse_number() else {
260 break;
261 };
262 let Some(x_rot) = self.try_parse_number() else {
263 break;
264 };
265 let Some(large_arc) = self.try_parse_flag() else {
266 break;
267 };
268 let Some(sweep) = self.try_parse_flag() else {
269 break;
270 };
271 let Some((x, y)) = self.try_parse_coordinate_pair() else {
272 break;
273 };
274 let (ax, ay) = if relative {
275 (self.cx + x, self.cy + y)
276 } else {
277 (x, y)
278 };
279 commands.push(PathCommand::ArcTo(rx, ry, x_rot, large_arc, sweep, ax, ay));
280 self.cx = ax;
281 self.cy = ay;
282 self.last_cubic_cp = None;
283 self.last_quad_cp = None;
284 }
285 }
286
287 // --- Low-level parsing helpers ---
288
289 fn skip_whitespace_and_commas(&mut self) {
290 while self.pos < self.input.len() {
291 let b = self.input[self.pos];
292 if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' || b == b',' {
293 self.pos += 1;
294 } else {
295 break;
296 }
297 }
298 }
299
300 fn try_parse_number(&mut self) -> Option<f32> {
301 self.skip_whitespace_and_commas();
302 if self.pos >= self.input.len() {
303 return None;
304 }
305
306 let start = self.pos;
307
308 // Optional sign.
309 if self.pos < self.input.len()
310 && (self.input[self.pos] == b'+' || self.input[self.pos] == b'-')
311 {
312 self.pos += 1;
313 }
314
315 let mut has_digits = false;
316
317 // Integer part.
318 while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() {
319 self.pos += 1;
320 has_digits = true;
321 }
322
323 // Fractional part.
324 if self.pos < self.input.len() && self.input[self.pos] == b'.' {
325 self.pos += 1;
326 while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() {
327 self.pos += 1;
328 has_digits = true;
329 }
330 }
331
332 if !has_digits {
333 self.pos = start;
334 return None;
335 }
336
337 // Exponent.
338 if self.pos < self.input.len()
339 && (self.input[self.pos] == b'e' || self.input[self.pos] == b'E')
340 {
341 self.pos += 1;
342 if self.pos < self.input.len()
343 && (self.input[self.pos] == b'+' || self.input[self.pos] == b'-')
344 {
345 self.pos += 1;
346 }
347 while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() {
348 self.pos += 1;
349 }
350 }
351
352 let s = std::str::from_utf8(&self.input[start..self.pos]).ok()?;
353 s.parse::<f32>().ok()
354 }
355
356 fn try_parse_coordinate_pair(&mut self) -> Option<(f32, f32)> {
357 let saved = self.pos;
358 let x = self.try_parse_number()?;
359 let y = match self.try_parse_number() {
360 Some(y) => y,
361 None => {
362 self.pos = saved;
363 return None;
364 }
365 };
366 Some((x, y))
367 }
368
369 fn try_parse_n_numbers(&mut self, n: usize) -> Option<Vec<f32>> {
370 let saved = self.pos;
371 let mut nums = Vec::with_capacity(n);
372 for _ in 0..n {
373 match self.try_parse_number() {
374 Some(v) => nums.push(v),
375 None => {
376 self.pos = saved;
377 return None;
378 }
379 }
380 }
381 Some(nums)
382 }
383
384 fn try_parse_flag(&mut self) -> Option<bool> {
385 self.skip_whitespace_and_commas();
386 if self.pos >= self.input.len() {
387 return None;
388 }
389 match self.input[self.pos] {
390 b'0' => {
391 self.pos += 1;
392 Some(false)
393 }
394 b'1' => {
395 self.pos += 1;
396 Some(true)
397 }
398 _ => None,
399 }
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn parse_move_and_line() {
409 let cmds = parse_path_data("M 10 20 L 30 40");
410 assert_eq!(cmds.len(), 2);
411 assert_eq!(cmds[0], PathCommand::MoveTo(10.0, 20.0));
412 assert_eq!(cmds[1], PathCommand::LineTo(30.0, 40.0));
413 }
414
415 #[test]
416 fn parse_relative_line() {
417 let cmds = parse_path_data("M 10 10 l 5 5");
418 assert_eq!(cmds.len(), 2);
419 assert_eq!(cmds[0], PathCommand::MoveTo(10.0, 10.0));
420 assert_eq!(cmds[1], PathCommand::LineTo(15.0, 15.0));
421 }
422
423 #[test]
424 fn parse_horizontal_vertical() {
425 let cmds = parse_path_data("M 0 0 H 50 V 50");
426 assert_eq!(cmds.len(), 3);
427 assert_eq!(cmds[1], PathCommand::HorizontalLineTo(50.0));
428 assert_eq!(cmds[2], PathCommand::VerticalLineTo(50.0));
429 }
430
431 #[test]
432 fn parse_cubic_bezier() {
433 let cmds = parse_path_data("M 0 0 C 10 20 30 40 50 60");
434 assert_eq!(cmds.len(), 2);
435 assert_eq!(
436 cmds[1],
437 PathCommand::CubicTo(10.0, 20.0, 30.0, 40.0, 50.0, 60.0)
438 );
439 }
440
441 #[test]
442 fn parse_quadratic_bezier() {
443 let cmds = parse_path_data("M 0 0 Q 10 20 30 40");
444 assert_eq!(cmds.len(), 2);
445 assert_eq!(cmds[1], PathCommand::QuadraticTo(10.0, 20.0, 30.0, 40.0));
446 }
447
448 #[test]
449 fn parse_arc() {
450 let cmds = parse_path_data("M 0 0 A 25 25 0 0 1 50 50");
451 assert_eq!(cmds.len(), 2);
452 assert_eq!(
453 cmds[1],
454 PathCommand::ArcTo(25.0, 25.0, 0.0, false, true, 50.0, 50.0)
455 );
456 }
457
458 #[test]
459 fn parse_close() {
460 let cmds = parse_path_data("M 0 0 L 50 0 L 50 50 Z");
461 assert_eq!(cmds.len(), 4);
462 assert_eq!(cmds[3], PathCommand::Close);
463 }
464
465 #[test]
466 fn parse_implicit_lineto_after_moveto() {
467 let cmds = parse_path_data("M 0 0 10 20 30 40");
468 assert_eq!(cmds.len(), 3);
469 assert_eq!(cmds[0], PathCommand::MoveTo(0.0, 0.0));
470 assert_eq!(cmds[1], PathCommand::LineTo(10.0, 20.0));
471 assert_eq!(cmds[2], PathCommand::LineTo(30.0, 40.0));
472 }
473
474 #[test]
475 fn parse_compact_notation() {
476 let cmds = parse_path_data("M0,0L10,20");
477 assert_eq!(cmds.len(), 2);
478 assert_eq!(cmds[0], PathCommand::MoveTo(0.0, 0.0));
479 assert_eq!(cmds[1], PathCommand::LineTo(10.0, 20.0));
480 }
481
482 #[test]
483 fn parse_negative_numbers() {
484 let cmds = parse_path_data("M-10-20L-30-40");
485 assert_eq!(cmds.len(), 2);
486 assert_eq!(cmds[0], PathCommand::MoveTo(-10.0, -20.0));
487 assert_eq!(cmds[1], PathCommand::LineTo(-30.0, -40.0));
488 }
489
490 #[test]
491 fn parse_empty_string() {
492 let cmds = parse_path_data("");
493 assert!(cmds.is_empty());
494 }
495
496 #[test]
497 fn parse_smooth_cubic() {
498 let cmds = parse_path_data("M 0 0 S 10 20 30 40");
499 assert_eq!(cmds.len(), 2);
500 assert_eq!(cmds[1], PathCommand::SmoothCubicTo(10.0, 20.0, 30.0, 40.0));
501 }
502
503 #[test]
504 fn parse_smooth_quadratic() {
505 let cmds = parse_path_data("M 0 0 T 30 40");
506 assert_eq!(cmds.len(), 2);
507 assert_eq!(cmds[1], PathCommand::SmoothQuadraticTo(30.0, 40.0));
508 }
509}