omena-scss-eval 0.3.0

Oracle-first SCSS and Less value evaluator rail for Omena CSS
Documentation
use std::collections::BTreeMap;

use omena_value_lattice::parse_whole_function_value_arguments;

use super::less_guard::{
    static_less_guard_unit_text, static_less_guard_value_has_unit,
    static_less_guard_value_is_color, static_less_guard_value_is_keyword,
    static_less_guard_value_is_number, static_less_guard_value_is_string,
    static_less_guard_value_is_url, static_less_value_condition_matches,
};
use super::less_mixin_values::static_less_value_is_detached_ruleset_reference;
use super::model::{
    StaticLessDetachedRulesetDeclaration, StaticStylesheetPropertyDeclaration,
    StaticStylesheetScope, StaticStylesheetVariableDeclaration, StaticStylesheetVariableKind,
};
use super::variable_references::collect_static_stylesheet_variable_references;
use super::{
    find_static_less_detached_ruleset_declaration,
    find_static_less_property_declaration_for_reference, find_static_less_variable_declaration,
    static_less_variable_name_is_safe, static_stylesheet_literal_value_is_safe,
    static_stylesheet_property_name_is_safe,
};

pub(super) fn parse_static_less_if_value(value: &str) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, "if")?;
    let [condition, truthy, falsey] = arguments.as_slice() else {
        return None;
    };
    Some(
        if static_less_value_condition_matches(condition.trim())? {
            truthy
        } else {
            falsey
        }
        .trim()
        .to_string(),
    )
}

pub(super) fn parse_static_less_boolean_value(value: &str) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, "boolean")?;
    let [condition] = arguments.as_slice() else {
        return None;
    };
    Some(static_less_value_condition_matches(condition.trim())?.to_string())
}

pub(super) fn parse_static_less_isnumber_value(value: &str) -> Option<String> {
    parse_static_less_unary_predicate_value(value, "isnumber", static_less_guard_value_is_number)
}

pub(super) fn parse_static_less_iscolor_value(value: &str) -> Option<String> {
    parse_static_less_unary_predicate_value(value, "iscolor", static_less_guard_value_is_color)
}

pub(super) fn parse_static_less_isstring_value(value: &str) -> Option<String> {
    parse_static_less_unary_predicate_value(value, "isstring", static_less_guard_value_is_string)
}

pub(super) fn parse_static_less_iskeyword_value(value: &str) -> Option<String> {
    parse_static_less_unary_predicate_value(value, "iskeyword", static_less_guard_value_is_keyword)
}

pub(super) fn parse_static_less_isurl_value(value: &str) -> Option<String> {
    parse_static_less_unary_predicate_value(value, "isurl", static_less_guard_value_is_url)
}

pub(super) fn parse_static_less_isdefined_value(value: &str) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, "isdefined")?;
    let [value] = arguments.as_slice() else {
        return None;
    };
    let value = value.trim();
    (!value.starts_with('@') && !value.starts_with('$')).then_some(true.to_string())
}

pub(super) fn parse_static_less_isdefined_value_with_context(
    value: &str,
    scope_id: usize,
    reference_position: usize,
    scopes: &[StaticStylesheetScope],
    variable_declarations: &BTreeMap<(usize, String), StaticStylesheetVariableDeclaration>,
    property_declarations: &BTreeMap<(usize, String), StaticStylesheetPropertyDeclaration>,
    detached_ruleset_declarations: &[StaticLessDetachedRulesetDeclaration],
) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, "isdefined")?;
    let [value] = arguments.as_slice() else {
        return None;
    };
    static_less_isdefined_argument_matches(
        value,
        scope_id,
        reference_position,
        scopes,
        variable_declarations,
        property_declarations,
        detached_ruleset_declarations,
    )
    .map(|defined| defined.to_string())
}

