omena-scss-eval 0.3.0

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

use omena_parser::LexedToken;
use omena_syntax::SyntaxKind;

use super::{
    OmenaScssEvalResolvedReplacementV0, StaticStylesheetEvaluationEdit, StaticStylesheetScope,
    StaticStylesheetScopedVariableDeclaration, resolved_replacement_value,
    scss_variables::resolve_static_scss_variable_value_at_position,
    static_stylesheet_position_is_inside_ranges, static_stylesheet_scope_for_position,
    static_stylesheet_skip_trivia_tokens, static_stylesheet_token_end,
    static_stylesheet_token_is_trivia, static_stylesheet_token_start,
    static_stylesheet_variable_name_is_safe,
};

pub(super) struct StaticScssInterpolationEvaluationEdits {
    pub(super) edits: Vec<StaticStylesheetEvaluationEdit>,
    pub(super) replacements: Vec<OmenaScssEvalResolvedReplacementV0>,
    pub(super) preserved_dynamic_interpolation_count: usize,
}

pub(super) fn collect_static_scss_interpolation_ranges(
    tokens: &[LexedToken],
) -> Vec<(usize, usize)> {
    let mut ranges = Vec::new();
    let mut index = 0usize;
    while index < tokens.len() {
        if tokens[index].kind != SyntaxKind::ScssInterpolationStart {
            index += 1;
            continue;
        }
        let Some(end_index) = static_scss_interpolation_end_token_index(tokens, index) else {
            index += 1;
            continue;
        };
        ranges.push((
            static_stylesheet_token_start(&tokens[index]),
            static_stylesheet_token_end(&tokens[end_index]),
        ));
        index = end_index + 1;
    }
    ranges
}

pub(super) fn collect_static_scss_interpolation_evaluation_edits(
    tokens: &[LexedToken],
    scopes: &[StaticStylesheetScope],
    declarations: &[StaticStylesheetScopedVariableDeclaration],
    excluded_ranges: &[(usize, usize)],
) -> Option<StaticScssInterpolationEvaluationEdits> {
    let mut edits = Vec::new();
    let mut replacements = Vec::new();
    let mut preserved_dynamic_interpolation_count = 0usize;
    let mut index = 0usize;
    while index < tokens.len() {
        if tokens[index].kind != SyntaxKind::ScssInterpolationStart {
            index += 1;
            continue;
        }
        let interpolation_start = static_stylesheet_token_start(&tokens[index]);
        let Some(end_index) = static_scss_interpolation_end_token_index(tokens, index) else {
            preserved_dynamic_interpolation_count += 1;
            index += 1;
            continue;
        };
        let interpolation_end = static_stylesheet_token_end(&tokens[end_index]);
        if static_stylesheet_position_is_inside_ranges(interpolation_start, excluded_ranges) {
            index = end_index + 1;
            continue;
        }
        if !static_scss_interpolation_is_inside_declaration_value(tokens, interpolation_start) {
            preserved_dynamic_interpolation_count += 1;
            index = end_index + 1;
            continue;
        }
        let Some(variable_token_index) =
            static_scss_interpolation_single_variable_token_index(tokens, index, end_index)
        else {
            preserved_dynamic_interpolation_count += 1;
            index = end_index + 1;
            continue;
        };
        let variable_name = tokens[variable_token_index].text.as_str();
        if !variable_name.starts_with('$')
            || !static_stylesheet_variable_name_is_safe(&variable_name[1..])
        {
            preserved_dynamic_interpolation_count += 1;
            index = end_index + 1;
            continue;
        }
        let _scope_id = static_stylesheet_scope_for_position(scopes, interpolation_start)?;
        let mut stack = BTreeSet::new();
        let Some(replacement) = resolve_static_scss_variable_value_at_position(
            variable_name,
            interpolation_start,
            scopes,
            declarations,
            &mut stack,
        ) else {
            preserved_dynamic_interpolation_count += 1;
            index = end_index + 1;
            continue;
        };
        replacements.push(resolved_replacement_value(
            variable_name,
            interpolation_start,
            interpolation_end,
            replacement.as_str(),
        ));
        edits.push(StaticStylesheetEvaluationEdit {
            start: interpolation_start,
            end: interpolation_end,
            replacement,
        });
        index = end_index + 1;
    }
    Some(StaticScssInterpolationEvaluationEdits {
        edits,
        replacements,
        preserved_dynamic_interpolation_count,
    })
}

fn static_scss_interpolation_end_token_index(
    tokens: &[LexedToken],
    interpolation_start_index: usize,
) -> Option<usize> {
    let mut depth = 0usize;
    for (index, token) in tokens.iter().enumerate().skip(interpolation_start_index) {
        match token.kind {
            SyntaxKind::ScssInterpolationStart => depth += 1,
            SyntaxKind::ScssInterpolationEnd => {
                depth = depth.checked_sub(1)?;
                if depth == 0 {
                    return Some(index);
                }
            }
            _ => {}
        }
    }
    None
}

fn static_scss_interpolation_single_variable_token_index(
    tokens: &[LexedToken],
    start_index: usize,
    end_index: usize,
) -> Option<usize> {
    let variable_index = static_stylesheet_skip_trivia_tokens(tokens, start_index + 1);
    if variable_index >= end_index || tokens[variable_index].kind != SyntaxKind::ScssVariable {
        return None;
    }
    let after_variable_index = static_stylesheet_skip_trivia_tokens(tokens, variable_index + 1);
    (after_variable_index == end_index).then_some(variable_index)
}

fn static_scss_interpolation_is_inside_declaration_value(
    tokens: &[LexedToken],
    position: usize,
) -> bool {
    let Some(mut index) = tokens.iter().position(|token| {
        position >= static_stylesheet_token_start(token)
            && position < static_stylesheet_token_end(token)
    }) else {
        return false;
    };
    while index > 0 {
        index -= 1;
        match tokens[index].kind {
            kind if static_stylesheet_token_is_trivia(kind) => {}
            SyntaxKind::Colon => return true,
            SyntaxKind::LeftBrace | SyntaxKind::RightBrace | SyntaxKind::Semicolon => return false,
            _ => {}
        }
    }
    false
}