diff options
author | Lukas Lihotzki <lukas@lihotzki.de> | 2025-03-26 13:12:44 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-26 12:12:44 +0000 |
commit | 251eeb2c2dee8aa91b69d7f451b1a1a7607a3bd1 (patch) | |
tree | 369165369cb932d19b88b9d7269bbec8a0f30432 /components/script/svgpath | |
parent | f0ea3c6150ba4e2523ed07f7f3864cfa6c88772f (diff) | |
download | servo-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.rs | 13 | ||||
-rw-r--r-- | components/script/svgpath/number.rs | 198 | ||||
-rw-r--r-- | components/script/svgpath/path.rs | 393 | ||||
-rw-r--r-- | components/script/svgpath/stream.rs | 162 |
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), + } + } +} |