svgtypes 0.15.3

SVG types parser.
Documentation
// Copyright 2021 the SVG Types Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use crate::{Error, Stream};

/// Representation of the [`<IRI>`] type.
///
/// [`<IRI>`]: https://www.w3.org/TR/SVG11/types.html#DataTypeIRI
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct IRI<'a>(pub &'a str);

impl<'a> IRI<'a> {
    /// Parsers a `IRI` from a string.
    ///
    /// By the SVG spec, the ID must contain only [Name] characters,
    /// but since no one fallows this it will parse any characters.
    ///
    /// We can't use the `FromStr` trait because it requires
    /// an owned value as a return type.
    ///
    /// [Name]: https://www.w3.org/TR/xml/#NT-Name
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(text: &'a str) -> Result<Self, Error> {
        let mut s = Stream::from(text);
        let link = s.parse_iri()?;
        s.skip_spaces();
        if !s.at_end() {
            return Err(Error::UnexpectedData(s.calc_char_pos()));
        }

        Ok(Self(link))
    }
}

/// Representation of the [`<FuncIRI>`] type.
///
/// [`<FuncIRI>`]: https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct FuncIRI<'a>(pub &'a str);

impl<'a> FuncIRI<'a> {
    /// Parsers a `FuncIRI` from a string.
    ///
    /// By the SVG spec, the ID must contain only [Name] characters,
    /// but since no one fallows this it will parse any characters.
    ///
    /// We can't use the `FromStr` trait because it requires
    /// an owned value as a return type.
    ///
    /// [Name]: https://www.w3.org/TR/xml/#NT-Name
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(text: &'a str) -> Result<Self, Error> {
        let mut s = Stream::from(text);
        let link = s.parse_func_iri()?;
        s.skip_spaces();
        if !s.at_end() {
            return Err(Error::UnexpectedData(s.calc_char_pos()));
        }

        Ok(Self(link))
    }
}

impl<'a> Stream<'a> {
    pub fn parse_iri(&mut self) -> Result<&'a str, Error> {
        self.skip_spaces();
        self.consume_byte(b'#')?;
        let link = self.consume_bytes(|_, c| c != b' ');
        if link.is_empty() {
            return Err(Error::InvalidValue);
        }
        Ok(link)
    }

    pub fn parse_func_iri(&mut self) -> Result<&'a str, Error> {
        self.skip_spaces();
        self.consume_string(b"url(")?;
        self.skip_spaces();

        let quote = match self.curr_byte() {
            Ok(b'\'') | Ok(b'"') => self.curr_byte().ok(),
            _ => None,
        };
        if quote.is_some() {
            self.advance(1);
            self.skip_spaces();
        }
        self.consume_byte(b'#')?;
        let link = if let Some(quote) = quote {
            self.consume_bytes(|_, c| c != quote).trim_end()
        } else {
            self.consume_bytes(|_, c| c != b' ' && c != b')')
        };
        if link.is_empty() {
            return Err(Error::InvalidValue);
        }
        // Non-paired quotes is an error.
        if link.contains('\'') || link.contains('"') {
            return Err(Error::InvalidValue);
        }
        self.skip_spaces();
        if let Some(quote) = quote {
            self.consume_byte(quote)?;
            self.skip_spaces();
        }
        self.consume_byte(b')')?;
        Ok(link)
    }
}

#[rustfmt::skip]
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_iri_1() {
        assert_eq!(IRI::from_str("#id").unwrap(), IRI("id"));
    }

    #[test]
    fn parse_iri_2() {
        assert_eq!(IRI::from_str("   #id   ").unwrap(), IRI("id"));
    }

    #[test]
    fn parse_iri_3() {
        // Trailing data is ok for the Stream, by not for IRI.
        assert_eq!(Stream::from("   #id   text").parse_iri().unwrap(), "id");
        assert_eq!(IRI::from_str("   #id   text").unwrap_err().to_string(),
                   "unexpected data at position 10");
    }

    #[test]
    fn parse_iri_4() {
        assert_eq!(IRI::from_str("#1").unwrap(), IRI("1"));
    }

    #[test]
    fn parse_err_iri_1() {
        assert_eq!(IRI::from_str("# id").unwrap_err().to_string(), "invalid value");
    }

    #[test]
    fn parse_func_iri_1() {
        assert_eq!(FuncIRI::from_str("url(#id)").unwrap(), FuncIRI("id"));
    }

    #[test]
    fn parse_func_iri_2() {
        assert_eq!(FuncIRI::from_str("url(#1)").unwrap(), FuncIRI("1"));
    }

    #[test]
    fn parse_func_iri_3() {
        assert_eq!(FuncIRI::from_str("    url(    #id    )   ").unwrap(), FuncIRI("id"));
    }

    #[test]
    fn parse_func_iri_4() {
        // Trailing data is ok for the Stream, by not for FuncIRI.
        assert_eq!(Stream::from("url(#id) qwe").parse_func_iri().unwrap(), "id");
        assert_eq!(FuncIRI::from_str("url(#id) qwe").unwrap_err().to_string(),
                   "unexpected data at position 10");
    }

    #[test]
    fn parse_func_iri_5() {
        assert_eq!(FuncIRI::from_str("url('#id')").unwrap(), FuncIRI("id"));
        assert_eq!(FuncIRI::from_str("url(' #id ')").unwrap(), FuncIRI("id"));
    }

    #[test]
    fn parse_func_iri_6() {
        assert_eq!(FuncIRI::from_str("url(\"#id\")").unwrap(), FuncIRI("id"));
        assert_eq!(FuncIRI::from_str("url(\" #id \")").unwrap(), FuncIRI("id"));
    }

    #[test]
    fn parse_err_func_iri_1() {
        assert_eq!(FuncIRI::from_str("url ( #1 )").unwrap_err().to_string(),
                   "expected 'url(' not 'url ' at position 1");
    }

    #[test]
    fn parse_err_func_iri_2() {
        assert_eq!(FuncIRI::from_str("url(#)").unwrap_err().to_string(), "invalid value");
    }

    #[test]
    fn parse_err_func_iri_3() {
        assert_eq!(FuncIRI::from_str("url(# id)").unwrap_err().to_string(),
                   "invalid value");
    }

    #[test]
    fn parse_err_func_iri_4() {
        // If single quotes are present around the ID, they should be on both sides
        assert_eq!(FuncIRI::from_str("url('#id)").unwrap_err().to_string(),
                   "unexpected end of stream");
        assert_eq!(FuncIRI::from_str("url(#id')").unwrap_err().to_string(),
                   "invalid value");
    }
}