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