omena-scss-eval 0.3.0

Oracle-first SCSS and Less value evaluator rail for Omena CSS
Documentation
use omena_value_lattice::css_values_canonically_equal;

pub(super) fn static_scss_named_argument(value: &str) -> Option<Option<(&str, &str)>> {
    let Some(index) = static_scss_top_level_separator_index(value, ':')? else {
        return Some(None);
    };
    let name = value.get(..index)?.trim().strip_prefix('$')?;
    let argument_value = value.get(index + ':'.len_utf8()..)?.trim();
    if name.is_empty() || argument_value.is_empty() {
        return None;
    }
    Some(Some((name, argument_value)))
}

pub(super) fn static_scss_named_argument_value<'a>(
    value: &'a str,
    name: &str,
) -> Option<Option<&'a str>> {
    match static_scss_named_argument(value)? {
        Some((argument_name, argument_value)) if argument_name == name => {
            Some(Some(argument_value))
        }
        Some(_) => None,
        None => Some(None),
    }
}

pub(super) fn static_scss_render_comma_list(items: Vec<String>) -> Option<String> {
    Some(if items.is_empty() {
        "()".to_string()
    } else {
        format!("({})", items.join(", "))
    })
}

pub(super) fn static_scss_render_map_entries(entries: Vec<(String, String)>) -> Option<String> {
    Some(if entries.is_empty() {
        "()".to_string()
    } else {
        let entries = entries
            .into_iter()
            .map(|(key, value)| format!("{key}: {value}"))
            .collect::<Vec<_>>();
        format!("({})", entries.join(", "))
    })
}

pub(super) fn static_scss_list_separator(value: &str) -> Option<&'static str> {
    let source = strip_static_scss_outer_container(value.trim()).unwrap_or_else(|| value.trim());
    if source.is_empty() {
        return None;
    }
    if split_static_scss_top_level(source, ',').is_some_and(|items| items.len() > 1) {
        return Some("comma");
    }
    if split_static_scss_top_level(source, '/').is_some_and(|items| items.len() > 1) {
        return Some("slash");
    }
    if split_static_scss_top_level_whitespace(source).is_some_and(|items| items.len() > 1) {
        return Some("space");
    }
    static_scss_collection_member_is_static(source).then_some("space")
}

pub(super) fn parse_static_scss_map_entries(value: &str) -> Option<Vec<(String, String)>> {
    let source = strip_static_scss_outer_container(value.trim())?;
    if source.is_empty() {
        return Some(Vec::new());
    }
    let entries = split_static_scss_top_level(source, ',')?;
    let mut pairs = Vec::with_capacity(entries.len());
    for entry in entries {
        let colon_index = static_scss_top_level_separator_index(entry.as_str(), ':')??;
        let key = entry.get(..colon_index)?.trim();
        let value = entry.get(colon_index + ':'.len_utf8()..)?.trim();
        if key.is_empty()
            || value.is_empty()
            || key.contains('$')
            || !static_scss_collection_member_is_static(value)
        {
            return None;
        }
        pairs.push((key.to_string(), value.to_string()));
    }
    Some(pairs)
}

pub(super) fn static_scss_update_nested_map_entries<F>(
    mut entries: Vec<(String, String)>,
    path: &[String],
    update: F,
) -> Option<Vec<(String, String)>>
where
    F: FnOnce(Vec<(String, String)>) -> Option<Vec<(String, String)>>,
{
    let Some((key, remaining_path)) = path.split_first() else {
        return update(entries);
    };
    let canonical_key = canonical_static_scss_map_key(key)?;
    let existing_index = static_scss_map_entry_index(entries.as_slice(), canonical_key.as_str())?;
    let child_entries = match existing_index {
        Some(index) => static_scss_nested_map_child_entries(entries[index].1.as_str())?,
        None => Vec::new(),
    };
    let updated_child_entries =
        static_scss_update_nested_map_entries(child_entries, remaining_path, update)?;
    let updated_child_value = static_scss_render_map_entries(updated_child_entries)?;
    if let Some(index) = existing_index {
        entries[index].1 = updated_child_value;
    } else {
        entries.push((key.trim().to_string(), updated_child_value));
    }
    Some(entries)
}

fn static_scss_nested_map_child_entries(value: &str) -> Option<Vec<(String, String)>> {
    if let Some(entries) = parse_static_scss_map_entries(value) {
        return Some(entries);
    }
    static_scss_collection_member_is_static(value).then(Vec::new)
}

pub(super) fn static_scss_existing_nested_map_child_entries(
    value: &str,
) -> Option<Option<Vec<(String, String)>>> {
    if let Some(entries) = parse_static_scss_map_entries(value) {
        return Some(Some(entries));
    }
    static_scss_collection_member_is_static(value).then_some(None)
}

pub(super) fn static_scss_map_entry_index(
    entries: &[(String, String)],
    canonical_key: &str,
) -> Option<Option<usize>> {
    for (index, (key, _)) in entries.iter().enumerate() {
        if canonical_static_scss_map_key(key.as_str())? == canonical_key {
            return Some(Some(index));
        }
    }
    Some(None)
}

pub(super) fn static_scss_map_entry_value(map: &str, key: &str) -> Option<String> {
    parse_static_scss_map_entries(map)?
        .into_iter()
        .find_map(|(candidate_key, candidate_value)| {
            canonical_static_scss_map_key(candidate_key.as_str())
                .is_some_and(|candidate| candidate == key)
                .then_some(candidate_value)
        })
}

pub(super) fn static_scss_map_contains_key(map: &str, key: &str) -> bool {
    parse_static_scss_map_entries(map).is_some_and(|entries| {
        entries.into_iter().any(|(candidate_key, _)| {
            canonical_static_scss_map_key(candidate_key.as_str())
                .is_some_and(|candidate| candidate == key)
        })
    })
}

