svgtypes/
funciri.rs

1// Copyright 2021 the SVG Types Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use crate::{Error, Stream};
5
6/// Representation of the [`<IRI>`] type.
7///
8/// [`<IRI>`]: https://www.w3.org/TR/SVG11/types.html#DataTypeIRI
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub struct IRI<'a>(pub &'a str);
11
12impl<'a> IRI<'a> {
13    /// Parsers a `IRI` from a string.
14    ///
15    /// By the SVG spec, the ID must contain only [Name] characters,
16    /// but since no one fallows this it will parse any characters.
17    ///
18    /// We can't use the `FromStr` trait because it requires
19    /// an owned value as a return type.
20    ///
21    /// [Name]: https://www.w3.org/TR/xml/#NT-Name
22    #[allow(clippy::should_implement_trait)]
23    pub fn from_str(text: &'a str) -> Result<Self, Error> {
24        let mut s = Stream::from(text);
25        let link = s.parse_iri()?;
26        s.skip_spaces();
27        if !s.at_end() {
28            return Err(Error::UnexpectedData(s.calc_char_pos()));
29        }
30
31        Ok(Self(link))
32    }
33}
34
35/// Representation of the [`<FuncIRI>`] type.
36///
37/// [`<FuncIRI>`]: https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI
38#[derive(Clone, Copy, PartialEq, Eq, Debug)]
39pub struct FuncIRI<'a>(pub &'a str);
40
41impl<'a> FuncIRI<'a> {
42    /// Parsers a `FuncIRI` from a string.
43    ///
44    /// By the SVG spec, the ID must contain only [Name] characters,
45    /// but since no one fallows this it will parse any characters.
46    ///
47    /// We can't use the `FromStr` trait because it requires
48    /// an owned value as a return type.
49    ///
50    /// [Name]: https://www.w3.org/TR/xml/#NT-Name
51    #[allow(clippy::should_implement_trait)]
52    pub fn from_str(text: &'a str) -> Result<Self, Error> {
53        let mut s = Stream::from(text);
54        let link = s.parse_func_iri()?;
55        s.skip_spaces();
56        if !s.at_end() {
57            return Err(Error::UnexpectedData(s.calc_char_pos()));
58        }
59
60        Ok(Self(link))
61    }
62}
63
64impl<'a> Stream<'a> {
65    pub fn parse_iri(&mut self) -> Result<&'a str, Error> {
66        self.skip_spaces();
67        self.consume_byte(b'#')?;
68        let link = self.consume_bytes(|_, c| c != b' ');
69        if link.is_empty() {
70            return Err(Error::InvalidValue);
71        }
72        Ok(link)
73    }
74
75    pub fn parse_func_iri(&mut self) -> Result<&'a str, Error> {
76        self.skip_spaces();
77        self.consume_string(b"url(")?;
78        self.skip_spaces();
79
80        let quote = match self.curr_byte() {
81            Ok(b'\'') | Ok(b'"') => self.curr_byte().ok(),
82            _ => None,
83        };
84        if quote.is_some() {
85            self.advance(1);
86            self.skip_spaces();
87        }
88        self.consume_byte(b'#')?;
89        let link = if let Some(quote) = quote {
90            self.consume_bytes(|_, c| c != quote).trim_end()
91        } else {
92            self.consume_bytes(|_, c| c != b' ' && c != b')')
93        };
94        if link.is_empty() {
95            return Err(Error::InvalidValue);
96        }
97        // Non-paired quotes is an error.
98        if link.contains('\'') || link.contains('"') {
99            return Err(Error::InvalidValue);
100        }
101        self.skip_spaces();
102        if let Some(quote) = quote {
103            self.consume_byte(quote)?;
104            self.skip_spaces();
105        }
106        self.consume_byte(b')')?;
107        Ok(link)
108    }
109}
110
111#[rustfmt::skip]
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn parse_iri_1() {
118        assert_eq!(IRI::from_str("#id").unwrap(), IRI("id"));
119    }
120
121    #[test]
122    fn parse_iri_2() {
123        assert_eq!(IRI::from_str("   #id   ").unwrap(), IRI("id"));
124    }
125
126    #[test]
127    fn parse_iri_3() {
128        // Trailing data is ok for the Stream, by not for IRI.
129        assert_eq!(Stream::from("   #id   text").parse_iri().unwrap(), "id");
130        assert_eq!(IRI::from_str("   #id   text").unwrap_err().to_string(),
131                   "unexpected data at position 10");
132    }
133
134    #[test]
135    fn parse_iri_4() {
136        assert_eq!(IRI::from_str("#1").unwrap(), IRI("1"));
137    }
138
139    #[test]
140    fn parse_err_iri_1() {
141        assert_eq!(IRI::from_str("# id").unwrap_err().to_string(), "invalid value");
142    }
143
144    #[test]
145    fn parse_func_iri_1() {
146        assert_eq!(FuncIRI::from_str("url(#id)").unwrap(), FuncIRI("id"));
147    }
148
149    #[test]
150    fn parse_func_iri_2() {
151        assert_eq!(FuncIRI::from_str("url(#1)").unwrap(), FuncIRI("1"));
152    }
153
154    #[test]
155    fn parse_func_iri_3() {
156        assert_eq!(FuncIRI::from_str("    url(    #id    )   ").unwrap(), FuncIRI("id"));
157    }
158
159    #[test]
160    fn parse_func_iri_4() {
161        // Trailing data is ok for the Stream, by not for FuncIRI.
162        assert_eq!(Stream::from("url(#id) qwe").parse_func_iri().unwrap(), "id");
163        assert_eq!(FuncIRI::from_str("url(#id) qwe").unwrap_err().to_string(),
164                   "unexpected data at position 10");
165    }
166
167    #[test]
168    fn parse_func_iri_5() {
169        assert_eq!(FuncIRI::from_str("url('#id')").unwrap(), FuncIRI("id"));
170        assert_eq!(FuncIRI::from_str("url(' #id ')").unwrap(), FuncIRI("id"));
171    }
172
173    #[test]
174    fn parse_func_iri_6() {
175        assert_eq!(FuncIRI::from_str("url(\"#id\")").unwrap(), FuncIRI("id"));
176        assert_eq!(FuncIRI::from_str("url(\" #id \")").unwrap(), FuncIRI("id"));
177    }
178
179    #[test]
180    fn parse_err_func_iri_1() {
181        assert_eq!(FuncIRI::from_str("url ( #1 )").unwrap_err().to_string(),
182                   "expected 'url(' not 'url ' at position 1");
183    }
184
185    #[test]
186    fn parse_err_func_iri_2() {
187        assert_eq!(FuncIRI::from_str("url(#)").unwrap_err().to_string(), "invalid value");
188    }
189
190    #[test]
191    fn parse_err_func_iri_3() {
192        assert_eq!(FuncIRI::from_str("url(# id)").unwrap_err().to_string(),
193                   "invalid value");
194    }
195
196    #[test]
197    fn parse_err_func_iri_4() {
198        // If single quotes are present around the ID, they should be on both sides
199        assert_eq!(FuncIRI::from_str("url('#id)").unwrap_err().to_string(),
200                   "unexpected end of stream");
201        assert_eq!(FuncIRI::from_str("url(#id')").unwrap_err().to_string(),
202                   "invalid value");
203    }
204}