aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/svgpath
diff options
context:
space:
mode:
authorLukas Lihotzki <lukas@lihotzki.de>2025-03-26 13:12:44 +0100
committerGitHub <noreply@github.com>2025-03-26 12:12:44 +0000
commit251eeb2c2dee8aa91b69d7f451b1a1a7607a3bd1 (patch)
tree369165369cb932d19b88b9d7269bbec8a0f30432 /components/script/svgpath
parentf0ea3c6150ba4e2523ed07f7f3864cfa6c88772f (diff)
downloadservo-251eeb2c2dee8aa91b69d7f451b1a1a7607a3bd1.tar.gz
servo-251eeb2c2dee8aa91b69d7f451b1a1a7607a3bd1.zip
Add `Path2D` (#35783)
Signed-off-by: Lukas Lihotzki <lukas@lihotzki.de>
Diffstat (limited to 'components/script/svgpath')
-rw-r--r--components/script/svgpath/mod.rs13
-rw-r--r--components/script/svgpath/number.rs198
-rw-r--r--components/script/svgpath/path.rs393
-rw-r--r--components/script/svgpath/stream.rs162
4 files changed, 766 insertions, 0 deletions
diff --git a/components/script/svgpath/mod.rs b/components/script/svgpath/mod.rs
new file mode 100644
index 00000000000..f3093e3d282
--- /dev/null
+++ b/components/script/svgpath/mod.rs
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+mod number;
+mod path;
+mod stream;
+
+#[derive(Debug, Default, Eq, PartialEq)]
+pub struct Error;
+
+pub(crate) use path::PathParser;
+pub(crate) use stream::Stream;
diff --git a/components/script/svgpath/number.rs b/components/script/svgpath/number.rs
new file mode 100644
index 00000000000..b199b357868
--- /dev/null
+++ b/components/script/svgpath/number.rs
@@ -0,0 +1,198 @@
+// Copyright 2018 the SVG Types Authors
+// Copyright 2025 the Servo Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use std::str::FromStr;
+
+use crate::svgpath::{Error, Stream};
+
+/// An [SVG number](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber).
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct Number(pub f32);
+
+impl std::str::FromStr for Number {
+ type Err = Error;
+
+ fn from_str(text: &str) -> Result<Self, Self::Err> {
+ let mut s = Stream::from(text);
+ let n = s.parse_number()?;
+ s.skip_spaces();
+ if !s.at_end() {
+ return Err(Error);
+ }
+
+ Ok(Self(n))
+ }
+}
+
+impl Stream<'_> {
+ /// Parses number from the stream.
+ ///
+ /// This method will detect a number length and then
+ /// will pass a substring to the `f64::from_str` method.
+ ///
+ /// <https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber>
+ pub fn parse_number(&mut self) -> Result<f32, Error> {
+ // Strip off leading whitespaces.
+ self.skip_spaces();
+
+ if self.at_end() {
+ return Err(Error);
+ }
+
+ self.parse_number_impl().map_err(|_| Error)
+ }
+
+ fn parse_number_impl(&mut self) -> Result<f32, Error> {
+ let start = self.pos();
+
+ let mut c = self.curr_byte()?;
+
+ // Consume sign.
+ if matches!(c, b'+' | b'-') {
+ self.advance(1);
+ c = self.curr_byte()?;
+ }
+
+ // Consume integer.
+ match c {
+ b'0'..=b'9' => self.skip_digits(),
+ b'.' => {},
+ _ => return Err(Error),
+ }
+
+ // Consume fraction.
+ if let Ok(b'.') = self.curr_byte() {
+ self.advance(1);
+ self.skip_digits();
+ }
+
+ if let Ok(c) = self.curr_byte() {
+ if matches!(c, b'e' | b'E') {
+ let c2 = self.next_byte()?;
+ // Check for `em`/`ex`.
+ if c2 != b'm' && c2 != b'x' {
+ self.advance(1);
+
+ match self.curr_byte()? {
+ b'+' | b'-' => {
+ self.advance(1);
+ self.skip_digits();
+ },
+ b'0'..=b'9' => self.skip_digits(),
+ _ => {
+ return Err(Error);
+ },
+ }
+ }
+ }
+ }
+
+ let s = self.slice_back(start);
+
+ // Use the default f32 parser now.
+ if let Ok(n) = f32::from_str(s) {
+ // inf, nan, etc. are an error.
+ if n.is_finite() {
+ return Ok(n);
+ }
+ }
+
+ Err(Error)
+ }
+
+ /// Parses number from a list of numbers.
+ pub fn parse_list_number(&mut self) -> Result<f32, Error> {
+ if self.at_end() {
+ return Err(Error);
+ }
+
+ let n = self.parse_number()?;
+ self.skip_spaces();
+ self.parse_list_separator();
+ Ok(n)
+ }
+}
+
+/// A pull-based [`<list-of-numbers>`] parser.
+///
+/// # Examples
+///
+/// ```
+/// use svgtypes::NumberListParser;
+///
+/// let mut p = NumberListParser::from("10, 20 -50");
+/// assert_eq!(p.next().unwrap().unwrap(), 10.0);
+/// assert_eq!(p.next().unwrap().unwrap(), 20.0);
+/// assert_eq!(p.next().unwrap().unwrap(), -50.0);
+/// assert_eq!(p.next().is_none(), true);
+/// ```
+///
+/// [`<list-of-numbers>`]: https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumberList
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct NumberListParser<'a>(Stream<'a>);
+
+impl<'a> From<&'a str> for NumberListParser<'a> {
+ #[inline]
+ fn from(v: &'a str) -> Self {
+ NumberListParser(Stream::from(v))
+ }
+}
+
+impl Iterator for NumberListParser<'_> {
+ type Item = Result<f32, Error>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.0.at_end() {
+ None
+ } else {
+ let v = self.0.parse_list_number();
+ if v.is_err() {
+ self.0.jump_to_end();
+ }
+
+ Some(v)
+ }
+ }
+}
+
+#[rustfmt::skip]
+#[cfg(test)]
+mod tests {
+ use crate::svgpath::Stream;
+
+ macro_rules! test_p {
+ ($name:ident, $text:expr, $result:expr) => (
+ #[test]
+ fn $name() {
+ let mut s = Stream::from($text);
+ assert_eq!(s.parse_number().unwrap(), $result);
+ }
+ )
+ }
+
+ test_p!(parse_1, "0", 0.0);
+ test_p!(parse_2, "1", 1.0);
+ test_p!(parse_3, "-1", -1.0);
+ test_p!(parse_4, " -1 ", -1.0);
+ test_p!(parse_5, " 1 ", 1.0);
+ test_p!(parse_6, ".4", 0.4);
+ test_p!(parse_7, "-.4", -0.4);
+ test_p!(parse_8, "-.4text", -0.4);
+ test_p!(parse_9, "-.01 text", -0.01);
+ test_p!(parse_10, "-.01 4", -0.01);
+ test_p!(parse_11, ".0000000000008", 0.0000000000008);
+ test_p!(parse_12, "1000000000000", 1000000000000.0);
+ test_p!(parse_13, "123456.123456", 123456.123456);
+ test_p!(parse_14, "+10", 10.0);
+ test_p!(parse_15, "1e2", 100.0);
+ test_p!(parse_16, "1e+2", 100.0);
+ test_p!(parse_17, "1E2", 100.0);
+ test_p!(parse_18, "1e-2", 0.01);
+ test_p!(parse_19, "1ex", 1.0);
+ test_p!(parse_20, "1em", 1.0);
+ test_p!(parse_21, "12345678901234567890", 12345678901234567000.0);
+ test_p!(parse_22, "0.", 0.0);
+ test_p!(parse_23, "1.3e-2", 0.013);
+ // test_number!(parse_24, "1e", 1.0); // TODO: this
+}
diff --git a/components/script/svgpath/path.rs b/components/script/svgpath/path.rs
new file mode 100644
index 00000000000..7d97df22cd2
--- /dev/null
+++ b/components/script/svgpath/path.rs
@@ -0,0 +1,393 @@
+// Copyright 2021 the SVG Types Authors
+// Copyright 2025 the Servo Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use canvas_traits::canvas::PathSegment;
+
+use crate::svgpath::{Error, Stream};
+
+pub struct PathParser<'a> {
+ stream: Stream<'a>,
+ state: State,
+ last_cmd: u8,
+}
+
+impl<'a> PathParser<'a> {
+ pub fn new(string: &'a str) -> Self {
+ Self {
+ stream: Stream::from(string),
+ state: State::default(),
+ last_cmd: b' ',
+ }
+ }
+}
+
+impl Iterator for PathParser<'_> {
+ type Item = Result<PathSegment, Error>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.stream.skip_spaces();
+
+ let Ok(curr_byte) = self.stream.curr_byte() else {
+ return None;
+ };
+
+ let cmd = if self.last_cmd == b' ' {
+ if let move_to @ (b'm' | b'M') = curr_byte {
+ self.stream.advance(1);
+ move_to
+ } else {
+ return Some(Err(Error));
+ }
+ } else if curr_byte.is_ascii_alphabetic() {
+ self.stream.advance(1);
+ curr_byte
+ } else {
+ match self.last_cmd {
+ b'm' => b'l',
+ b'M' => b'L',
+ b'z' | b'Z' => return Some(Err(Error)),
+ cmd => cmd,
+ }
+ };
+
+ self.last_cmd = cmd;
+ Some(to_point(&mut self.stream, cmd, &mut self.state))
+ }
+}
+
+#[derive(Default)]
+pub struct State {
+ start: (f32, f32),
+ pos: (f32, f32),
+ quad: (f32, f32),
+ cubic: (f32, f32),
+}
+
+pub fn to_point(s: &mut Stream<'_>, cmd: u8, state: &mut State) -> Result<PathSegment, Error> {
+ let abs = cmd.is_ascii_uppercase();
+ let cmd = cmd.to_ascii_lowercase();
+ let (dx, dy) = if abs { (0., 0.) } else { state.pos };
+ let seg = match cmd {
+ b'm' => PathSegment::MoveTo {
+ x: s.parse_list_number()? + dx,
+ y: s.parse_list_number()? + dy,
+ },
+ b'l' => PathSegment::LineTo {
+ x: s.parse_list_number()? + dx,
+ y: s.parse_list_number()? + dy,
+ },
+ b'h' => PathSegment::LineTo {
+ x: s.parse_list_number()? + dx,
+ y: state.pos.1,
+ },
+ b'v' => PathSegment::LineTo {
+ x: state.pos.0,
+ y: s.parse_list_number()? + dy,
+ },
+ b'c' => PathSegment::Bezier {
+ cp1x: s.parse_list_number()? + dx,
+ cp1y: s.parse_list_number()? + dy,
+ cp2x: s.parse_list_number()? + dx,
+ cp2y: s.parse_list_number()? + dy,
+ x: s.parse_list_number()? + dx,
+ y: s.parse_list_number()? + dy,
+ },
+ b's' => PathSegment::Bezier {
+ cp1x: state.cubic.0,
+ cp1y: state.cubic.1,
+ cp2x: s.parse_list_number()? + dx,
+ cp2y: s.parse_list_number()? + dy,
+ x: s.parse_list_number()? + dx,
+ y: s.parse_list_number()? + dy,
+ },
+ b'q' => PathSegment::Quadratic {
+ cpx: s.parse_list_number()? + dx,
+ cpy: s.parse_list_number()? + dy,
+ x: s.parse_list_number()? + dx,
+ y: s.parse_list_number()? + dy,
+ },
+ b't' => PathSegment::Quadratic {
+ cpx: state.quad.0,
+ cpy: state.quad.1,
+ x: s.parse_list_number()? + dx,
+ y: s.parse_list_number()? + dy,
+ },
+ b'a' => PathSegment::SvgArc {
+ radius_x: s.parse_list_number()?,
+ radius_y: s.parse_list_number()?,
+ rotation: s.parse_list_number()?,
+ large_arc: s.parse_flag()?,
+ sweep: s.parse_flag()?,
+ x: s.parse_list_number()? + dx,
+ y: s.parse_list_number()? + dy,
+ },
+ b'z' => PathSegment::ClosePath,
+ _ => return Err(crate::svgpath::Error),
+ };
+
+ match seg {
+ PathSegment::MoveTo { x, y } => {
+ state.start = (x, y);
+ state.pos = (x, y);
+ state.quad = (x, y);
+ state.cubic = (x, y);
+ },
+ PathSegment::LineTo { x, y } | PathSegment::SvgArc { x, y, .. } => {
+ state.pos = (x, y);
+ state.quad = (x, y);
+ state.cubic = (x, y);
+ },
+ PathSegment::Bezier {
+ cp2x, cp2y, x, y, ..
+ } => {
+ state.pos = (x, y);
+ state.quad = (x, y);
+ state.cubic = (x * 2.0 - cp2x, y * 2.0 - cp2y);
+ },
+ PathSegment::Quadratic { cpx, cpy, x, y, .. } => {
+ state.pos = (x, y);
+ state.quad = (x * 2.0 - cpx, y * 2.0 - cpy);
+ state.cubic = (x, y);
+ },
+ PathSegment::ClosePath => {
+ state.pos = state.start;
+ state.quad = state.start;
+ state.cubic = state.start;
+ },
+ _ => {},
+ }
+
+ Ok(seg)
+}
+
+#[rustfmt::skip]
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ macro_rules! test {
+ ($name:ident, $text:expr, $( $seg:expr ),*) => (
+ #[test]
+ fn $name() {
+ let mut s = PathParser::new($text);
+ $(
+ assert_eq!(s.next().unwrap().unwrap(), $seg);
+ )*
+
+ if let Some(res) = s.next() {
+ assert!(res.is_err());
+ }
+ }
+ )
+ }
+
+ test!(null, "", );
+ test!(not_a_path, "q", );
+ test!(not_a_move_to, "L 20 30", );
+ test!(stop_on_err_1, "M 10 20 L 30 40 L 50",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 30.0, y: 40.0 }
+ );
+
+ test!(move_to_1, "M 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 });
+ test!(move_to_2, "m 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 });
+ test!(move_to_3, "M 10 20 30 40 50 60",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 30.0, y: 40.0 },
+ PathSegment::LineTo { x: 50.0, y: 60.0 }
+ );
+ test!(move_to_4, "M 10 20 30 40 50 60 M 70 80 90 100 110 120",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 30.0, y: 40.0 },
+ PathSegment::LineTo { x: 50.0, y: 60.0 },
+ PathSegment::MoveTo { x: 70.0, y: 80.0 },
+ PathSegment::LineTo { x: 90.0, y: 100.0 },
+ PathSegment::LineTo { x: 110.0, y: 120.0 }
+ );
+
+ test!(arc_to_1, "M 10 20 A 5 5 30 1 1 20 20",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::SvgArc {
+ radius_x: 5.0, radius_y: 5.0,
+ rotation: 30.0,
+ large_arc: true, sweep: true,
+ x: 20.0, y: 20.0
+ }
+ );
+
+ test!(arc_to_2, "M 10 20 a 5 5 30 0 0 20 20",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::SvgArc {
+ radius_x: 5.0, radius_y: 5.0,
+ rotation: 30.0,
+ large_arc: false, sweep: false,
+ x: 30.0, y: 40.0
+ }
+ );
+
+ test!(arc_to_10, "M10-20A5.5.3-4 010-.1",
+ PathSegment::MoveTo { x: 10.0, y: -20.0 },
+ PathSegment::SvgArc {
+ radius_x: 5.5, radius_y: 0.3,
+ rotation: -4.0,
+ large_arc: false, sweep: true,
+ x: 0.0, y: -0.1
+ }
+ );
+
+ test!(separator_1, "M 10 20 L 5 15 C 10 20 30 40 50 60",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 5.0, y: 15.0 },
+ PathSegment::Bezier {
+ cp1x: 10.0, cp1y: 20.0,
+ cp2x: 30.0, cp2y: 40.0,
+ x: 50.0, y: 60.0,
+ }
+ );
+
+ test!(separator_2, "M 10, 20 L 5, 15 C 10, 20 30, 40 50, 60",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 5.0, y: 15.0 },
+ PathSegment::Bezier {
+ cp1x: 10.0, cp1y: 20.0,
+ cp2x: 30.0, cp2y: 40.0,
+ x: 50.0, y: 60.0,
+ }
+ );
+
+ test!(separator_3, "M 10,20 L 5,15 C 10,20 30,40 50,60",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 5.0, y: 15.0 },
+ PathSegment::Bezier {
+ cp1x: 10.0, cp1y: 20.0,
+ cp2x: 30.0, cp2y: 40.0,
+ x: 50.0, y: 60.0,
+ }
+ );
+
+ test!(separator_4, "M10, 20 L5, 15 C10, 20 30 40 50 60",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 5.0, y: 15.0 },
+ PathSegment::Bezier {
+ cp1x: 10.0, cp1y: 20.0,
+ cp2x: 30.0, cp2y: 40.0,
+ x: 50.0, y: 60.0,
+ }
+ );
+
+ test!(separator_5, "M10 20V30H40V50H60Z",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 10.0, y: 30.0 },
+ PathSegment::LineTo { x: 40.0, y: 30.0 },
+ PathSegment::LineTo { x: 40.0, y: 50.0 },
+ PathSegment::LineTo { x: 60.0, y: 50.0 },
+ PathSegment::ClosePath
+ );
+
+ test!(all_segments_1, "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 S 130 140 150 160
+ Q 170 180 190 200 T 210 220 A 50 50 30 1 1 230 240 Z",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 30.0, y: 40.0 },
+ PathSegment::LineTo { x: 50.0, y: 40.0 },
+ PathSegment::LineTo { x: 50.0, y: 60.0 },
+ PathSegment::Bezier {
+ cp1x: 70.0, cp1y: 80.0,
+ cp2x: 90.0, cp2y: 100.0,
+ x: 110.0, y: 120.0,
+ },
+ PathSegment::Bezier {
+ cp1x: 130.0, cp1y: 140.0,
+ cp2x: 130.0, cp2y: 140.0,
+ x: 150.0, y: 160.0,
+ },
+ PathSegment::Quadratic {
+ cpx: 170.0, cpy: 180.0,
+ x: 190.0, y: 200.0,
+ },
+ PathSegment::Quadratic {
+ cpx: 210.0, cpy: 220.0,
+ x: 210.0, y: 220.0,
+ },
+ PathSegment::SvgArc {
+ radius_x: 50.0, radius_y: 50.0,
+ rotation: 30.0,
+ large_arc: true, sweep: true,
+ x: 230.0, y: 240.0
+ },
+ PathSegment::ClosePath
+ );
+
+ test!(all_segments_2, "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 s 130 140 150 160
+ q 170 180 190 200 t 210 220 a 50 50 30 1 1 230 240 z",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 40.0, y: 60.0 },
+ PathSegment::LineTo { x: 90.0, y: 60.0 },
+ PathSegment::LineTo { x: 90.0, y: 120.0 },
+ PathSegment::Bezier {
+ cp1x: 160.0, cp1y: 200.0,
+ cp2x: 180.0, cp2y: 220.0,
+ x: 200.0, y: 240.0,
+ },
+ PathSegment::Bezier {
+ cp1x: 220.0, cp1y: 260.0, //?
+ cp2x: 330.0, cp2y: 380.0,
+ x: 350.0, y: 400.0,
+ },
+ PathSegment::Quadratic {
+ cpx: 520.0, cpy: 580.0,
+ x: 540.0, y: 600.0,
+ },
+ PathSegment::Quadratic {
+ cpx: 560.0, cpy: 620.0, //?
+ x: 750.0, y: 820.0
+ },
+ PathSegment::SvgArc {
+ radius_x: 50.0, radius_y: 50.0,
+ rotation: 30.0,
+ large_arc: true, sweep: true,
+ x: 980.0, y: 1060.0
+ },
+ PathSegment::ClosePath
+ );
+
+ test!(close_path_1, "M10 20 L 30 40 ZM 100 200 L 300 400",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 30.0, y: 40.0 },
+ PathSegment::ClosePath,
+ PathSegment::MoveTo { x: 100.0, y: 200.0 },
+ PathSegment::LineTo { x: 300.0, y: 400.0 }
+ );
+
+ test!(close_path_2, "M10 20 L 30 40 zM 100 200 L 300 400",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 30.0, y: 40.0 },
+ PathSegment::ClosePath,
+ PathSegment::MoveTo { x: 100.0, y: 200.0 },
+ PathSegment::LineTo { x: 300.0, y: 400.0 }
+ );
+
+ test!(close_path_3, "M10 20 L 30 40 Z Z Z",
+ PathSegment::MoveTo { x: 10.0, y: 20.0 },
+ PathSegment::LineTo { x: 30.0, y: 40.0 },
+ PathSegment::ClosePath,
+ PathSegment::ClosePath,
+ PathSegment::ClosePath
+ );
+
+ // first token should be EndOfStream
+ test!(invalid_1, "M\t.", );
+
+ // ClosePath can't be followed by a number
+ test!(invalid_2, "M 0 0 Z 2",
+ PathSegment::MoveTo { x: 0.0, y: 0.0 },
+ PathSegment::ClosePath
+ );
+
+ // ClosePath can be followed by any command
+ test!(invalid_3, "M 0 0 Z H 10",
+ PathSegment::MoveTo { x: 0.0, y: 0.0 },
+ PathSegment::ClosePath,
+ PathSegment::LineTo { x: 10.0, y: 0.0 }
+ );
+}
diff --git a/components/script/svgpath/stream.rs b/components/script/svgpath/stream.rs
new file mode 100644
index 00000000000..2ed044ba3b6
--- /dev/null
+++ b/components/script/svgpath/stream.rs
@@ -0,0 +1,162 @@
+// Copyright 2018 the SVG Types Authors
+// Copyright 2025 the Servo Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use crate::svgpath::Error;
+
+/// A streaming text parsing interface.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct Stream<'a> {
+ text: &'a str,
+ pos: usize,
+}
+
+impl<'a> From<&'a str> for Stream<'a> {
+ #[inline]
+ fn from(text: &'a str) -> Self {
+ Stream { text, pos: 0 }
+ }
+}
+
+impl<'a> Stream<'a> {
+ /// Returns the current position in bytes.
+ #[inline]
+ pub fn pos(&self) -> usize {
+ self.pos
+ }
+
+ /// Sets current position equal to the end.
+ ///
+ /// Used to indicate end of parsing on error.
+ #[inline]
+ pub fn jump_to_end(&mut self) {
+ self.pos = self.text.len();
+ }
+
+ /// Checks if the stream is reached the end.
+ ///
+ /// Any [`pos()`] value larger than original text length indicates stream end.
+ ///
+ /// Accessing stream after reaching end via safe methods will produce
+ /// an error.
+ ///
+ /// Accessing stream after reaching end via *_unchecked methods will produce
+ /// a Rust's bound checking error.
+ ///
+ /// [`pos()`]: #method.pos
+ #[inline]
+ pub fn at_end(&self) -> bool {
+ self.pos >= self.text.len()
+ }
+
+ /// Returns a byte from a current stream position.
+ #[inline]
+ pub fn curr_byte(&self) -> Result<u8, Error> {
+ if self.at_end() {
+ return Err(Error);
+ }
+
+ Ok(self.curr_byte_unchecked())
+ }
+
+ /// Returns a byte from a current stream position.
+ ///
+ /// # Panics
+ ///
+ /// - if the current position is after the end of the data
+ #[inline]
+ pub fn curr_byte_unchecked(&self) -> u8 {
+ self.text.as_bytes()[self.pos]
+ }
+
+ /// Checks that current byte is equal to provided.
+ ///
+ /// Returns `false` if no bytes left.
+ #[inline]
+ pub fn is_curr_byte_eq(&self, c: u8) -> bool {
+ if !self.at_end() {
+ self.curr_byte_unchecked() == c
+ } else {
+ false
+ }
+ }
+
+ /// Returns a next byte from a current stream position.
+ #[inline]
+ pub fn next_byte(&self) -> Result<u8, Error> {
+ if self.pos + 1 >= self.text.len() {
+ return Err(Error);
+ }
+
+ Ok(self.text.as_bytes()[self.pos + 1])
+ }
+
+ /// Advances by `n` bytes.
+ #[inline]
+ pub fn advance(&mut self, n: usize) {
+ debug_assert!(self.pos + n <= self.text.len());
+ self.pos += n;
+ }
+
+ /// Skips whitespaces.
+ ///
+ /// Accepted values: `' ' \n \r \t`.
+ pub fn skip_spaces(&mut self) {
+ while !self.at_end() && matches!(self.curr_byte_unchecked(), b' ' | b'\t' | b'\n' | b'\r') {
+ self.advance(1);
+ }
+ }
+
+ /// Consumes bytes by the predicate.
+ pub fn skip_bytes<F>(&mut self, f: F)
+ where
+ F: Fn(&Stream<'_>, u8) -> bool,
+ {
+ while !self.at_end() {
+ let c = self.curr_byte_unchecked();
+ if f(self, c) {
+ self.advance(1);
+ } else {
+ break;
+ }
+ }
+ }
+
+ /// Slices data from `pos` to the current position.
+ #[inline]
+ pub fn slice_back(&self, pos: usize) -> &'a str {
+ &self.text[pos..self.pos]
+ }
+
+ /// Skips digits.
+ pub fn skip_digits(&mut self) {
+ self.skip_bytes(|_, c| c.is_ascii_digit());
+ }
+
+ #[inline]
+ pub(crate) fn parse_list_separator(&mut self) {
+ if self.is_curr_byte_eq(b',') {
+ self.advance(1);
+ }
+ }
+
+ // By the SVG spec 'large-arc' and 'sweep' must contain only one char
+ // and can be written without any separators, e.g.: 10 20 30 01 10 20.
+ pub(crate) fn parse_flag(&mut self) -> Result<bool, Error> {
+ self.skip_spaces();
+
+ let c = self.curr_byte()?;
+ match c {
+ b'0' | b'1' => {
+ self.advance(1);
+ if self.is_curr_byte_eq(b',') {
+ self.advance(1);
+ }
+ self.skip_spaces();
+
+ Ok(c == b'1')
+ },
+ _ => Err(Error),
+ }
+ }
+}