@@ -69,7 +69,6 @@ struct ParseContext {
69 variables: HashMap<String, String>,
69 variables: HashMap<String, String>,
70 defines: HashMap<String, Vec<Attribute>>,
70 defines: HashMap<String, Vec<Attribute>>,
71 functions: HashMap<String, FnDef>,
71 functions: HashMap<String, FnDef>,
72− keyframes: Vec<(String, String)>,
73 css_vars: Vec<(String, String)>,
72 css_vars: Vec<(String, String)>,
74 custom_css: Vec<String>,
73 custom_css: Vec<String>,
75 og_tags: Vec<(String, String)>,
74 og_tags: Vec<(String, String)>,
@@ -157,7 +156,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
157 variables: HashMap::new(),
156 variables: HashMap::new(),
158 defines: HashMap::new(),
157 defines: HashMap::new(),
159 functions: HashMap::new(),
158 functions: HashMap::new(),
160− keyframes: Vec::new(),
161 css_vars: Vec::new(),
159 css_vars: Vec::new(),
162 custom_css: Vec::new(),
160 custom_css: Vec::new(),
163 og_tags: Vec::new(),
161 og_tags: Vec::new(),
@@ -189,7 +187,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
189 head_blocks: ctx.head_blocks,
187 head_blocks: ctx.head_blocks,
190 variables: ctx.variables,
188 variables: ctx.variables,
191 defines: ctx.defines,
189 defines: ctx.defines,
192− keyframes: ctx.keyframes,
193 css_vars: ctx.css_vars,
190 css_vars: ctx.css_vars,
194 custom_css: ctx.custom_css,
191 custom_css: ctx.custom_css,
195 og_tags: ctx.og_tags,
192 og_tags: ctx.og_tags,
@@ -751,34 +748,6 @@ impl Evaluator {
752 // --- @each loop ---
749 // --- @each loop ---
754− // --- @keyframes directive ---
756− if let Some(rest) = content.strip_prefix("@keyframes ") {
757− let name = rest.trim().to_string();
758− if name.is_empty() {
759− return Err(ParseError {
761− message: "@keyframes requires a name".to_string(),
764− let mut body = String::new();
765− for_each_line(children, &mut |node| {
766− if let Syntax::Line { text, .. } = node {
767− let trimmed = text.trim();
768− // Support htmlang-style: from [opacity 0] / to [opacity 1] / 50% [transform scale(1.5)]
769− if let Some(kf_css) = parse_keyframe_line(trimmed) {
770− body.push_str(&kf_css);
772− body.push_str(trimmed);
776− ctx.keyframes.push((name, body));
782 // --- Function call ---
751 // --- Function call ---
784 if content.starts_with('@') {
753 if content.starts_with('@') {
@@ -1421,7 +1390,6 @@ const KNOWN_DIRECTIVES: &[&str] = &[
1422 "include",
1391 "include",
@@ -1525,6 +1493,7 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
1525 "put each locale's strings in a JSON file and use `@data $t locales/$lang.json`",
1493 "put each locale's strings in a JSON file and use `@data $t locales/$lang.json`",
1527 ("@defer", "remove it: the content is already in the page"),
1495 ("@defer", "remove it: the content is already in the page"),
1496+ ("@keyframes", "write the CSS `@keyframes` rule in `@style`"),
1528 ("@log", "removed: a layout needs no compile-time messages"),
1497 ("@log", "removed: a layout needs no compile-time messages"),
1529 ("@warn", "removed: a layout needs no compile-time messages"),
1498 ("@warn", "removed: a layout needs no compile-time messages"),
1530 ("@assert", "removed: a layout needs no compile-time checks"),
1499 ("@assert", "removed: a layout needs no compile-time checks"),
@@ -3161,47 +3130,6 @@ fn matching_brace(s: &str) -> Option<usize> {
3164−/// Parse a keyframe line in htmlang syntax: `from [opacity 0]` / `50% [transform scale(1.5)]`
3165−fn parse_keyframe_line(line: &str) -> Option<String> {
3166− let (selector, rest) = if let Some(rest) = line.strip_prefix("from") {
3167− ("from", rest.trim())
3168− } else if let Some(rest) = line.strip_prefix("to") {
3169− ("to", rest.trim())
3170− } else if let Some(pct_end) = line.find('%') {
3171− let rest = line[pct_end + 1..].trim();
3172− let selector = &line[..pct_end + 1];
3178− if !rest.starts_with('[') || !rest.ends_with(']') {
3182− let inner = &rest[1..rest.len() - 1];
3183− // Parse comma-separated key-value pairs into CSS
3184− let mut css = String::new();
3185− for part in split_commas(inner) {
3186− let part = part.trim();
3187− if part.is_empty() {
3190− if let Some((key, value)) = part.split_once(' ') {
3191− css.push_str(key.trim());
3193− css.push_str(value.trim());
3198− if css.is_empty() {
3202− Some(format!("{}{{{}}}", selector, css))
3205// ---------------------------------------------------------------------------
3133// ---------------------------------------------------------------------------
3206// Variable usage tracking
3134// Variable usage tracking
3207// ---------------------------------------------------------------------------
3135// ---------------------------------------------------------------------------