1 | use 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)] |
7 | pub struct IRI<'a>(pub &'a str); |
8 | |
9 | impl<'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)] |
36 | pub struct FuncIRI<'a>(pub &'a str); |
37 | |
38 | impl<'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 | |
61 | impl<'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)] |
97 | mod 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 | |