pub(super) fn canonical_static_scss_map_key(value: &str) -> Option<String> {
    let value = value.trim();
    if value.is_empty()
        || value.contains('$')
        || static_scss_top_level_separator_index(value, ':')?.is_some()
    {
        return None;
    }
    Some(strip_static_scss_quotes(value).unwrap_or(value).to_string())
}

pub(super) fn static_scss_comparable_collection_value(value: &str) -> Option<String> {
    let value = value.trim();
    if !static_scss_collection_member_is_static(value) {
        return None;
    }
    Some(strip_static_scss_quotes(value).unwrap_or(value).to_string())
}

pub(super) fn static_scss_collection_values_equal(left: &str, right: &str) -> bool {
    left == right || css_values_canonically_equal(left, right)
}

pub(super) fn static_scss_collection_member_is_static(value: &str) -> bool {
    !value.trim().is_empty()
        && !value.contains('$')
        && static_scss_top_level_separator_index(value, ':').is_some_and(|index| index.is_none())
}

pub(super) fn strip_static_scss_quotes(value: &str) -> Option<&str> {
    let quote = value.chars().next()?;
    if !matches!(quote, '"' | '\'') || !value.ends_with(quote) || value.len() < 2 {
        return None;
    }
    value.get(quote.len_utf8()..value.len().saturating_sub(quote.len_utf8()))
}

pub(super) fn strip_static_scss_outer_container(value: &str) -> Option<&str> {
    let trimmed = value.trim();
    if trimmed.len() < 2 {
        return None;
    }
    let (open, close) = match trimmed.chars().next()? {
        '(' => ('(', ')'),
        '[' => ('[', ']'),
        _ => return None,
    };
    let end = static_scss_balanced_value_end(trimmed, 0, open, close)?;
    if end != trimmed.len() {
        return None;
    }
    trimmed
        .get(open.len_utf8()..trimmed.len().saturating_sub(close.len_utf8()))
        .map(str::trim)
}

pub(super) fn split_static_scss_top_level(source: &str, separator: char) -> Option<Vec<String>> {
    let mut values = Vec::new();
    let mut cursor = 0usize;
    let mut index = 0usize;
    while index < source.len() {
        let ch = source[index..].chars().next()?;
        if ch == separator {
            let value = source.get(cursor..index)?.trim();
            if value.is_empty() {
                return None;
            }
            values.push(value.to_string());
            cursor = index + ch.len_utf8();
        }
        index = static_scss_next_value_index(source, index)?;
    }
    let value = source.get(cursor..)?.trim();
    if value.is_empty() {
        return None;
    }
    values.push(value.to_string());
    Some(values)
}

pub(super) fn split_static_scss_top_level_whitespace(source: &str) -> Option<Vec<String>> {
    let mut values = Vec::new();
    let mut cursor = 0usize;
    let mut index = 0usize;
    while index < source.len() {
        let ch = source[index..].chars().next()?;
        if ch.is_ascii_whitespace() {
            let value = source.get(cursor..index)?.trim();
            if !value.is_empty() {
                values.push(value.to_string());
            }
            index += ch.len_utf8();
            while index < source.len() {
                let Some(next_ch) = source[index..].chars().next() else {
                    break;
                };
                if !next_ch.is_ascii_whitespace() {
                    break;
                }
                index += next_ch.len_utf8();
            }
            cursor = index;
            continue;
        }
        index = static_scss_next_value_index(source, index)?;
    }
    let value = source.get(cursor..)?.trim();
    if !value.is_empty() {
        values.push(value.to_string());
    }
    Some(values)
}

pub(super) fn static_scss_top_level_separator_index(
    source: &str,
    separator: char,
) -> Option<Option<usize>> {
    let mut index = 0usize;
    while index < source.len() {
        let ch = source[index..].chars().next()?;
        if ch == separator {
            return Some(Some(index));
        }
        index = static_scss_next_value_index(source, index)?;
    }
    Some(None)
}

fn static_scss_next_value_index(source: &str, index: usize) -> Option<usize> {
    let ch = source[index..].chars().next()?;
    match ch {
        '"' | '\'' => static_scss_quoted_value_end(source, index, ch),
        '(' => static_scss_balanced_value_end(source, index, '(', ')'),
        '[' => static_scss_balanced_value_end(source, index, '[', ']'),
        ')' | ']' => None,
        _ => Some(index + ch.len_utf8()),
    }
}

pub(super) fn static_scss_quoted_value_end(
    source: &str,
    start: usize,
    quote: char,
) -> Option<usize> {
    let mut index = start + quote.len_utf8();
    while index < source.len() {
        let ch = source[index..].chars().next()?;
        index += ch.len_utf8();
        if ch == '\\' {
            if let Some(escaped) = source[index..].chars().next() {
                index += escaped.len_utf8();
            }
        } else if ch == quote {
            return Some(index);
        }
    }
    None
}

fn static_scss_balanced_value_end(
    source: &str,
    start: usize,
    open: char,
    close: char,
) -> Option<usize> {
    let mut depth = 0usize;
    let mut index = start;
    while index < source.len() {
        let ch = source[index..].chars().next()?;
        match ch {
            '"' | '\'' => index = static_scss_quoted_value_end(source, index, ch)?,
            _ if ch == open => {
                depth += 1;
                index += ch.len_utf8();
                continue;
            }
            _ if ch == close => {
                depth = depth.checked_sub(1)?;
                index += ch.len_utf8();
                if depth == 0 {
                    return Some(index);
                }
                continue;
            }
            _ => index += ch.len_utf8(),
        }
    }
    None
}