Skip to main content

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    use alloc::string::ToString;
116
117    #[test]
118    fn parse_iri_1() {
119        assert_eq!(IRI::from_str("#id").unwrap(), IRI("id"));
120    }
121
122    #[test]
123    fn parse_iri_2() {
124        assert_eq!(IRI::from_str("   #id   ").unwrap(), IRI("id"));
125    }
126
127    #[test]
128    fn parse_iri_3() {
129        // Trailing data is ok for the Stream, by not for IRI.
130        assert_eq!(Stream::from("   #id   text").parse_iri().unwrap(), "id");
131        assert_eq!(IRI::from_str("   #id   text").unwrap_err().to_string(),
132                   "unexpected data at position 10");
133    }
134
135    #[test]
136    fn parse_iri_4() {
137        assert_eq!(IRI::from_str("#1").unwrap(), IRI("1"));
138    }
139
140    #[test]
141    fn parse_err_iri_1() {
142        assert_eq!(IRI::from_str("# id").unwrap_err().to_string(), "invalid value");
143    }
144
145    #[test]
146    fn parse_func_iri_1() {
147        assert_eq!(FuncIRI::from_str("url(#id)").unwrap(), FuncIRI("id"));
148    }
149
150    #[test]
151    fn parse_func_iri_2() {
152        assert_eq!(FuncIRI::from_str("url(#1)").unwrap(), FuncIRI("1"));
153    }
154
155    #[test]
156    fn parse_func_iri_3() {
157        assert_eq!(FuncIRI::from_str("    url(    #id    )   ").unwrap(), FuncIRI("id"));
158    }
159
160    #[test]
161    fn parse_func_iri_4() {
162        // Trailing data is ok for the Stream, by not for FuncIRI.
163        assert_eq!(Stream::from("url(#id) qwe").parse_func_iri().unwrap(), "id");
164        assert_eq!(FuncIRI::from_str("url(#id) qwe").unwrap_err().to_string(),
165                   "unexpected data at position 10");
166    }
167
168    #[test]
169    fn parse_func_iri_5() {
170        assert_eq!(FuncIRI::from_str("url('#id')").unwrap(), FuncIRI("id"));
171        assert_eq!(FuncIRI::from_str("url(' #id ')").unwrap(), FuncIRI("id"));
172    }
173
174    #[test]
175    fn parse_func_iri_6() {
176        assert_eq!(FuncIRI::from_str("url(\"#id\")").unwrap(), FuncIRI("id"));
177        assert_eq!(FuncIRI::from_str("url(\" #id \")").unwrap(), FuncIRI("id"));
178    }
179
180    #[test]
181    fn parse_err_func_iri_1() {
182        assert_eq!(FuncIRI::from_str("url ( #1 )").unwrap_err().to_string(),
183                   "expected 'url(' not 'url ' at position 1");
184    }
185
186    #[test]
187    fn parse_err_func_iri_2() {
188        assert_eq!(FuncIRI::from_str("url(#)").unwrap_err().to_string(), "invalid value");
189    }
190
191    #[test]
192    fn parse_err_func_iri_3() {
193        assert_eq!(FuncIRI::from_str("url(# id)").unwrap_err().to_string(),
194                   "invalid value");
195    }
196
197    #[test]
198    fn parse_err_func_iri_4() {
199        // If single quotes are present around the ID, they should be on both sides
200        assert_eq!(FuncIRI::from_str("url('#id)").unwrap_err().to_string(),
201                   "unexpected end of stream");
202        assert_eq!(FuncIRI::from_str("url(#id')").unwrap_err().to_string(),
203                   "invalid value");
204    }
205}