use super::truthiness::{
StaticScssComparisonOperator, static_scss_comparison_operands_truthiness,
static_scss_leaf_truthiness,
};
pub(super) fn scanner_literal_truthiness(value: &str) -> Option<bool> {
let trimmed = value.trim();
if let Some(inner) = strip_static_scss_outer_parens(trimmed) {
return scanner_literal_truthiness(inner);
}
match split_static_scss_boolean_operands(trimmed, "or") {
Ok(Some(operands)) => return static_scss_or_truthiness(operands),
Ok(None) => {}
Err(()) => return None,
}
match split_static_scss_boolean_operands(trimmed, "and") {
Ok(Some(operands)) => return static_scss_and_truthiness(operands),
Ok(None) => {}
Err(()) => return None,
}
if trimmed
.get(..3)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("not"))
&& let Some(operand) = trimmed.get(3..)
&& operand.chars().next().is_some_and(char::is_whitespace)
{
return scanner_literal_truthiness(operand.trim()).map(|truthy| !truthy);
}
match static_scss_comparison_truthiness(trimmed) {
Ok(Some(truthy)) => return Some(truthy),
Ok(None) => {}
Err(()) => return None,
}
static_scss_leaf_truthiness(trimmed)
}
fn strip_static_scss_outer_parens(value: &str) -> Option<&str> {
let inner_start = value.strip_prefix('(')?;
value.strip_suffix(')')?;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut quote: Option<char> = None;
let mut index = 0usize;
while index < value.len() {
let ch = value[index..].chars().next()?;
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = value[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
if matches!(ch, '"' | '\'') {
quote = Some(ch);
index += ch.len_utf8();
continue;
}
match ch {
'(' => paren_depth += 1,
')' => {
paren_depth = paren_depth.checked_sub(1)?;
if paren_depth == 0 && index + ch.len_utf8() != value.len() {
return None;
}
}
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.checked_sub(1)?,
_ => {}
}
index += ch.len_utf8();
}
(quote.is_none() && paren_depth == 0 && bracket_depth == 0)
.then(|| inner_start.strip_suffix(')').unwrap_or(inner_start).trim())
}
fn static_scss_or_truthiness(operands: Vec<&str>) -> Option<bool> {
let mut saw_unknown = false;
for operand in operands {
match scanner_literal_truthiness(operand) {
Some(true) => return Some(true),
Some(false) => {}
None => saw_unknown = true,
}
}
(!saw_unknown).then_some(false)
}
fn static_scss_and_truthiness(operands: Vec<&str>) -> Option<bool> {
let mut saw_unknown = false;
for operand in operands {
match scanner_literal_truthiness(operand) {
Some(true) => {}
Some(false) => return Some(false),
None => saw_unknown = true,
}
}
(!saw_unknown).then_some(true)
}
fn split_static_scss_boolean_operands<'a>(
value: &'a str,
keyword: &str,
) -> Result<Option<Vec<&'a str>>, ()> {
let mut operands = Vec::new();
let mut cursor = 0usize;
let mut index = 0usize;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut quote: Option<char> = None;
while index < value.len() {
let ch = value[index..].chars().next().ok_or(())?;
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = value[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
if matches!(ch, '"' | '\'') {
quote = Some(ch);
index += ch.len_utf8();
continue;
}
match ch {
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.checked_sub(1).ok_or(())?,
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.checked_sub(1).ok_or(())?,
_ => {}
}
if paren_depth == 0
&& bracket_depth == 0
&& static_scss_boolean_keyword_at(value, index, keyword)
{
let operand = value.get(cursor..index).ok_or(())?.trim();
if operand.is_empty() {
return Err(());
}
operands.push(operand);
index += keyword.len();
cursor = index;
continue;
}
index += ch.len_utf8();
}
if quote.is_some() || paren_depth != 0 || bracket_depth != 0 {
return Err(());
}
if operands.is_empty() {
return Ok(None);
}
let operand = value.get(cursor..).ok_or(())?.trim();
if operand.is_empty() {
return Err(());
}
operands.push(operand);
Ok(Some(operands))
}
fn static_scss_boolean_keyword_at(value: &str, index: usize, keyword: &str) -> bool {
if !value
.get(index..)
.is_some_and(|suffix| suffix.starts_with(keyword))
{
return false;
}
let before_ok = value
.get(..index)
.and_then(|prefix| prefix.chars().next_back())
.is_some_and(char::is_whitespace);
let after_index = index + keyword.len();
let after_ok = value
.get(after_index..)
.and_then(|suffix| suffix.chars().next())
.is_some_and(char::is_whitespace);
before_ok && after_ok
}
fn static_scss_comparison_truthiness(value: &str) -> Result<Option<bool>, ()> {
let Some((left, operator, right)) = split_static_scss_comparison(value)? else {
return Ok(None);
};
static_scss_comparison_operands_truthiness(left, operator, right)
}
fn split_static_scss_comparison(
value: &str,
) -> Result<Option<(&str, StaticScssComparisonOperator, &str)>, ()> {
let mut comparison = None;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut quote: Option<char> = None;
let mut index = 0usize;
while index < value.len() {
let ch = value[index..].chars().next().ok_or(())?;
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = value[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
if matches!(ch, '"' | '\'') {
quote = Some(ch);
index += ch.len_utf8();
continue;
}
match ch {
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.checked_sub(1).ok_or(())?,
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.checked_sub(1).ok_or(())?,
'=' | '!' | '<' | '>' if paren_depth == 0 && bracket_depth == 0 => {
let (operator, width) = static_scss_comparison_operator_at(value, index)?;
let left = value.get(..index).ok_or(())?.trim();
let right = value.get(index + width..).ok_or(())?.trim();
if left.is_empty() || right.is_empty() || comparison.is_some() {
return Err(());
}
comparison = Some((left, operator, right));
index += width;
continue;
}
_ => {}
}
index += ch.len_utf8();
}
if quote.is_some() || paren_depth != 0 || bracket_depth != 0 {
return Err(());
}
Ok(comparison)
}
fn static_scss_comparison_operator_at(
value: &str,
index: usize,
) -> Result<(StaticScssComparisonOperator, usize), ()> {
let suffix = value.get(index..).ok_or(())?;
if suffix.starts_with("==") {
return Ok((StaticScssComparisonOperator::Equal, 2));
}
if suffix.starts_with("!=") {
return Ok((StaticScssComparisonOperator::NotEqual, 2));
}
if suffix.starts_with("<=") {
return Ok((StaticScssComparisonOperator::LessThanOrEqual, 2));
}
if suffix.starts_with(">=") {
return Ok((StaticScssComparisonOperator::GreaterThanOrEqual, 2));
}
if suffix.starts_with('<') {
return Ok((StaticScssComparisonOperator::LessThan, 1));
}
if suffix.starts_with('>') {
return Ok((StaticScssComparisonOperator::GreaterThan, 1));
}
Err(())
}