1use cstree::text::TextRange;
7use omena_syntax::SyntaxKind;
8use std::collections::BTreeSet;
9
10use crate::{Token, matches_ignore_ascii_case, next_non_trivia_token_index_until};
11
12use super::StyleFactSink;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ParsedAnimationFact {
16 pub kind: ParsedAnimationFactKind,
17 pub name: String,
18 pub range: TextRange,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
22pub enum ParsedAnimationFactKind {
23 KeyframesDeclaration,
24 AnimationNameReference,
25}
26
27pub(crate) fn collect_animation_facts_from_sink(
28 sink: &StyleFactSink<'_>,
29) -> Vec<ParsedAnimationFact> {
30 let mut animations = Vec::new();
31 let mut seen = BTreeSet::new();
32 for node in sink.nodes().filter(|node| {
33 matches!(
34 node.kind,
35 SyntaxKind::KeyframesRule | SyntaxKind::Declaration
36 )
37 }) {
38 collect_animation_facts_from_syntax_tokens(
39 sink.node_tokens(node),
40 &mut animations,
41 &mut seen,
42 );
43 }
44 animations
45}
46
47fn collect_animation_facts_from_syntax_tokens(
48 tokens: &[Token<'_>],
49 animations: &mut Vec<ParsedAnimationFact>,
50 seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
51) {
52 for (index, token) in tokens.iter().enumerate() {
53 if token.kind == SyntaxKind::AtKeyword && at_keyword_is_keyframes_rule(token.text) {
54 if let Some(name_index) =
55 next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
56 && let Some(name) = animation_name_from_token(tokens[name_index])
57 {
58 push_animation_fact(
59 animations,
60 seen,
61 ParsedAnimationFactKind::KeyframesDeclaration,
62 name,
63 tokens[name_index].range,
64 );
65 }
66 continue;
67 }
68
69 if token.kind == SyntaxKind::Ident
70 && matches_ignore_ascii_case(token.text, &["animation-name"])
71 && let Some(colon_index) =
72 next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
73 && tokens[colon_index].kind == SyntaxKind::Colon
74 {
75 collect_animation_name_references_until(tokens, colon_index + 1, animations, seen);
76 }
77
78 if token.kind == SyntaxKind::Ident
79 && matches_ignore_ascii_case(token.text, &["animation"])
80 && let Some(colon_index) =
81 next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
82 && tokens[colon_index].kind == SyntaxKind::Colon
83 {
84 collect_animation_shorthand_references_until(tokens, colon_index + 1, animations, seen);
85 }
86 }
87}
88
89fn collect_animation_name_references_until(
90 tokens: &[Token<'_>],
91 start: usize,
92 animations: &mut Vec<ParsedAnimationFact>,
93 seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
94) {
95 let mut index = start;
96 let mut paren_depth = 0usize;
97 let mut bracket_depth = 0usize;
98 while index < tokens.len() {
99 match tokens[index].kind {
100 SyntaxKind::LeftParen => paren_depth += 1,
101 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
102 SyntaxKind::LeftBracket => bracket_depth += 1,
103 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
104 SyntaxKind::Semicolon
105 | SyntaxKind::SassOptionalSemicolon
106 | SyntaxKind::RightBrace
107 | SyntaxKind::SassDedent
108 if paren_depth == 0 && bracket_depth == 0 =>
109 {
110 break;
111 }
112 _ => {}
113 }
114
115 if paren_depth == 0
116 && bracket_depth == 0
117 && !animation_name_token_is_interpolation_adjacent(tokens, index)
118 && let Some(name) = animation_name_from_token(tokens[index])
119 {
120 push_animation_fact(
121 animations,
122 seen,
123 ParsedAnimationFactKind::AnimationNameReference,
124 name,
125 tokens[index].range,
126 );
127 }
128 index += 1;
129 }
130}
131
132fn collect_animation_shorthand_references_until(
133 tokens: &[Token<'_>],
134 start: usize,
135 animations: &mut Vec<ParsedAnimationFact>,
136 seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
137) {
138 let mut index = start;
139 let mut paren_depth = 0usize;
140 let mut bracket_depth = 0usize;
141 while index < tokens.len() {
142 match tokens[index].kind {
143 SyntaxKind::LeftParen => paren_depth += 1,
144 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
145 SyntaxKind::LeftBracket => bracket_depth += 1,
146 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
147 SyntaxKind::Semicolon
148 | SyntaxKind::SassOptionalSemicolon
149 | SyntaxKind::RightBrace
150 | SyntaxKind::SassDedent
151 if paren_depth == 0 && bracket_depth == 0 =>
152 {
153 break;
154 }
155 _ => {}
156 }
157
158 if paren_depth == 0
159 && bracket_depth == 0
160 && animation_shorthand_token_can_be_name(tokens, index)
161 && let Some(name) = animation_name_from_token(tokens[index])
162 {
163 push_animation_fact(
164 animations,
165 seen,
166 ParsedAnimationFactKind::AnimationNameReference,
167 name,
168 tokens[index].range,
169 );
170 }
171 index += 1;
172 }
173}
174
175fn animation_shorthand_token_can_be_name(tokens: &[Token<'_>], index: usize) -> bool {
176 let token = tokens[index];
177 if token.kind == SyntaxKind::String {
178 return true;
179 }
180 if token.kind != SyntaxKind::Ident {
181 return false;
182 }
183 if animation_name_token_is_interpolation_adjacent(tokens, index) {
188 return false;
189 }
190 if animation_shorthand_ident_is_time_unit(token.text) {
192 return false;
193 }
194 if let Some(next_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
195 && tokens[next_index].kind == SyntaxKind::LeftParen
196 {
197 return false;
198 }
199 !animation_shorthand_ident_is_non_name(token.text)
200}
201
202fn animation_shorthand_ident_is_time_unit(name: &str) -> bool {
203 matches_ignore_ascii_case(name, &["s", "ms"])
204}
205
206fn animation_name_token_is_interpolation_adjacent(tokens: &[Token<'_>], index: usize) -> bool {
216 if index > 0
217 && matches!(
218 tokens[index - 1].kind,
219 SyntaxKind::ScssInterpolationEnd | SyntaxKind::LessInterpolationEnd
220 )
221 {
222 return true;
223 }
224 if let Some(next) = tokens.get(index + 1)
225 && matches!(
226 next.kind,
227 SyntaxKind::ScssInterpolationStart | SyntaxKind::LessInterpolationStart
228 )
229 {
230 return true;
231 }
232 false
233}
234
235fn animation_shorthand_ident_is_non_name(name: &str) -> bool {
236 matches_ignore_ascii_case(
237 name,
238 &[
239 "ease",
240 "ease-in",
241 "ease-out",
242 "ease-in-out",
243 "linear",
244 "step-start",
245 "step-end",
246 "infinite",
247 "normal",
248 "reverse",
249 "alternate",
250 "alternate-reverse",
251 "running",
252 "paused",
253 "forwards",
254 "backwards",
255 "both",
256 "replace",
257 "add",
258 "accumulate",
259 "auto",
260 ],
261 )
262}
263
264fn push_animation_fact(
265 animations: &mut Vec<ParsedAnimationFact>,
266 seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
267 kind: ParsedAnimationFactKind,
268 name: String,
269 range: TextRange,
270) {
271 if seen.insert((
272 kind,
273 name.clone(),
274 u32::from(range.start()),
275 u32::from(range.end()),
276 )) {
277 animations.push(ParsedAnimationFact { kind, name, range });
278 }
279}
280
281fn animation_name_from_token(token: Token<'_>) -> Option<String> {
282 if !matches!(token.kind, SyntaxKind::Ident | SyntaxKind::String) {
283 return None;
284 }
285 let name = token
286 .text
287 .trim_matches(|character| character == '"' || character == '\'')
288 .to_string();
289 if name.is_empty() || animation_name_is_reserved(&name) {
290 return None;
291 }
292 Some(name)
293}
294
295fn animation_name_is_reserved(name: &str) -> bool {
296 matches_ignore_ascii_case(
297 name,
298 &[
299 "none",
300 "initial",
301 "inherit",
302 "unset",
303 "revert",
304 "revert-layer",
305 ],
306 )
307}
308
309fn at_keyword_is_keyframes_rule(text: &str) -> bool {
317 let Some(rule) = text.strip_prefix('@') else {
318 return false;
319 };
320 if matches_ignore_ascii_case(rule, &["keyframes"]) {
321 return true;
322 }
323 if let Some(rest) = rule.strip_prefix('-')
325 && let Some((vendor, remainder)) = rest.split_once('-')
326 && !vendor.is_empty()
327 && matches_ignore_ascii_case(remainder, &["keyframes"])
328 {
329 return true;
330 }
331 false
332}