animate/path/
parser.rs

1use std::str::FromStr;
2
3use super::{Error, Path, PathSegment, Result, Stream};
4
5impl FromStr for Path {
6    type Err = Error;
7
8    /// Parses a string as `Path`.
9    ///
10    /// # Errors
11    ///
12    /// Will always return `Ok`. If an error will occur during the parsing,
13    /// will return current segments. Even if there are none of them.
14    fn from_str(text: &str) -> Result<Self> {
15        let mut data = Vec::new();
16        for token in PathParser::from(text) {
17            match token {
18                Ok(token) => data.push(token),
19                Err(_) => break,
20            }
21        }
22
23        Ok(Path(data))
24    }
25}
26
27/// A pull-based [path data] parser.
28///
29/// # Errors
30///
31/// - Most of the `Error` types can occur.
32///
33/// # Notes
34///
35/// The library does not support implicit commands, so they will be converted to an explicit one.
36/// It mostly affects an implicit MoveTo, which will be converted, according to the spec,
37/// into explicit LineTo.
38///
39/// Example: `M 10 20 30 40 50 60` -> `M 10 20 L 30 40 L 50 60`
40///
41/// # Example
42///
43// /// ```
44// /// use svgtypes::{PathParser, PathSegment};
45// ///
46// /// let mut segments = Vec::new();
47// /// for segment in PathParser::from("M10-20l30.1.5.1-20z") {
48// ///     segments.push(segment.unwrap());
49// /// }
50// ///
51// /// assert_eq!(segments, &[
52// ///     PathSegment::MoveTo { abs: true, x: 10.0, y: -20.0 },
53// ///     PathSegment::LineTo { abs: false, x: 30.1, y: 0.5 },
54// ///     PathSegment::LineTo { abs: false, x: 0.1, y: -20.0 },
55// ///     PathSegment::ClosePath { abs: false },
56// /// ]);
57// /// ```
58///
59/// [path data]: https://www.w3.org/TR/SVG11/paths.html#PathData
60#[derive(Clone, Copy, PartialEq, Debug)]
61pub struct PathParser<'a> {
62    stream: Stream<'a>,
63    prev_cmd: Option<u8>,
64}
65
66impl<'a> From<&'a str> for PathParser<'a> {
67    #[inline]
68    fn from(v: &'a str) -> Self {
69        PathParser {
70            stream: Stream::from(v),
71            prev_cmd: None,
72        }
73    }
74}
75
76impl<'a> Iterator for PathParser<'a> {
77    type Item = Result<PathSegment>;
78
79    #[inline]
80    fn next(&mut self) -> Option<Self::Item> {
81        let s = &mut self.stream;
82
83        s.skip_spaces();
84
85        if s.at_end() {
86            return None;
87        }
88
89        let res = next_impl(s, &mut self.prev_cmd);
90        if res.is_err() {
91            s.jump_to_end();
92        }
93
94        Some(res)
95    }
96}
97
98fn next_impl(s: &mut Stream, prev_cmd: &mut Option<u8>) -> Result<PathSegment> {
99    let start = s.pos();
100
101    let has_prev_cmd = prev_cmd.is_some();
102    let first_char = s.curr_byte_unchecked();
103
104    if !has_prev_cmd && !is_cmd(first_char) {
105        return Err(Error::UnexpectedData(s.calc_char_pos_at(start)));
106    }
107
108    if !has_prev_cmd {
109        if !matches!(first_char, b'M' | b'm') {
110            // The first segment must be a MoveTo.
111            return Err(Error::UnexpectedData(s.calc_char_pos_at(start)));
112        }
113    }
114
115    // TODO: simplify
116    let is_implicit_move_to;
117    let cmd: u8;
118    if is_cmd(first_char) {
119        is_implicit_move_to = false;
120        cmd = first_char;
121        s.advance(1);
122    } else if is_number_start(first_char) && has_prev_cmd {
123        // unwrap is safe, because we checked 'has_prev_cmd'
124        let p_cmd = prev_cmd.unwrap();
125
126        if p_cmd == b'Z' || p_cmd == b'z' {
127            // ClosePath cannot be followed by a number.
128            return Err(Error::UnexpectedData(s.calc_char_pos_at(start)));
129        }
130
131        if p_cmd == b'M' || p_cmd == b'm' {
132            // 'If a moveto is followed by multiple pairs of coordinates,
133            // the subsequent pairs are treated as implicit lineto commands.'
134            // So we parse them as LineTo.
135            is_implicit_move_to = true;
136            cmd = if is_absolute(p_cmd) { b'L' } else { b'l' };
137        } else {
138            is_implicit_move_to = false;
139            cmd = p_cmd;
140        }
141    } else {
142        return Err(Error::UnexpectedData(s.calc_char_pos_at(start)));
143    }
144
145    let cmdl = to_relative(cmd);
146    let absolute = is_absolute(cmd);
147    let token = match cmdl {
148        b'm' => PathSegment::MoveTo {
149            abs: absolute,
150            x: s.parse_list_number()?,
151            y: s.parse_list_number()?,
152        },
153        b'l' => PathSegment::LineTo {
154            abs: absolute,
155            x: s.parse_list_number()?,
156            y: s.parse_list_number()?,
157        },
158        b'h' => PathSegment::HorizontalLineTo {
159            abs: absolute,
160            x: s.parse_list_number()?,
161        },
162        b'v' => PathSegment::VerticalLineTo {
163            abs: absolute,
164            y: s.parse_list_number()?,
165        },
166        b'c' => PathSegment::CurveTo {
167            abs: absolute,
168            x1: s.parse_list_number()?,
169            y1: s.parse_list_number()?,
170            x2: s.parse_list_number()?,
171            y2: s.parse_list_number()?,
172            x: s.parse_list_number()?,
173            y: s.parse_list_number()?,
174        },
175        b's' => PathSegment::SmoothCurveTo {
176            abs: absolute,
177            x2: s.parse_list_number()?,
178            y2: s.parse_list_number()?,
179            x: s.parse_list_number()?,
180            y: s.parse_list_number()?,
181        },
182        b'q' => PathSegment::Quadratic {
183            abs: absolute,
184            x1: s.parse_list_number()?,
185            y1: s.parse_list_number()?,
186            x: s.parse_list_number()?,
187            y: s.parse_list_number()?,
188        },
189        b't' => PathSegment::SmoothQuadratic {
190            abs: absolute,
191            x: s.parse_list_number()?,
192            y: s.parse_list_number()?,
193        },
194        b'a' => {
195            // TODO: radius cannot be negative
196            PathSegment::EllipticalArc {
197                abs: absolute,
198                rx: s.parse_list_number()?,
199                ry: s.parse_list_number()?,
200                x_axis_rotation: s.parse_list_number()?,
201                large_arc: parse_flag(s)?,
202                sweep: parse_flag(s)?,
203                x: s.parse_list_number()?,
204                y: s.parse_list_number()?,
205            }
206        }
207        b'z' => PathSegment::ClosePath { abs: absolute },
208        _ => unreachable!(),
209    };
210
211    *prev_cmd = Some(if is_implicit_move_to {
212        if absolute {
213            b'M'
214        } else {
215            b'm'
216        }
217    } else {
218        cmd
219    });
220
221    Ok(token)
222}
223
224/// Returns `true` if the selected char is the command.
225#[inline]
226fn is_cmd(c: u8) -> bool {
227    match c {
228        b'M' | b'm' | b'Z' | b'z' | b'L' | b'l' | b'H' | b'h' | b'V' | b'v' | b'C' | b'c'
229        | b'S' | b's' | b'Q' | b'q' | b'T' | b't' | b'A' | b'a' => true,
230        _ => false,
231    }
232}
233
234/// Returns `true` if the selected char is the absolute command.
235#[inline]
236fn is_absolute(c: u8) -> bool {
237    debug_assert!(is_cmd(c));
238    match c {
239        b'M' | b'Z' | b'L' | b'H' | b'V' | b'C' | b'S' | b'Q' | b'T' | b'A' => true,
240        _ => false,
241    }
242}
243
244/// Converts the selected command char into the relative command char.
245#[inline]
246fn to_relative(c: u8) -> u8 {
247    debug_assert!(is_cmd(c));
248    match c {
249        b'M' => b'm',
250        b'Z' => b'z',
251        b'L' => b'l',
252        b'H' => b'h',
253        b'V' => b'v',
254        b'C' => b'c',
255        b'S' => b's',
256        b'Q' => b'q',
257        b'T' => b't',
258        b'A' => b'a',
259        _ => c,
260    }
261}
262
263#[inline]
264fn is_number_start(c: u8) -> bool {
265    matches!(c, b'0'..=b'9' | b'.' | b'-' | b'+')
266}
267
268// By the SVG spec 'large-arc' and 'sweep' must contain only one char
269// and can be written without any separators, e.g.: 10 20 30 01 10 20.
270fn parse_flag(s: &mut Stream) -> Result<bool> {
271    s.skip_spaces();
272
273    let c = s.curr_byte()?;
274    match c {
275        b'0' | b'1' => {
276            s.advance(1);
277            if s.is_curr_byte_eq(b',') {
278                s.advance(1);
279            }
280            s.skip_spaces();
281
282            Ok(c == b'1')
283        }
284        _ => Err(Error::UnexpectedData(s.calc_char_pos_at(s.pos()))),
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    macro_rules! test {
293        ($name:ident, $text:expr, $( $seg:expr ),*) => (
294            #[test]
295            fn $name() {
296                let mut s = PathParser::from($text);
297                $(
298                    assert_eq!(s.next().unwrap().unwrap(), $seg);
299                )*
300
301                if let Some(res) = s.next() {
302                    assert!(res.is_err());
303                }
304            }
305        )
306    }
307
308    test!(null, "",);
309    test!(not_a_path, "q",);
310    test!(not_a_move_to, "L 20 30",);
311    test!(
312        stop_on_err_1,
313        "M 10 20 L 30 40 L 50",
314        PathSegment::MoveTo {
315            abs: true,
316            x: 10.0,
317            y: 20.0
318        },
319        PathSegment::LineTo {
320            abs: true,
321            x: 30.0,
322            y: 40.0
323        }
324    );
325
326    test!(
327        move_to_1,
328        "M 10 20",
329        PathSegment::MoveTo {
330            abs: true,
331            x: 10.0,
332            y: 20.0
333        }
334    );
335    test!(
336        move_to_2,
337        "m 10 20",
338        PathSegment::MoveTo {
339            abs: false,
340            x: 10.0,
341            y: 20.0
342        }
343    );
344    test!(
345        move_to_3,
346        "M 10 20 30 40 50 60",
347        PathSegment::MoveTo {
348            abs: true,
349            x: 10.0,
350            y: 20.0
351        },
352        PathSegment::LineTo {
353            abs: true,
354            x: 30.0,
355            y: 40.0
356        },
357        PathSegment::LineTo {
358            abs: true,
359            x: 50.0,
360            y: 60.0
361        }
362    );
363    test!(
364        move_to_4,
365        "M 10 20 30 40 50 60 M 70 80 90 100 110 120",
366        PathSegment::MoveTo {
367            abs: true,
368            x: 10.0,
369            y: 20.0
370        },
371        PathSegment::LineTo {
372            abs: true,
373            x: 30.0,
374            y: 40.0
375        },
376        PathSegment::LineTo {
377            abs: true,
378            x: 50.0,
379            y: 60.0
380        },
381        PathSegment::MoveTo {
382            abs: true,
383            x: 70.0,
384            y: 80.0
385        },
386        PathSegment::LineTo {
387            abs: true,
388            x: 90.0,
389            y: 100.0
390        },
391        PathSegment::LineTo {
392            abs: true,
393            x: 110.0,
394            y: 120.0
395        }
396    );
397
398    test!(
399        arc_to_1,
400        "M 10 20 A 5 5 30 1 1 20 20",
401        PathSegment::MoveTo {
402            abs: true,
403            x: 10.0,
404            y: 20.0
405        },
406        PathSegment::EllipticalArc {
407            abs: true,
408            rx: 5.0,
409            ry: 5.0,
410            x_axis_rotation: 30.0,
411            large_arc: true,
412            sweep: true,
413            x: 20.0,
414            y: 20.0
415        }
416    );
417
418    test!(
419        arc_to_2,
420        "M 10 20 a 5 5 30 0 0 20 20",
421        PathSegment::MoveTo {
422            abs: true,
423            x: 10.0,
424            y: 20.0
425        },
426        PathSegment::EllipticalArc {
427            abs: false,
428            rx: 5.0,
429            ry: 5.0,
430            x_axis_rotation: 30.0,
431            large_arc: false,
432            sweep: false,
433            x: 20.0,
434            y: 20.0
435        }
436    );
437
438    test!(
439        arc_to_10,
440        "M10-20A5.5.3-4 010-.1",
441        PathSegment::MoveTo {
442            abs: true,
443            x: 10.0,
444            y: -20.0
445        },
446        PathSegment::EllipticalArc {
447            abs: true,
448            rx: 5.5,
449            ry: 0.3,
450            x_axis_rotation: -4.0,
451            large_arc: false,
452            sweep: true,
453            x: 0.0,
454            y: -0.1
455        }
456    );
457
458    test!(
459        separator_1,
460        "M 10 20 L 5 15 C 10 20 30 40 50 60",
461        PathSegment::MoveTo {
462            abs: true,
463            x: 10.0,
464            y: 20.0
465        },
466        PathSegment::LineTo {
467            abs: true,
468            x: 5.0,
469            y: 15.0
470        },
471        PathSegment::CurveTo {
472            abs: true,
473            x1: 10.0,
474            y1: 20.0,
475            x2: 30.0,
476            y2: 40.0,
477            x: 50.0,
478            y: 60.0,
479        }
480    );
481
482    test!(
483        separator_2,
484        "M 10, 20 L 5, 15 C 10, 20 30, 40 50, 60",
485        PathSegment::MoveTo {
486            abs: true,
487            x: 10.0,
488            y: 20.0
489        },
490        PathSegment::LineTo {
491            abs: true,
492            x: 5.0,
493            y: 15.0
494        },
495        PathSegment::CurveTo {
496            abs: true,
497            x1: 10.0,
498            y1: 20.0,
499            x2: 30.0,
500            y2: 40.0,
501            x: 50.0,
502            y: 60.0,
503        }
504    );
505
506    test!(
507        separator_3,
508        "M 10,20 L 5,15 C 10,20 30,40 50,60",
509        PathSegment::MoveTo {
510            abs: true,
511            x: 10.0,
512            y: 20.0
513        },
514        PathSegment::LineTo {
515            abs: true,
516            x: 5.0,
517            y: 15.0
518        },
519        PathSegment::CurveTo {
520            abs: true,
521            x1: 10.0,
522            y1: 20.0,
523            x2: 30.0,
524            y2: 40.0,
525            x: 50.0,
526            y: 60.0,
527        }
528    );
529
530    test!(
531        separator_4,
532        "M10, 20 L5, 15 C10, 20 30 40 50 60",
533        PathSegment::MoveTo {
534            abs: true,
535            x: 10.0,
536            y: 20.0
537        },
538        PathSegment::LineTo {
539            abs: true,
540            x: 5.0,
541            y: 15.0
542        },
543        PathSegment::CurveTo {
544            abs: true,
545            x1: 10.0,
546            y1: 20.0,
547            x2: 30.0,
548            y2: 40.0,
549            x: 50.0,
550            y: 60.0,
551        }
552    );
553
554    test!(
555        separator_5,
556        "M10 20V30H40V50H60Z",
557        PathSegment::MoveTo {
558            abs: true,
559            x: 10.0,
560            y: 20.0
561        },
562        PathSegment::VerticalLineTo { abs: true, y: 30.0 },
563        PathSegment::HorizontalLineTo { abs: true, x: 40.0 },
564        PathSegment::VerticalLineTo { abs: true, y: 50.0 },
565        PathSegment::HorizontalLineTo { abs: true, x: 60.0 },
566        PathSegment::ClosePath { abs: true }
567    );
568
569    test!(
570        all_segments_1,
571        "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 S 130 140 150 160
572        Q 170 180 190 200 T 210 220 A 50 50 30 1 1 230 240 Z",
573        PathSegment::MoveTo {
574            abs: true,
575            x: 10.0,
576            y: 20.0
577        },
578        PathSegment::LineTo {
579            abs: true,
580            x: 30.0,
581            y: 40.0
582        },
583        PathSegment::HorizontalLineTo { abs: true, x: 50.0 },
584        PathSegment::VerticalLineTo { abs: true, y: 60.0 },
585        PathSegment::CurveTo {
586            abs: true,
587            x1: 70.0,
588            y1: 80.0,
589            x2: 90.0,
590            y2: 100.0,
591            x: 110.0,
592            y: 120.0,
593        },
594        PathSegment::SmoothCurveTo {
595            abs: true,
596            x2: 130.0,
597            y2: 140.0,
598            x: 150.0,
599            y: 160.0,
600        },
601        PathSegment::Quadratic {
602            abs: true,
603            x1: 170.0,
604            y1: 180.0,
605            x: 190.0,
606            y: 200.0,
607        },
608        PathSegment::SmoothQuadratic {
609            abs: true,
610            x: 210.0,
611            y: 220.0
612        },
613        PathSegment::EllipticalArc {
614            abs: true,
615            rx: 50.0,
616            ry: 50.0,
617            x_axis_rotation: 30.0,
618            large_arc: true,
619            sweep: true,
620            x: 230.0,
621            y: 240.0
622        },
623        PathSegment::ClosePath { abs: true }
624    );
625
626    test!(
627        all_segments_2,
628        "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 s 130 140 150 160
629        q 170 180 190 200 t 210 220 a 50 50 30 1 1 230 240 z",
630        PathSegment::MoveTo {
631            abs: false,
632            x: 10.0,
633            y: 20.0
634        },
635        PathSegment::LineTo {
636            abs: false,
637            x: 30.0,
638            y: 40.0
639        },
640        PathSegment::HorizontalLineTo {
641            abs: false,
642            x: 50.0
643        },
644        PathSegment::VerticalLineTo {
645            abs: false,
646            y: 60.0
647        },
648        PathSegment::CurveTo {
649            abs: false,
650            x1: 70.0,
651            y1: 80.0,
652            x2: 90.0,
653            y2: 100.0,
654            x: 110.0,
655            y: 120.0,
656        },
657        PathSegment::SmoothCurveTo {
658            abs: false,
659            x2: 130.0,
660            y2: 140.0,
661            x: 150.0,
662            y: 160.0,
663        },
664        PathSegment::Quadratic {
665            abs: false,
666            x1: 170.0,
667            y1: 180.0,
668            x: 190.0,
669            y: 200.0,
670        },
671        PathSegment::SmoothQuadratic {
672            abs: false,
673            x: 210.0,
674            y: 220.0
675        },
676        PathSegment::EllipticalArc {
677            abs: false,
678            rx: 50.0,
679            ry: 50.0,
680            x_axis_rotation: 30.0,
681            large_arc: true,
682            sweep: true,
683            x: 230.0,
684            y: 240.0
685        },
686        PathSegment::ClosePath { abs: false }
687    );
688
689    test!(
690        close_path_1,
691        "M10 20 L 30 40 ZM 100 200 L 300 400",
692        PathSegment::MoveTo {
693            abs: true,
694            x: 10.0,
695            y: 20.0
696        },
697        PathSegment::LineTo {
698            abs: true,
699            x: 30.0,
700            y: 40.0
701        },
702        PathSegment::ClosePath { abs: true },
703        PathSegment::MoveTo {
704            abs: true,
705            x: 100.0,
706            y: 200.0
707        },
708        PathSegment::LineTo {
709            abs: true,
710            x: 300.0,
711            y: 400.0
712        }
713    );
714
715    test!(
716        close_path_2,
717        "M10 20 L 30 40 zM 100 200 L 300 400",
718        PathSegment::MoveTo {
719            abs: true,
720            x: 10.0,
721            y: 20.0
722        },
723        PathSegment::LineTo {
724            abs: true,
725            x: 30.0,
726            y: 40.0
727        },
728        PathSegment::ClosePath { abs: false },
729        PathSegment::MoveTo {
730            abs: true,
731            x: 100.0,
732            y: 200.0
733        },
734        PathSegment::LineTo {
735            abs: true,
736            x: 300.0,
737            y: 400.0
738        }
739    );
740
741    test!(
742        close_path_3,
743        "M10 20 L 30 40 Z Z Z",
744        PathSegment::MoveTo {
745            abs: true,
746            x: 10.0,
747            y: 20.0
748        },
749        PathSegment::LineTo {
750            abs: true,
751            x: 30.0,
752            y: 40.0
753        },
754        PathSegment::ClosePath { abs: true },
755        PathSegment::ClosePath { abs: true },
756        PathSegment::ClosePath { abs: true }
757    );
758
759    // first token should be EndOfStream
760    test!(invalid_1, "M\t.",);
761
762    // ClosePath can't be followed by a number
763    test!(
764        invalid_2,
765        "M 0 0 Z 2",
766        PathSegment::MoveTo {
767            abs: true,
768            x: 0.0,
769            y: 0.0
770        },
771        PathSegment::ClosePath { abs: true }
772    );
773
774    // ClosePath can be followed by any command
775    test!(
776        invalid_3,
777        "M 0 0 Z H 10",
778        PathSegment::MoveTo {
779            abs: true,
780            x: 0.0,
781            y: 0.0
782        },
783        PathSegment::ClosePath { abs: true },
784        PathSegment::HorizontalLineTo { abs: true, x: 10.0 }
785    );
786}