we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 509 lines 16 kB view raw
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}