omena-scss-eval 0.3.0

Oracle-first SCSS and Less value evaluator rail for Omena CSS
Documentation
use omena_parser::{ParsedVariableFactKind, StyleDialect};

use super::{
    declarations::extract_static_stylesheet_variable_declaration,
    model::{StaticScssMixinBodyLocalDeclaration, StaticStylesheetVariableKind},
    safety::static_stylesheet_scss_declaration_value_is_removal_safe,
    scss_arguments::static_scss_top_level_colon_index,
    tokens::parser_text_size_to_usize,
    variable_references::static_stylesheet_variable_reference_is_named_argument_label,
};

pub(super) fn collect_static_scss_mixin_body_local_declarations(
    body: &str,
    dialect: StyleDialect,
) -> Option<Vec<StaticScssMixinBodyLocalDeclaration>> {
    let facts = omena_parser::collect_style_facts(body, dialect);
    let mut declarations = Vec::new();
    for fact in facts
        .variables
        .iter()
        .filter(|fact| fact.kind == ParsedVariableFactKind::ScssDeclaration)
    {
        let start = parser_text_size_to_usize(fact.range.start().into());
        let end = parser_text_size_to_usize(fact.range.end().into());
        if static_stylesheet_variable_reference_is_named_argument_label(body, start, end) {
            continue;
        }
        let declaration = extract_static_stylesheet_variable_declaration(
            body,
            start,
            end,
            dialect,
            StaticStylesheetVariableKind::Scss,
        )?;
        if !static_stylesheet_scss_declaration_value_is_removal_safe(&declaration.value) {
            return None;
        }
        declarations.push(StaticScssMixinBodyLocalDeclaration {
            name: fact.name.clone(),
            declaration,
        });
    }
    declarations.sort_by_key(|declaration| declaration.declaration.span_start);
    Some(declarations)
}

pub(super) fn collect_static_scss_mixin_body_declaration_value_ranges(
    body: &str,
    dialect: StyleDialect,
) -> Option<Vec<(usize, usize)>> {
    let mut ranges = Vec::new();
    let mut statement_start = 0usize;
    let mut index = 0usize;
    let mut paren_depth = 0usize;
    let mut bracket_depth = 0usize;
    let mut quote: Option<char> = None;

    while index < body.len() {
        let ch = body[index..].chars().next()?;
        if let Some(quote_ch) = quote {
            index += ch.len_utf8();
            if ch == '\\' {
                if let Some(escaped) = body[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)?,
            '[' => bracket_depth += 1,
            ']' => bracket_depth = bracket_depth.checked_sub(1)?,
            ';' if paren_depth == 0 && bracket_depth == 0 => {
                collect_static_scss_mixin_body_statement_value_range(
                    body,
                    statement_start,
                    index,
                    &mut ranges,
                )?;
                statement_start = index + ch.len_utf8();
            }
            '\n' if dialect == StyleDialect::Sass && paren_depth == 0 && bracket_depth == 0 => {
                collect_static_scss_mixin_body_statement_value_range(
                    body,
                    statement_start,
                    index,
                    &mut ranges,
                )?;
                statement_start = index + ch.len_utf8();
            }
            _ => {}
        }
        index += ch.len_utf8();
    }

    if quote.is_some() || paren_depth != 0 || bracket_depth != 0 {
        return None;
    }
    let trailing = body.get(statement_start..)?;
    if trailing.trim().is_empty() {
        return Some(ranges);
    }
    if dialect == StyleDialect::Sass {
        collect_static_scss_mixin_body_statement_value_range(
            body,
            statement_start,
            body.len(),
            &mut ranges,
        )?;
        return Some(ranges);
    }
    None
}

fn collect_static_scss_mixin_body_statement_value_range(
    body: &str,
    statement_start: usize,
    statement_end: usize,
    ranges: &mut Vec<(usize, usize)>,
) -> Option<()> {
    let statement = body.get(statement_start..statement_end)?;
    if statement.trim().is_empty() {
        return Some(());
    }
    let colon_index = static_scss_top_level_colon_index(statement)??;
    let mut value_start = statement_start + colon_index + ':'.len_utf8();
    let mut value_end = statement_end;
    while value_start < value_end {
        let ch = body[value_start..].chars().next()?;
        if !ch.is_ascii_whitespace() {
            break;
        }
        value_start += ch.len_utf8();
    }
    while value_start < value_end {
        let ch = body[..value_end].chars().next_back()?;
        if !ch.is_ascii_whitespace() {
            break;
        }
        value_end -= ch.len_utf8();
    }
    if value_start >= value_end {
        return None;
    }
    ranges.push((value_start, value_end));
    Some(())
}