use cstree::text::TextRange;
use omena_syntax::SyntaxKind;
use std::collections::BTreeSet;
use crate::{ParseResult, Token, matches_ignore_ascii_case, next_non_trivia_token_index_until};
use super::tokens_from_syntax_node;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedAnimationFact {
pub kind: ParsedAnimationFactKind,
pub name: String,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedAnimationFactKind {
KeyframesDeclaration,
AnimationNameReference,
}
pub(crate) fn collect_animation_facts_from_cst(
text: &str,
parsed: &ParseResult,
) -> Vec<ParsedAnimationFact> {
let mut animations = Vec::new();
let mut seen = BTreeSet::new();
for tokens in animation_fact_tokens_from_cst(text, parsed) {
collect_animation_facts_from_syntax_tokens(&tokens, &mut animations, &mut seen);
}
animations
}
fn collect_animation_facts_from_syntax_tokens(
tokens: &[Token<'_>],
animations: &mut Vec<ParsedAnimationFact>,
seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
) {
for (index, token) in tokens.iter().enumerate() {
if token.kind == SyntaxKind::AtKeyword && at_keyword_is_keyframes_rule(token.text) {
if let Some(name_index) =
next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& let Some(name) = animation_name_from_token(tokens[name_index])
{
push_animation_fact(
animations,
seen,
ParsedAnimationFactKind::KeyframesDeclaration,
name,
tokens[name_index].range,
);
}
continue;
}
if token.kind == SyntaxKind::Ident
&& matches_ignore_ascii_case(token.text, &["animation-name"])
&& let Some(colon_index) =
next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& tokens[colon_index].kind == SyntaxKind::Colon
{
collect_animation_name_references_until(tokens, colon_index + 1, animations, seen);
}
if token.kind == SyntaxKind::Ident
&& matches_ignore_ascii_case(token.text, &["animation"])
&& let Some(colon_index) =
next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& tokens[colon_index].kind == SyntaxKind::Colon
{
collect_animation_shorthand_references_until(tokens, colon_index + 1, animations, seen);
}
}
}
fn animation_fact_tokens_from_cst<'text>(
text: &'text str,
parsed: &ParseResult,
) -> Vec<Vec<Token<'text>>> {
parsed
.syntax()
.descendants()
.filter(|node| {
matches!(
node.kind(),
SyntaxKind::KeyframesRule | SyntaxKind::Declaration
)
})
.map(|node| tokens_from_syntax_node(text, parsed, node))
.collect()
}
fn collect_animation_name_references_until(
tokens: &[Token<'_>],
start: usize,
animations: &mut Vec<ParsedAnimationFact>,
seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
) {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < tokens.len() {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent
if paren_depth == 0 && bracket_depth == 0 =>
{
break;
}
_ => {}
}
if paren_depth == 0
&& bracket_depth == 0
&& !animation_name_token_is_interpolation_adjacent(tokens, index)
&& let Some(name) = animation_name_from_token(tokens[index])
{
push_animation_fact(
animations,
seen,
ParsedAnimationFactKind::AnimationNameReference,
name,
tokens[index].range,
);
}
index += 1;
}
}
fn collect_animation_shorthand_references_until(
tokens: &[Token<'_>],
start: usize,
animations: &mut Vec<ParsedAnimationFact>,
seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
) {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < tokens.len() {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent
if paren_depth == 0 && bracket_depth == 0 =>
{
break;
}
_ => {}
}
if paren_depth == 0
&& bracket_depth == 0
&& animation_shorthand_token_can_be_name(tokens, index)
&& let Some(name) = animation_name_from_token(tokens[index])
{
push_animation_fact(
animations,
seen,
ParsedAnimationFactKind::AnimationNameReference,
name,
tokens[index].range,
);
}
index += 1;
}
}
fn animation_shorthand_token_can_be_name(tokens: &[Token<'_>], index: usize) -> bool {
let token = tokens[index];
if token.kind == SyntaxKind::String {
return true;
}
if token.kind != SyntaxKind::Ident {
return false;
}
if animation_name_token_is_interpolation_adjacent(tokens, index) {
return false;
}
if animation_shorthand_ident_is_time_unit(token.text) {
return false;
}
if let Some(next_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& tokens[next_index].kind == SyntaxKind::LeftParen
{
return false;
}
!animation_shorthand_ident_is_non_name(token.text)
}
fn animation_shorthand_ident_is_time_unit(name: &str) -> bool {
matches_ignore_ascii_case(name, &["s", "ms"])
}
fn animation_name_token_is_interpolation_adjacent(tokens: &[Token<'_>], index: usize) -> bool {
if index > 0
&& matches!(
tokens[index - 1].kind,
SyntaxKind::ScssInterpolationEnd | SyntaxKind::LessInterpolationEnd
)
{
return true;
}
if let Some(next) = tokens.get(index + 1)
&& matches!(
next.kind,
SyntaxKind::ScssInterpolationStart | SyntaxKind::LessInterpolationStart
)
{
return true;
}
false
}
fn animation_shorthand_ident_is_non_name(name: &str) -> bool {
matches_ignore_ascii_case(
name,
&[
"ease",
"ease-in",
"ease-out",
"ease-in-out",
"linear",
"step-start",
"step-end",
"infinite",
"normal",
"reverse",
"alternate",
"alternate-reverse",
"running",
"paused",
"forwards",
"backwards",
"both",
"replace",
"add",
"accumulate",
"auto",
],
)
}
fn push_animation_fact(
animations: &mut Vec<ParsedAnimationFact>,
seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
kind: ParsedAnimationFactKind,
name: String,
range: TextRange,
) {
if seen.insert((
kind,
name.clone(),
u32::from(range.start()),
u32::from(range.end()),
)) {
animations.push(ParsedAnimationFact { kind, name, range });
}
}
fn animation_name_from_token(token: Token<'_>) -> Option<String> {
if !matches!(token.kind, SyntaxKind::Ident | SyntaxKind::String) {
return None;
}
let name = token
.text
.trim_matches(|character| character == '"' || character == '\'')
.to_string();
if name.is_empty() || animation_name_is_reserved(&name) {
return None;
}
Some(name)
}
fn animation_name_is_reserved(name: &str) -> bool {
matches_ignore_ascii_case(
name,
&[
"none",
"initial",
"inherit",
"unset",
"revert",
"revert-layer",
],
)
}
fn at_keyword_is_keyframes_rule(text: &str) -> bool {
let Some(rule) = text.strip_prefix('@') else {
return false;
};
if matches_ignore_ascii_case(rule, &["keyframes"]) {
return true;
}
if let Some(rest) = rule.strip_prefix('-')
&& let Some((vendor, remainder)) = rest.split_once('-')
&& !vendor.is_empty()
&& matches_ignore_ascii_case(remainder, &["keyframes"])
{
return true;
}
false
}