aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/svgpath/number.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/svgpath/number.rs')
-rw-r--r--components/script/svgpath/number.rs198
1 files changed, 198 insertions, 0 deletions
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
+}