1use crate::props::basic::CssImageParseError;
2
3pub fn split_string_respect_comma<'a>(input: &'a str) -> Vec<&'a str> {
9 split_string_by_char(input, ',')
10}
11
12pub fn split_string_respect_whitespace<'a>(input: &'a str) -> Vec<&'a str> {
16 let mut items = Vec::<&str>::new();
17 let mut current_start = 0;
18 let mut depth = 0;
19 let input_bytes = input.as_bytes();
20
21 for (idx, &ch) in input_bytes.iter().enumerate() {
22 match ch {
23 b'(' => depth += 1,
24 b')' => depth -= 1,
25 b' ' | b'\t' | b'\n' | b'\r' if depth == 0 => {
26 if current_start < idx {
27 items.push(&input[current_start..idx]);
28 }
29 current_start = idx + 1;
30 }
31 _ => {}
32 }
33 }
34
35 if current_start < input.len() {
37 items.push(&input[current_start..]);
38 }
39
40 items
41}
42
43fn split_string_by_char<'a>(input: &'a str, target_char: char) -> Vec<&'a str> {
44 let mut comma_separated_items = Vec::<&str>::new();
45 let mut current_input = &input[..];
46
47 'outer: loop {
48 let (skip_next_braces_result, character_was_found) =
49 match skip_next_braces(¤t_input, target_char) {
50 Some(s) => s,
51 None => break 'outer,
52 };
53 let new_push_item = if character_was_found {
54 ¤t_input[..skip_next_braces_result]
55 } else {
56 ¤t_input[..]
57 };
58 let new_current_input = ¤t_input[(skip_next_braces_result + 1)..];
59 comma_separated_items.push(new_push_item);
60 current_input = new_current_input;
61 if !character_was_found {
62 break 'outer;
63 }
64 }
65
66 comma_separated_items
67}
68
69pub fn skip_next_braces(input: &str, target_char: char) -> Option<(usize, bool)> {
71 let mut depth = 0;
72 let mut last_character = 0;
73 let mut character_was_found = false;
74
75 if input.is_empty() {
76 return None;
77 }
78
79 for (idx, ch) in input.char_indices() {
80 last_character = idx;
81 match ch {
82 '(' => {
83 depth += 1;
84 }
85 ')' => {
86 depth -= 1;
87 }
88 c => {
89 if c == target_char && depth == 0 {
90 character_was_found = true;
91 break;
92 }
93 }
94 }
95 }
96
97 if last_character == 0 {
98 None
100 } else {
101 Some((last_character, character_was_found))
102 }
103}
104
105#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
106pub enum ParenthesisParseError<'a> {
107 UnclosedBraces,
108 NoOpeningBraceFound,
109 NoClosingBraceFound,
110 StopWordNotFound(&'a str),
111 EmptyInput,
112}
113
114impl_display! { ParenthesisParseError<'a>, {
115 UnclosedBraces => format!("Unclosed parenthesis"),
116 NoOpeningBraceFound => format!("Expected value in parenthesis (missing \"(\")"),
117 NoClosingBraceFound => format!("Missing closing parenthesis (missing \")\")"),
118 StopWordNotFound(e) => format!("Stopword not found, found: \"{}\"", e),
119 EmptyInput => format!("Empty parenthesis"),
120}}
121
122#[derive(Debug, Clone, PartialEq)]
124pub enum ParenthesisParseErrorOwned {
125 UnclosedBraces,
126 NoOpeningBraceFound,
127 NoClosingBraceFound,
128 StopWordNotFound(String),
129 EmptyInput,
130}
131
132impl<'a> ParenthesisParseError<'a> {
133 pub fn to_contained(&self) -> ParenthesisParseErrorOwned {
134 match self {
135 ParenthesisParseError::UnclosedBraces => ParenthesisParseErrorOwned::UnclosedBraces,
136 ParenthesisParseError::NoOpeningBraceFound => {
137 ParenthesisParseErrorOwned::NoOpeningBraceFound
138 }
139 ParenthesisParseError::NoClosingBraceFound => {
140 ParenthesisParseErrorOwned::NoClosingBraceFound
141 }
142 ParenthesisParseError::StopWordNotFound(s) => {
143 ParenthesisParseErrorOwned::StopWordNotFound(s.to_string())
144 }
145 ParenthesisParseError::EmptyInput => ParenthesisParseErrorOwned::EmptyInput,
146 }
147 }
148}
149
150impl ParenthesisParseErrorOwned {
151 pub fn to_shared<'a>(&'a self) -> ParenthesisParseError<'a> {
152 match self {
153 ParenthesisParseErrorOwned::UnclosedBraces => ParenthesisParseError::UnclosedBraces,
154 ParenthesisParseErrorOwned::NoOpeningBraceFound => {
155 ParenthesisParseError::NoOpeningBraceFound
156 }
157 ParenthesisParseErrorOwned::NoClosingBraceFound => {
158 ParenthesisParseError::NoClosingBraceFound
159 }
160 ParenthesisParseErrorOwned::StopWordNotFound(s) => {
161 ParenthesisParseError::StopWordNotFound(s.as_str())
162 }
163 ParenthesisParseErrorOwned::EmptyInput => ParenthesisParseError::EmptyInput,
164 }
165 }
166}
167
168pub fn parse_parentheses<'a>(
195 input: &'a str,
196 stopwords: &[&'static str],
197) -> Result<(&'static str, &'a str), ParenthesisParseError<'a>> {
198 use self::ParenthesisParseError::*;
199
200 let input = input.trim();
201 if input.is_empty() {
202 return Err(EmptyInput);
203 }
204
205 let first_open_brace = input.find('(').ok_or(NoOpeningBraceFound)?;
206 let found_stopword = &input[..first_open_brace];
207
208 let mut validated_stopword = None;
210 for stopword in stopwords {
211 if found_stopword == *stopword {
212 validated_stopword = Some(stopword);
213 break;
214 }
215 }
216
217 let validated_stopword = validated_stopword.ok_or(StopWordNotFound(found_stopword))?;
218 let last_closing_brace = input.rfind(')').ok_or(NoClosingBraceFound)?;
219
220 Ok((
221 validated_stopword,
222 &input[(first_open_brace + 1)..last_closing_brace],
223 ))
224}
225
226#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
228pub struct UnclosedQuotesError<'a>(pub &'a str);
229
230impl<'a> From<UnclosedQuotesError<'a>> for CssImageParseError<'a> {
231 fn from(err: UnclosedQuotesError<'a>) -> Self {
232 CssImageParseError::UnclosedQuotes(err.0)
233 }
234}
235
236#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
238pub struct QuoteStripped<'a>(pub &'a str);
239
240pub fn strip_quotes<'a>(input: &'a str) -> Result<QuoteStripped<'a>, UnclosedQuotesError<'a>> {
258 let mut double_quote_iter = input.splitn(2, '"');
259 double_quote_iter.next();
260 let mut single_quote_iter = input.splitn(2, '\'');
261 single_quote_iter.next();
262
263 let first_double_quote = double_quote_iter.next();
264 let first_single_quote = single_quote_iter.next();
265 if first_double_quote.is_some() && first_single_quote.is_some() {
266 return Err(UnclosedQuotesError(input));
267 }
268 if first_double_quote.is_some() {
269 let quote_contents = first_double_quote.unwrap();
270 if !quote_contents.ends_with('"') {
271 return Err(UnclosedQuotesError(quote_contents));
272 }
273 Ok(QuoteStripped(quote_contents.trim_end_matches("\"")))
274 } else if first_single_quote.is_some() {
275 let quote_contents = first_single_quote.unwrap();
276 if !quote_contents.ends_with('\'') {
277 return Err(UnclosedQuotesError(input));
278 }
279 Ok(QuoteStripped(quote_contents.trim_end_matches("'")))
280 } else {
281 Err(UnclosedQuotesError(input))
282 }
283}
284
285#[cfg(all(test, feature = "parser"))]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_strip_quotes() {
291 assert_eq!(strip_quotes("'hello'").unwrap(), QuoteStripped("hello"));
292 assert_eq!(strip_quotes("\"world\"").unwrap(), QuoteStripped("world"));
293 assert_eq!(
294 strip_quotes("\" spaced \"").unwrap(),
295 QuoteStripped(" spaced ")
296 );
297 assert!(strip_quotes("'unclosed").is_err());
298 assert!(strip_quotes("\"mismatched'").is_err());
299 assert!(strip_quotes("no-quotes").is_err());
300 }
301
302 #[test]
303 fn test_parse_parentheses() {
304 assert_eq!(
305 parse_parentheses("url(image.png)", &["url"]),
306 Ok(("url", "image.png"))
307 );
308 assert_eq!(
309 parse_parentheses("linear-gradient(red, blue)", &["linear-gradient"]),
310 Ok(("linear-gradient", "red, blue"))
311 );
312 assert_eq!(
313 parse_parentheses("var(--my-var, 10px)", &["var"]),
314 Ok(("var", "--my-var, 10px"))
315 );
316 assert_eq!(
317 parse_parentheses(" rgb( 255, 0, 0 ) ", &["rgb", "rgba"]),
318 Ok(("rgb", " 255, 0, 0 "))
319 );
320 }
321
322 #[test]
323 fn test_parse_parentheses_errors() {
324 assert!(parse_parentheses("rgba(255,0,0,1)", &["rgb"]).is_err());
326 assert!(parse_parentheses("url'image.png'", &["url"]).is_err());
328 assert!(parse_parentheses("url(image.png", &["url"]).is_err());
330 }
331
332 #[test]
333 fn test_split_string_respect_comma() {
334 let simple = "one, two, three";
336 assert_eq!(
337 split_string_respect_comma(simple),
338 vec!["one", " two", " three"]
339 );
340
341 let with_parens = "rgba(255, 0, 0, 1), #ff00ff";
343 assert_eq!(
344 split_string_respect_comma(with_parens),
345 vec!["rgba(255, 0, 0, 1)", " #ff00ff"]
346 );
347
348 let multi_parens =
350 "linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1)), url(image.png)";
351 assert_eq!(
352 split_string_respect_comma(multi_parens),
353 vec![
354 "linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1))",
355 " url(image.png)"
356 ]
357 );
358
359 let no_commas = "rgb(0,0,0)";
361 assert_eq!(split_string_respect_comma(no_commas), vec!["rgb(0,0,0)"]);
362 }
363}