omena-scss-eval 0.3.0

Oracle-first SCSS and Less value evaluator rail for Omena CSS
Documentation
use cstree::syntax::SyntaxNode;
use omena_syntax::SyntaxKind;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct CstTokenRange {
    pub(super) kind: SyntaxKind,
    pub(super) start: usize,
    pub(super) end: usize,
    pub(super) text: String,
}

pub(super) fn cst_token_ranges(root: &SyntaxNode<SyntaxKind>) -> Option<Vec<CstTokenRange>> {
    root.descendants_with_tokens()
        .filter_map(|element| element.into_token())
        .map(|token| {
            Some(CstTokenRange {
                kind: token.kind(),
                start: u32::from(token.text_range().start()) as usize,
                end: u32::from(token.text_range().end()) as usize,
                text: cst_token_text(token)?,
            })
        })
        .collect()
}

pub(super) fn cst_next_non_trivia_token_index(
    tokens: &[CstTokenRange],
    mut index: usize,
) -> Option<usize> {
    while tokens
        .get(index)
        .is_some_and(|token| cst_is_trivia_token(token.kind))
    {
        index += 1;
    }
    (index < tokens.len()).then_some(index)
}

pub(super) fn cst_next_sass_indent_token_index(
    tokens: &[CstTokenRange],
    mut index: usize,
) -> Option<usize> {
    while index < tokens.len() {
        match tokens[index].kind {
            SyntaxKind::SassIndent => return Some(index),
            SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon => return None,
            _ => index += 1,
        }
    }
    None
}

pub(super) fn cst_matching_right_paren_token_index(
    tokens: &[CstTokenRange],
    left_paren_index: usize,
) -> Option<usize> {
    let mut depth = 0usize;
    for (index, token) in tokens.iter().enumerate().skip(left_paren_index) {
        match token.kind {
            SyntaxKind::LeftParen => depth += 1,
            SyntaxKind::RightParen => {
                depth = depth.checked_sub(1)?;
                if depth == 0 {
                    return Some(index);
                }
            }
            _ => {}
        }
    }
    None
}

pub(super) fn cst_matching_sass_dedent_token_index(
    tokens: &[CstTokenRange],
    sass_indent_index: usize,
) -> Option<usize> {
    let mut depth = 0usize;
    for (index, token) in tokens.iter().enumerate().skip(sass_indent_index) {
        match token.kind {
            SyntaxKind::SassIndent => depth += 1,
            SyntaxKind::SassDedent => {
                depth = depth.checked_sub(1)?;
                if depth == 0 {
                    return Some(index);
                }
            }
            _ => {}
        }
    }
    None
}

pub(super) fn cst_declaration_end_token_index(
    tokens: &[CstTokenRange],
    mut index: usize,
) -> Option<usize> {
    let mut paren_depth = 0usize;
    while index < tokens.len() {
        match tokens[index].kind {
            SyntaxKind::LeftParen => paren_depth += 1,
            SyntaxKind::RightParen => paren_depth = paren_depth.checked_sub(1)?,
            SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon | SyntaxKind::SassIndent
                if paren_depth == 0 =>
            {
                return Some(index);
            }
            _ => {}
        }
        index += 1;
    }
    None
}

fn cst_token_text(token: &cstree::syntax::SyntaxToken<SyntaxKind>) -> Option<String> {
    if let Some(resolver) = token.resolver() {
        Some(token.resolve_text(&**resolver).to_string())
    } else {
        token.static_text().map(str::to_string)
    }
}

const fn cst_is_trivia_token(kind: SyntaxKind) -> bool {
    matches!(
        kind,
        SyntaxKind::Whitespace
            | SyntaxKind::LineComment
            | SyntaxKind::BlockComment
            | SyntaxKind::ScssSilentComment
            | SyntaxKind::SassIndentedNewline
    )
}