use crate::props::basic::CssImageParseError;
pub fn split_string_respect_comma<'a>(input: &'a str) -> Vec<&'a str> {
split_string_by_char(input, ',')
}
pub fn split_string_respect_whitespace<'a>(input: &'a str) -> Vec<&'a str> {
let mut items = Vec::<&str>::new();
let mut current_start = 0;
let mut depth = 0;
let input_bytes = input.as_bytes();
for (idx, &ch) in input_bytes.iter().enumerate() {
match ch {
b'(' => depth += 1,
b')' => depth -= 1,
b' ' | b'\t' | b'\n' | b'\r' if depth == 0 => {
if current_start < idx {
items.push(&input[current_start..idx]);
}
current_start = idx + 1;
}
_ => {}
}
}
if current_start < input.len() {
items.push(&input[current_start..]);
}
items
}
fn split_string_by_char<'a>(input: &'a str, target_char: char) -> Vec<&'a str> {
let mut comma_separated_items = Vec::<&str>::new();
let mut current_input = &input[..];
'outer: loop {
let (skip_next_braces_result, character_was_found) =
match skip_next_braces(¤t_input, target_char) {
Some(s) => s,
None => break 'outer,
};
let new_push_item = if character_was_found {
¤t_input[..skip_next_braces_result]
} else {
¤t_input[..]
};
let new_current_input = ¤t_input[(skip_next_braces_result + 1)..];
comma_separated_items.push(new_push_item);
current_input = new_current_input;
if !character_was_found {
break 'outer;
}
}
comma_separated_items
}
pub fn skip_next_braces(input: &str, target_char: char) -> Option<(usize, bool)> {
let mut depth = 0;
let mut last_character = 0;
let mut character_was_found = false;
if input.is_empty() {
return None;
}
for (idx, ch) in input.char_indices() {
last_character = idx;
match ch {
'(' => {
depth += 1;
}
')' => {
depth -= 1;
}
c => {
if c == target_char && depth == 0 {
character_was_found = true;
break;
}
}
}
}
if last_character == 0 {
None
} else {
Some((last_character, character_was_found))
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub enum ParenthesisParseError<'a> {
UnclosedBraces,
NoOpeningBraceFound,
NoClosingBraceFound,
StopWordNotFound(&'a str),
EmptyInput,
}
impl_display! { ParenthesisParseError<'a>, {
UnclosedBraces => format!("Unclosed parenthesis"),
NoOpeningBraceFound => format!("Expected value in parenthesis (missing \"(\")"),
NoClosingBraceFound => format!("Missing closing parenthesis (missing \")\")"),
StopWordNotFound(e) => format!("Stopword not found, found: \"{}\"", e),
EmptyInput => format!("Empty parenthesis"),
}}
#[derive(Debug, Clone, PartialEq)]
pub enum ParenthesisParseErrorOwned {
UnclosedBraces,
NoOpeningBraceFound,
NoClosingBraceFound,
StopWordNotFound(String),
EmptyInput,
}
impl<'a> ParenthesisParseError<'a> {
pub fn to_contained(&self) -> ParenthesisParseErrorOwned {
match self {
ParenthesisParseError::UnclosedBraces => ParenthesisParseErrorOwned::UnclosedBraces,
ParenthesisParseError::NoOpeningBraceFound => {
ParenthesisParseErrorOwned::NoOpeningBraceFound
}
ParenthesisParseError::NoClosingBraceFound => {
ParenthesisParseErrorOwned::NoClosingBraceFound
}
ParenthesisParseError::StopWordNotFound(s) => {
ParenthesisParseErrorOwned::StopWordNotFound(s.to_string())
}
ParenthesisParseError::EmptyInput => ParenthesisParseErrorOwned::EmptyInput,
}
}
}
impl ParenthesisParseErrorOwned {
pub fn to_shared<'a>(&'a self) -> ParenthesisParseError<'a> {
match self {
ParenthesisParseErrorOwned::UnclosedBraces => ParenthesisParseError::UnclosedBraces,
ParenthesisParseErrorOwned::NoOpeningBraceFound => {
ParenthesisParseError::NoOpeningBraceFound
}
ParenthesisParseErrorOwned::NoClosingBraceFound => {
ParenthesisParseError::NoClosingBraceFound
}
ParenthesisParseErrorOwned::StopWordNotFound(s) => {
ParenthesisParseError::StopWordNotFound(s.as_str())
}
ParenthesisParseErrorOwned::EmptyInput => ParenthesisParseError::EmptyInput,
}
}
}
pub fn parse_parentheses<'a>(
input: &'a str,
stopwords: &[&'static str],
) -> Result<(&'static str, &'a str), ParenthesisParseError<'a>> {
use self::ParenthesisParseError::*;
let input = input.trim();
if input.is_empty() {
return Err(EmptyInput);
}
let first_open_brace = input.find('(').ok_or(NoOpeningBraceFound)?;
let found_stopword = &input[..first_open_brace];
let mut validated_stopword = None;
for stopword in stopwords {
if found_stopword == *stopword {
validated_stopword = Some(stopword);
break;
}
}
let validated_stopword = validated_stopword.ok_or(StopWordNotFound(found_stopword))?;
let last_closing_brace = input.rfind(')').ok_or(NoClosingBraceFound)?;
Ok((
validated_stopword,
&input[(first_open_brace + 1)..last_closing_brace],
))
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct UnclosedQuotesError<'a>(pub &'a str);
impl<'a> From<UnclosedQuotesError<'a>> for CssImageParseError<'a> {
fn from(err: UnclosedQuotesError<'a>) -> Self {
CssImageParseError::UnclosedQuotes(err.0)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct QuoteStripped<'a>(pub &'a str);
pub fn strip_quotes<'a>(input: &'a str) -> Result<QuoteStripped<'a>, UnclosedQuotesError<'a>> {
let mut double_quote_iter = input.splitn(2, '"');
double_quote_iter.next();
let mut single_quote_iter = input.splitn(2, '\'');
single_quote_iter.next();
let first_double_quote = double_quote_iter.next();
let first_single_quote = single_quote_iter.next();
if first_double_quote.is_some() && first_single_quote.is_some() {
return Err(UnclosedQuotesError(input));
}
if first_double_quote.is_some() {
let quote_contents = first_double_quote.unwrap();
if !quote_contents.ends_with('"') {
return Err(UnclosedQuotesError(quote_contents));
}
Ok(QuoteStripped(quote_contents.trim_end_matches("\"")))
} else if first_single_quote.is_some() {
let quote_contents = first_single_quote.unwrap();
if !quote_contents.ends_with('\'') {
return Err(UnclosedQuotesError(input));
}
Ok(QuoteStripped(quote_contents.trim_end_matches("'")))
} else {
Err(UnclosedQuotesError(input))
}
}
#[cfg(all(test, feature = "parser"))]
mod tests {
use super::*;
#[test]
fn test_strip_quotes() {
assert_eq!(strip_quotes("'hello'").unwrap(), QuoteStripped("hello"));
assert_eq!(strip_quotes("\"world\"").unwrap(), QuoteStripped("world"));
assert_eq!(
strip_quotes("\" spaced \"").unwrap(),
QuoteStripped(" spaced ")
);
assert!(strip_quotes("'unclosed").is_err());
assert!(strip_quotes("\"mismatched'").is_err());
assert!(strip_quotes("no-quotes").is_err());
}
#[test]
fn test_parse_parentheses() {
assert_eq!(
parse_parentheses("url(image.png)", &["url"]),
Ok(("url", "image.png"))
);
assert_eq!(
parse_parentheses("linear-gradient(red, blue)", &["linear-gradient"]),
Ok(("linear-gradient", "red, blue"))
);
assert_eq!(
parse_parentheses("var(--my-var, 10px)", &["var"]),
Ok(("var", "--my-var, 10px"))
);
assert_eq!(
parse_parentheses(" rgb( 255, 0, 0 ) ", &["rgb", "rgba"]),
Ok(("rgb", " 255, 0, 0 "))
);
}
#[test]
fn test_parse_parentheses_errors() {
assert!(parse_parentheses("rgba(255,0,0,1)", &["rgb"]).is_err());
assert!(parse_parentheses("url'image.png'", &["url"]).is_err());
assert!(parse_parentheses("url(image.png", &["url"]).is_err());
}
#[test]
fn test_split_string_respect_comma() {
let simple = "one, two, three";
assert_eq!(
split_string_respect_comma(simple),
vec!["one", " two", " three"]
);
let with_parens = "rgba(255, 0, 0, 1), #ff00ff";
assert_eq!(
split_string_respect_comma(with_parens),
vec!["rgba(255, 0, 0, 1)", " #ff00ff"]
);
let multi_parens =
"linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1)), url(image.png)";
assert_eq!(
split_string_respect_comma(multi_parens),
vec![
"linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1))",
" url(image.png)"
]
);
let no_commas = "rgb(0,0,0)";
assert_eq!(split_string_respect_comma(no_commas), vec!["rgb(0,0,0)"]);
}
}