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