1 | // Copyright 2021 the SVG Types Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use 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)] |
10 | pub struct IRI<'a>(pub &'a str); |
11 | |
12 | impl<'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<'_> = Stream::from(text); |
25 | let link: &str = 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)] |
39 | pub struct FuncIRI<'a>(pub &'a str); |
40 | |
41 | impl<'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<'_> = Stream::from(text); |
54 | let link: &str = 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 | |
64 | impl<'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)] |
113 | mod 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 | } |
205 | |