fn static_less_isdefined_argument_matches(
    value: &str,
    scope_id: usize,
    reference_position: usize,
    scopes: &[StaticStylesheetScope],
    variable_declarations: &BTreeMap<(usize, String), StaticStylesheetVariableDeclaration>,
    property_declarations: &BTreeMap<(usize, String), StaticStylesheetPropertyDeclaration>,
    detached_ruleset_declarations: &[StaticLessDetachedRulesetDeclaration],
) -> Option<bool> {
    let value = value.trim();
    if value.is_empty() {
        return None;
    }
    if value.starts_with('$') {
        if !value
            .strip_prefix('$')
            .is_some_and(static_stylesheet_property_name_is_safe)
        {
            return None;
        }
        let declaration = find_static_less_property_declaration_for_reference(
            value,
            scope_id,
            scopes,
            property_declarations,
            reference_position,
        )?;
        return static_less_property_declaration_isdefined_matches(
            declaration,
            scope_id,
            reference_position,
            scopes,
            variable_declarations,
        );
    }
    if !value.starts_with('@') {
        return Some(true);
    }
    if value.starts_with("@@") || !static_less_variable_name_is_safe(value) {
        return None;
    }
    Some(
        find_static_less_variable_declaration(value, scope_id, scopes, variable_declarations)
            .is_some()
            || find_static_less_detached_ruleset_declaration(
                value,
                scope_id,
                scopes,
                detached_ruleset_declarations,
            )
            .is_some(),
    )
}

fn static_less_property_declaration_isdefined_matches(
    declaration: &StaticStylesheetPropertyDeclaration,
    scope_id: usize,
    reference_position: usize,
    scopes: &[StaticStylesheetScope],
    variable_declarations: &BTreeMap<(usize, String), StaticStylesheetVariableDeclaration>,
) -> Option<bool> {
    let references = collect_static_stylesheet_variable_references(
        declaration.value.as_str(),
        StaticStylesheetVariableKind::Less,
    )?;
    Some(!references.into_iter().any(|reference| {
        find_static_less_variable_declaration(
            reference.name.as_str(),
            scope_id,
            scopes,
            variable_declarations,
        )
        .is_some_and(|variable| variable.span_start == reference_position)
    }))
}

pub(super) fn parse_static_less_isruleset_value(value: &str) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, "isruleset")?;
    let [value] = arguments.as_slice() else {
        return None;
    };
    let value = value.trim();
    (!value.starts_with('@') && static_stylesheet_literal_value_is_safe(value))
        .then(|| false.to_string())
}

pub(super) fn parse_static_less_isruleset_value_with_context(
    value: &str,
    scope_id: usize,
    scopes: &[StaticStylesheetScope],
    detached_ruleset_declarations: &[StaticLessDetachedRulesetDeclaration],
) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, "isruleset")?;
    let [value] = arguments.as_slice() else {
        return None;
    };
    let value = value.trim();
    if static_less_value_is_detached_ruleset_reference(
        value,
        scope_id,
        scopes,
        detached_ruleset_declarations,
    ) {
        return Some(true.to_string());
    }
    (!value.starts_with('@') && static_stylesheet_literal_value_is_safe(value))
        .then(|| false.to_string())
}

pub(super) fn parse_static_less_ispixel_value(value: &str) -> Option<String> {
    parse_static_less_unary_predicate_value(value, "ispixel", |value| {
        static_less_guard_value_has_unit(value, "px")
    })
}

pub(super) fn parse_static_less_ispercentage_value(value: &str) -> Option<String> {
    parse_static_less_unary_predicate_value(value, "ispercentage", |value| {
        static_less_guard_value_has_unit(value, "%")
    })
}

pub(super) fn parse_static_less_isem_value(value: &str) -> Option<String> {
    parse_static_less_unary_predicate_value(value, "isem", |value| {
        static_less_guard_value_has_unit(value, "em")
    })
}

pub(super) fn parse_static_less_isunit_value(value: &str) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, "isunit")?;
    let [value, unit] = arguments.as_slice() else {
        return None;
    };
    let unit = static_less_guard_unit_text(unit.trim())?;
    Some(static_less_guard_value_has_unit(value.trim(), unit).to_string())
}

fn parse_static_less_unary_predicate_value(
    value: &str,
    function_name: &str,
    predicate: impl FnOnce(&str) -> bool,
) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, function_name)?;
    let [value] = arguments.as_slice() else {
        return None;
    };
    Some(predicate(value.trim()).to_string())
}