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