//! Vector-path literals shared by both surface languages: the s-expression //! form `(path (move x y) (line x y) (cubic …) (close))` and the //! `(svg-path "M0 0 L10 10 …")` convenience, both producing the same //! [`PathCmd`] sequence. One parser so the two spellings can never drift. use crate::diagnostics::Diagnostic; use crate::ir::PathCmd; use crate::sexpr::{Sexpr, Span}; /// Parse a `(path …)` or `(svg-path "…")` list form. `items` is the whole /// list including the head symbol; returns `None` (not an error) when the /// head is neither, so callers can fall through to other value kinds. pub fn parse_path_form(items: &[Sexpr], span: Span) -> Option, Diagnostic>> { match items.first().and_then(Sexpr::as_symbol) { Some("path") => Some(parse_sexpr_path(&items[1..])), Some("svg-path") => Some(parse_svg_form(&items[1..], span)), _ => None, } } /// `(path (move x y) (line x y) (quad cx cy x y) (cubic c1x c1y c2x c2y x y) /// (close))`. fn parse_sexpr_path(cmds: &[Sexpr]) -> Result, Diagnostic> { let mut out = Vec::with_capacity(cmds.len()); for cmd in cmds { let Some(list) = cmd.as_list() else { return Err(Diagnostic::new( "a path command is a list like `(move x y)` or `(close)`", cmd.span(), )); }; let Some(op) = list.first().and_then(Sexpr::as_symbol) else { return Err(Diagnostic::new( "a path command must start with `move`, `line`, `quad`, \ `cubic`, or `close`", cmd.span(), )); }; let nums = |n: usize| -> Result, Diagnostic> { if list.len() != n + 1 { return Err(Diagnostic::new( format!("`{op}` takes {n} coordinate(s), found {}", list.len() - 1), cmd.span(), )); } list[1..].iter().map(number).collect() }; out.push(match op { "move" => { let c = nums(2)?; PathCmd::Move(c[0], c[1]) } "line" => { let c = nums(2)?; PathCmd::Line(c[0], c[1]) } "quad" => { let c = nums(4)?; PathCmd::Quad(c[0], c[1], c[2], c[3]) } "cubic" => { let c = nums(6)?; PathCmd::Cubic(c[0], c[1], c[2], c[3], c[4], c[5]) } "close" => { nums(0)?; PathCmd::Close } other => { return Err(Diagnostic::new( format!( "unknown path command `{other}`; expected move, line, quad, cubic, or close" ), cmd.span(), )); } }); } Ok(out) } fn number(value: &Sexpr) -> Result { match value { Sexpr::Int(v, _) => Ok(*v as f64), Sexpr::Float(v, _) => Ok(*v), other => Err(Diagnostic::new( format!( "expected a path coordinate number, found {}", other.kind_name() ), other.span(), )), } } /// `(svg-path "…")` — exactly one string argument, an SVG path-data string. fn parse_svg_form(args: &[Sexpr], span: Span) -> Result, Diagnostic> { let [Sexpr::Str(data, str_span)] = args else { return Err(Diagnostic::new( "`svg-path` takes exactly one string of SVG path data", span, )); }; parse_svg_data(data, *str_span) } /// A pragmatic SVG path-data parser covering the absolute and relative /// command set guiduck's tokens use: M/L/H/V/C/Q/Z (and lowercase relative /// variants). Enough for hand-written and design-tool-exported marks; not a /// full SVG arc/smooth-curve implementation (A/S/T are rejected with a /// pointed message rather than silently mis-drawn). fn parse_svg_data(data: &str, span: Span) -> Result, Diagnostic> { let mut lex = SvgLexer::new(data, span); let mut out = Vec::new(); let (mut cx, mut cy) = (0.0_f64, 0.0_f64); let (mut sx, mut sy) = (0.0_f64, 0.0_f64); let mut cmd = 0_u8; loop { match lex.peek()? { Peek::End => break, Peek::Command(c) => { lex.take_command(); if c.eq_ignore_ascii_case(&b'Z') { out.push(PathCmd::Close); cx = sx; cy = sy; cmd = 0; continue; } // Set the command and fall through to read its first operand // set (an M's first pair really is a Move). cmd = c; } // A number where a command is expected repeats the last command // (an implicit M repeats as L, per the SVG grammar). Peek::Number => { if cmd == 0 { return Err(Diagnostic::new( "SVG path data must start with a move command (M or m)", span, )); } else if cmd == b'M' { cmd = b'L'; } else if cmd == b'm' { cmd = b'l'; } } } let rel = cmd.is_ascii_lowercase(); match cmd.to_ascii_uppercase() { b'M' => { let (x, y) = lex.point(&mut cx, &mut cy, rel)?; out.push(PathCmd::Move(x, y)); sx = x; sy = y; } b'L' => { let (x, y) = lex.point(&mut cx, &mut cy, rel)?; out.push(PathCmd::Line(x, y)); } b'H' => { let v = lex.number()?; cx = if rel { cx + v } else { v }; out.push(PathCmd::Line(cx, cy)); } b'V' => { let v = lex.number()?; cy = if rel { cy + v } else { v }; out.push(PathCmd::Line(cx, cy)); } b'Q' => { let (qx, qy) = lex.control(cx, cy, rel)?; let (x, y) = lex.point(&mut cx, &mut cy, rel)?; out.push(PathCmd::Quad(qx, qy, x, y)); } b'C' => { let (a, b) = lex.control(cx, cy, rel)?; let (c, d) = lex.control(cx, cy, rel)?; let (x, y) = lex.point(&mut cx, &mut cy, rel)?; out.push(PathCmd::Cubic(a, b, c, d, x, y)); } other => { return Err(Diagnostic::new( format!( "unsupported SVG path command `{}`; use the s-expr `(path …)` \ form or M/L/H/V/C/Q/Z", other as char ), span, )); } } } Ok(out) } enum Peek { Command(u8), Number, End, } /// A tiny scanner over SVG path data: whitespace/comma separated numbers /// interleaved with single-letter commands. struct SvgLexer<'a> { rest: &'a str, span: Span, } impl<'a> SvgLexer<'a> { fn new(data: &'a str, span: Span) -> Self { Self { rest: data, span } } fn skip_sep(&mut self) { self.rest = self .rest .trim_start_matches(|c: char| c.is_whitespace() || c == ','); } fn peek(&mut self) -> Result { self.skip_sep(); match self.rest.chars().next() { None => Ok(Peek::End), Some(c) if c.is_ascii_alphabetic() => Ok(Peek::Command(c as u8)), Some(_) => Ok(Peek::Number), } } fn take_command(&mut self) { self.rest = &self.rest[1..]; } fn number(&mut self) -> Result { self.skip_sep(); let end = self .rest .find(|c: char| { c.is_whitespace() || c == ',' || (c.is_ascii_alphabetic() && c != 'e' && c != 'E') }) .unwrap_or(self.rest.len()); let (tok, rest) = self.rest.split_at(end); self.rest = rest; tok.parse::().map_err(|_| { Diagnostic::new(format!("`{tok}` is not a valid SVG path number"), self.span) }) } /// Read an (x, y) pair and advance the current point. fn point(&mut self, cx: &mut f64, cy: &mut f64, rel: bool) -> Result<(f64, f64), Diagnostic> { let x = self.number()?; let y = self.number()?; let (ax, ay) = if rel { (*cx + x, *cy + y) } else { (x, y) }; *cx = ax; *cy = ay; Ok((ax, ay)) } /// Read an (x, y) control point resolved against the current point /// without moving it. fn control(&mut self, cx: f64, cy: f64, rel: bool) -> Result<(f64, f64), Diagnostic> { let x = self.number()?; let y = self.number()?; Ok(if rel { (cx + x, cy + y) } else { (x, y) }) } } #[cfg(test)] mod tests;