diff --git a/crates/rustmotion-cli/src/commands/validation.rs b/crates/rustmotion-cli/src/commands/validation.rs
index 51bfc72..df8d0d1 100644
--- a/crates/rustmotion-cli/src/commands/validation.rs
+++ b/crates/rustmotion-cli/src/commands/validation.rs
@@ -280,6 +280,28 @@ mod html_css_error_tests {
);
assert!(report.is_blocking(false), "must block rendering");
}
+
+ #[test]
+ fn unknown_animation_preset_from_html_is_a_blocking_error() {
+ // Unknown preset names are not validated in the transpiler (no schema
+ // dependency there); the typed deserialization of `style.animation`
+ // (tagged AnimationEffect enum) must reject them here, as a blocking
+ // and readable error.
+ let html = r##"
Hi
"##;
+ let value = rustmotion::loader::html_to_scenario_json(html).expect("transpiles");
+ let json = serde_json::to_string(&value).unwrap();
+ let loaded = load(ValidationSource::Inline(&json)).expect("loads");
+ let report = run_checks(&loaded, false);
+ assert!(
+ report
+ .schema_errors
+ .iter()
+ .any(|e| e.contains("not_a_preset")),
+ "expected a schema error naming the unknown preset: {:?}",
+ report.schema_errors
+ );
+ assert!(report.is_blocking(false), "must block rendering");
+ }
}
#[cfg(test)]
diff --git a/crates/rustmotion-html/src/element.rs b/crates/rustmotion-html/src/element.rs
index d5341a6..66db5c2 100644
--- a/crates/rustmotion-html/src/element.rs
+++ b/crates/rustmotion-html/src/element.rs
@@ -1,8 +1,8 @@
use markup5ever_rcdom::{Handle, NodeData};
use serde_json::{Map, Value};
-use crate::style::{coerce_value, parse_inline_style};
-use crate::{element_attrs, tag_name};
+use crate::style::{coerce_value, parse_anim_attr, parse_inline_style};
+use crate::{element_attrs, tag_name, HtmlError};
enum TagKind {
Container,
@@ -37,72 +37,83 @@ fn collect_text(handle: &Handle, out: &mut String) {
}
}
-/// Pull `style="..."` from an element's attributes into a JSON style object.
-fn style_object(attrs: &[(String, String)]) -> Option {
- let raw = attrs.iter().find(|(k, _)| k == "style").map(|(_, v)| v)?;
- let map = parse_inline_style(raw);
+/// Pull `style="..."` and `anim="..."` from an element's attributes into one
+/// JSON style object. `anim` (JSON or compact DSL, see
+/// [`crate::style::parse_anim_attr`]) lands in `style.animation` — inline CSS
+/// cannot express animation arrays, so `anim` is the only writer of that key.
+fn style_object(attrs: &[(String, String)]) -> Result