Skip to main content

dioxuscut_paths/
parser.rs

1//! SVG path parser and serializer.
2
3use crate::types::Instruction;
4use thiserror::Error;
5
6#[derive(Error, Debug, PartialEq)]
7pub enum PathParseError {
8    #[error("Invalid SVG command '{0}' at index {1}")]
9    InvalidCommand(char, usize),
10    #[error("Unexpected end of path string")]
11    UnexpectedEnd,
12    #[error("Failed to parse number '{0}'")]
13    ParseNumberError(String),
14}
15
16/// Parses an SVG path `d` string into a list of [`Instruction`]s.
17pub fn parse_path(d: &str) -> Result<Vec<Instruction>, PathParseError> {
18    let mut instructions = Vec::new();
19    let tokens = tokenize(d)?;
20    let mut idx = 0;
21
22    let mut current_x = 0.0;
23    let mut current_y = 0.0;
24
25    while idx < tokens.len() {
26        match tokens[idx].as_str() {
27            "M" => {
28                idx += 1;
29                let x = parse_num(&tokens, &mut idx)?;
30                let y = parse_num(&tokens, &mut idx)?;
31                current_x = x;
32                current_y = y;
33                instructions.push(Instruction::MoveTo { x, y });
34            }
35            "m" => {
36                idx += 1;
37                let dx = parse_num(&tokens, &mut idx)?;
38                let dy = parse_num(&tokens, &mut idx)?;
39                current_x += dx;
40                current_y += dy;
41                instructions.push(Instruction::MoveTo {
42                    x: current_x,
43                    y: current_y,
44                });
45            }
46            "L" => {
47                idx += 1;
48                let x = parse_num(&tokens, &mut idx)?;
49                let y = parse_num(&tokens, &mut idx)?;
50                current_x = x;
51                current_y = y;
52                instructions.push(Instruction::LineTo { x, y });
53            }
54            "l" => {
55                idx += 1;
56                let dx = parse_num(&tokens, &mut idx)?;
57                let dy = parse_num(&tokens, &mut idx)?;
58                current_x += dx;
59                current_y += dy;
60                instructions.push(Instruction::LineTo {
61                    x: current_x,
62                    y: current_y,
63                });
64            }
65            "H" => {
66                idx += 1;
67                let x = parse_num(&tokens, &mut idx)?;
68                current_x = x;
69                instructions.push(Instruction::LineTo {
70                    x: current_x,
71                    y: current_y,
72                });
73            }
74            "h" => {
75                idx += 1;
76                let dx = parse_num(&tokens, &mut idx)?;
77                current_x += dx;
78                instructions.push(Instruction::LineTo {
79                    x: current_x,
80                    y: current_y,
81                });
82            }
83            "V" => {
84                idx += 1;
85                let y = parse_num(&tokens, &mut idx)?;
86                current_y = y;
87                instructions.push(Instruction::LineTo {
88                    x: current_x,
89                    y: current_y,
90                });
91            }
92            "v" => {
93                idx += 1;
94                let dy = parse_num(&tokens, &mut idx)?;
95                current_y += dy;
96                instructions.push(Instruction::LineTo {
97                    x: current_x,
98                    y: current_y,
99                });
100            }
101            "C" => {
102                idx += 1;
103                let x1 = parse_num(&tokens, &mut idx)?;
104                let y1 = parse_num(&tokens, &mut idx)?;
105                let x2 = parse_num(&tokens, &mut idx)?;
106                let y2 = parse_num(&tokens, &mut idx)?;
107                let x = parse_num(&tokens, &mut idx)?;
108                let y = parse_num(&tokens, &mut idx)?;
109                current_x = x;
110                current_y = y;
111                instructions.push(Instruction::CubicCurveTo {
112                    x1,
113                    y1,
114                    x2,
115                    y2,
116                    x,
117                    y,
118                });
119            }
120            "c" => {
121                idx += 1;
122                let dx1 = parse_num(&tokens, &mut idx)?;
123                let dy1 = parse_num(&tokens, &mut idx)?;
124                let dx2 = parse_num(&tokens, &mut idx)?;
125                let dy2 = parse_num(&tokens, &mut idx)?;
126                let dx = parse_num(&tokens, &mut idx)?;
127                let dy = parse_num(&tokens, &mut idx)?;
128                let x1 = current_x + dx1;
129                let y1 = current_y + dy1;
130                let x2 = current_x + dx2;
131                let y2 = current_y + dy2;
132                let x = current_x + dx;
133                let y = current_y + dy;
134                current_x = x;
135                current_y = y;
136                instructions.push(Instruction::CubicCurveTo {
137                    x1,
138                    y1,
139                    x2,
140                    y2,
141                    x,
142                    y,
143                });
144            }
145            "Q" => {
146                idx += 1;
147                let x1 = parse_num(&tokens, &mut idx)?;
148                let y1 = parse_num(&tokens, &mut idx)?;
149                let x = parse_num(&tokens, &mut idx)?;
150                let y = parse_num(&tokens, &mut idx)?;
151                current_x = x;
152                current_y = y;
153                instructions.push(Instruction::QuadCurveTo { x1, y1, x, y });
154            }
155            "q" => {
156                idx += 1;
157                let dx1 = parse_num(&tokens, &mut idx)?;
158                let dy1 = parse_num(&tokens, &mut idx)?;
159                let dx = parse_num(&tokens, &mut idx)?;
160                let dy = parse_num(&tokens, &mut idx)?;
161                let x1 = current_x + dx1;
162                let y1 = current_y + dy1;
163                let x = current_x + dx;
164                let y = current_y + dy;
165                current_x = x;
166                current_y = y;
167                instructions.push(Instruction::QuadCurveTo { x1, y1, x, y });
168            }
169            "Z" | "z" => {
170                idx += 1;
171                instructions.push(Instruction::ClosePath);
172            }
173            other => {
174                return Err(PathParseError::ParseNumberError(other.to_string()));
175            }
176        }
177    }
178
179    Ok(instructions)
180}
181
182fn tokenize(d: &str) -> Result<Vec<String>, PathParseError> {
183    let mut tokens = Vec::new();
184    let mut current_token = String::new();
185
186    let chars: Vec<char> = d.chars().collect();
187    let mut i = 0;
188
189    while i < chars.len() {
190        let c = chars[i];
191        if c.is_alphabetic() {
192            if !current_token.trim().is_empty() {
193                tokens.push(current_token.trim().to_string());
194                current_token = String::new();
195            }
196            tokens.push(c.to_string());
197            i += 1;
198        } else if c == ',' || c.is_whitespace() {
199            if !current_token.trim().is_empty() {
200                tokens.push(current_token.trim().to_string());
201                current_token = String::new();
202            }
203            i += 1;
204        } else if c == '-'
205            && !current_token.is_empty()
206            && !current_token.ends_with('e')
207            && !current_token.ends_with('E')
208        {
209            tokens.push(current_token.trim().to_string());
210            current_token = String::new();
211            current_token.push(c);
212            i += 1;
213        } else {
214            current_token.push(c);
215            i += 1;
216        }
217    }
218
219    if !current_token.trim().is_empty() {
220        tokens.push(current_token.trim().to_string());
221    }
222
223    Ok(tokens)
224}
225
226fn parse_num(tokens: &[String], idx: &mut usize) -> Result<f64, PathParseError> {
227    if *idx >= tokens.len() {
228        return Err(PathParseError::UnexpectedEnd);
229    }
230    let val = tokens[*idx]
231        .parse::<f64>()
232        .map_err(|_| PathParseError::ParseNumberError(tokens[*idx].clone()))?;
233    *idx += 1;
234    Ok(val)
235}
236
237/// Serializes instructions back into a standardized SVG `d` path string.
238pub fn serialize_instructions(instructions: &[Instruction]) -> String {
239    let mut out = Vec::new();
240
241    for inst in instructions {
242        match inst {
243            Instruction::MoveTo { x, y } => out.push(format!("M {x:.4} {y:.4}")),
244            Instruction::LineTo { x, y } => out.push(format!("L {x:.4} {y:.4}")),
245            Instruction::CubicCurveTo {
246                x1,
247                y1,
248                x2,
249                y2,
250                x,
251                y,
252            } => out.push(format!("C {x1:.4} {y1:.4} {x2:.4} {y2:.4} {x:.4} {y:.4}")),
253            Instruction::QuadCurveTo { x1, y1, x, y } => {
254                out.push(format!("Q {x1:.4} {y1:.4} {x:.4} {y:.4}"))
255            }
256            Instruction::ClosePath => out.push("Z".to_string()),
257        }
258    }
259
260    out.join(" ")
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_parse_and_serialize_line() {
269        let d = "M 10 20 L 100 200 Z";
270        let insts = parse_path(d).unwrap();
271        assert_eq!(insts.len(), 3);
272        assert_eq!(insts[0], Instruction::MoveTo { x: 10.0, y: 20.0 });
273        assert_eq!(insts[1], Instruction::LineTo { x: 100.0, y: 200.0 });
274        assert_eq!(insts[2], Instruction::ClosePath);
275
276        let res = serialize_instructions(&insts);
277        assert_eq!(res, "M 10.0000 20.0000 L 100.0000 200.0000 Z");
278    }
279}