From b88e776d22b2ffae9ae79950392af7e7b39a942f Mon Sep 17 00:00:00 2001 From: Ken Micklas Date: Sat, 6 Jul 2024 12:45:44 +0100 Subject: [PATCH] Generate attributes from config --- examples/todomvc/src/main.rs | 2 +- ravel-web/build.rs | 128 +++++++++++- ravel-web/generate.toml | 15 ++ ravel-web/src/attr.rs | 335 ------------------------------- ravel-web/src/attr/mod.rs | 30 +++ ravel-web/src/attr/types.rs | 377 +++++++++++++++++++++++++++++++++++ ravel-web/src/el/mod.rs | 2 +- 7 files changed, 546 insertions(+), 343 deletions(-) delete mode 100644 ravel-web/src/attr.rs create mode 100644 ravel-web/src/attr/mod.rs create mode 100644 ravel-web/src/attr/types.rs diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs index 8dfa922..aa6411c 100644 --- a/examples/todomvc/src/main.rs +++ b/examples/todomvc/src/main.rs @@ -100,7 +100,7 @@ fn item(filter: Filter, id: usize, item: &Item) -> View!(Model, '_) { )), )), form(( - input((class("edit"), value_(&item.text))), + input((class("edit"), value_(CloneString(&item.text)))), on(Active(Submit), move |model: &mut Model, e| { e.prevent_default(); diff --git a/ravel-web/build.rs b/ravel-web/build.rs index 20e8490..d34aee8 100644 --- a/ravel-web/build.rs +++ b/ravel-web/build.rs @@ -5,6 +5,7 @@ use serde::Deserialize; #[derive(Deserialize)] struct Config { element: std::collections::HashMap, + attribute: std::collections::HashMap, } #[derive(Deserialize)] @@ -12,6 +13,28 @@ struct Element { // TODO: JS element type } +#[derive(Deserialize)] +struct Attribute { + build: Option, + value_type: Option, + value_trait: Option, + value_wrapper: Option, +} + +impl Attribute { + fn build_name(&self, name: &str) -> String { + match &self.build { + Some(build) => build.clone(), + None => name.replace('-', "_"), + } + } + + fn value_trait(&self) -> &str { + assert!(self.value_type.is_none()); + self.value_trait.as_deref().unwrap_or("AttrValue") + } +} + fn main() { let config = std::fs::read_to_string("generate.toml").unwrap(); let config: Config = toml::from_str(&config).unwrap(); @@ -19,6 +42,14 @@ fn main() { let out_dir = std::env::var_os("OUT_DIR").unwrap(); let out_dir = std::path::PathBuf::from(out_dir); + gen_el(&config, &out_dir); + gen_el_types(&config, &out_dir); + + gen_attr(&config, &out_dir); + gen_attr_types(&config, &out_dir); +} + +fn gen_el_types(config: &Config, out_dir: &std::path::Path) { let mut src = String::new(); src.push_str("#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#\"\n"); @@ -37,16 +68,18 @@ fn main() { src.push_str("}\n"); for name in config.element.keys() { - let t = title_case(name); + let t = type_name(name); writeln!(&mut src, "make_el!({name}, {t}, create_{name}());").unwrap(); } std::fs::write(out_dir.join("gen_el_types.rs"), src).unwrap(); +} +fn gen_el(config: &Config, out_dir: &std::path::Path) { let mut src = String::new(); for name in config.element.keys() { - let t = title_case(name); + let t = type_name(name); // Ideally this would be generated by a macro, but rust-analyzer can't // seem to handle doc attributes generated by a macro generated by a // build script. @@ -63,10 +96,93 @@ fn main() { println!("cargo::rerun-if-changed=generate.toml"); } -fn title_case(s: &str) -> String { +fn gen_attr_types(config: &Config, out_dir: &std::path::Path) { + let mut src = String::new(); + + // TODO: Per-attr JS snippets like for elements. + + for (name, attr) in &config.attribute { + let t = type_name(name); + + if let Some(value_type) = &attr.value_type { + assert!(attr.value_trait.is_none()); + + match &attr.value_wrapper { + Some(value_wrapper) => writeln!( + &mut src, + "make_attr_value_type!(\"{name}\", {t}, {value_type}, {value_wrapper});", + ), + None => writeln!( + &mut src, + "make_attr_value_type!(\"{name}\", {t}, {value_type});", + ), + } + .unwrap(); + } else { + let value_trait = attr.value_trait(); + + match &attr.value_wrapper { + Some(value_wrapper) => writeln!( + &mut src, + "make_attr_value_trait!(\"{name}\", {t}, {value_trait}, {value_wrapper});", + ), + None => writeln!( + &mut src, + "make_attr_value_trait!(\"{name}\", {t}, {value_trait});", + ), + } + .unwrap(); + } + } + + std::fs::write(out_dir.join("gen_attr_types.rs"), src).unwrap(); +} + +fn gen_attr(config: &Config, out_dir: &std::path::Path) { + let mut src = String::new(); + + for (name, attr) in &config.attribute { + let build = attr.build_name(name); + let t = type_name(name); + // Ideally this would be generated by a macro, but rust-analyzer can't + // seem to handle doc attributes generated by a macro generated by a + // build script. + writeln!(&mut src, "/// `{name}` attribute.").unwrap(); + write!(&mut src, "pub fn {build}").unwrap(); + + if let Some(value_type) = &attr.value_type { + assert!(attr.value_trait.is_none()); + + write!(&mut src, "(value: {value_type}) -> types::{t}").unwrap(); + } else { + let value_trait = attr.value_trait(); + + write!( + &mut src, + "(value: Value) -> types::{t}" + ) + .unwrap(); + } + + writeln!(&mut src, " {{ types::{t}(value) }}").unwrap(); + } + + std::fs::write(out_dir.join("gen_attr.rs"), src).unwrap(); + + println!("cargo::rerun-if-changed=generate.toml"); +} + +fn type_name(s: &str) -> String { let mut cs = s.chars(); - match cs.next() { - None => String::new(), - Some(c) => c.to_uppercase().collect::() + cs.as_str(), + let mut s = String::with_capacity(s.len()); + + s.push(cs.next().unwrap().to_ascii_uppercase()); + while let Some(c) = cs.next() { + s.push(match c { + '-' => cs.next().unwrap().to_ascii_uppercase(), + c => c, + }); } + + s } diff --git a/ravel-web/generate.toml b/ravel-web/generate.toml index e7e3548..4f4adc5 100644 --- a/ravel-web/generate.toml +++ b/ravel-web/generate.toml @@ -135,3 +135,18 @@ summary = {} # Web Components slot = {} template = {} + +[attribute] +aria-hidden = {} +autofocus = { value_type = "bool", value_wrapper = "BooleanAttrValue" } +checked = { value_type = "bool", value_wrapper = "BooleanAttrValue" } +class = { value_trait = "ClassValue", value_wrapper = "Classes" } +for = { build = "for_" } +href = {} +id = {} +max = {} +min = {} +placeholder = {} +style = {} +type = { build = "type_" } +value = { build = "value_" } # TODO: do we really need this rename? diff --git a/ravel-web/src/attr.rs b/ravel-web/src/attr.rs deleted file mode 100644 index 429a208..0000000 --- a/ravel-web/src/attr.rs +++ /dev/null @@ -1,335 +0,0 @@ -//! HTML attributes. - -use std::marker::PhantomData; - -use ravel::{Builder, State}; -use web_sys::wasm_bindgen::UnwrapThrowExt as _; - -use crate::{BuildCx, RebuildCx, Web}; - -/// Trait to identify attribute types. -pub trait AttrKind: 'static { - /// The name of the attribute. - const NAME: &'static str; -} - -/// An arbitrary attribute. -#[repr(transparent)] -#[derive(Copy, Clone, Debug)] -pub struct Attr { - value: Value, - kind: PhantomData, -} - -impl> Builder for Attr { - type State = AttrState; - - fn build(self, cx: BuildCx) -> Self::State { - AttrState::build(cx, Kind::NAME, self.value.as_ref()) - } - - fn rebuild(self, cx: RebuildCx, state: &mut Self::State) { - state.rebuild(cx.parent, Kind::NAME, self.value.as_ref()) - } -} - -/// The state of an [`Attr`]. -pub struct AttrState { - value: String, -} - -impl AttrState { - fn build(cx: BuildCx, name: &'static str, value: &str) -> Self { - cx.position.parent.set_attribute(name, value).unwrap_throw(); - - Self { - value: value.to_string(), - } - } - - fn rebuild( - &mut self, - parent: &web_sys::Element, - name: &'static str, - value: &str, - ) { - if self.value == value { - return; - } - - self.value = value.to_string(); - parent.set_attribute(name, value).unwrap_throw() - } -} - -impl State for AttrState { - fn run(&mut self, _: &mut Output) {} -} - -/// An arbitrary attribute. -pub fn attr(_: Kind, value: Value) -> Attr { - Attr { - value, - kind: PhantomData, - } -} - -/// Trait for `class` attribute values. -/// -/// In HTML, `class` is a space separated list. Rather than requiring you to -/// construct the string by hand, this trait allows easily constructing it from -/// various types: -/// -/// * A [`String`] or `&'static str` is just a class name. -/// * A tuple of `ClassValue`s is the union of the component class names. -/// * An [`Option`] is an optional set of classes. -pub trait ClassValue: Eq { - /// If the value is available as a static string, providing it allows a more - /// efficient implementation. The default implementation returns [`None`]. - fn as_str(&self) -> Option<&'static str> { - None - } - - /// Calls a callback for each class name. - fn for_each(&self, f: F); -} - -impl ClassValue for &'static str { - fn as_str(&self) -> Option<&'static str> { - Some(self) - } - - fn for_each(&self, mut f: F) { - f(self) - } -} - -impl ClassValue for String { - fn for_each(&self, mut f: F) { - f(self) - } -} - -impl ClassValue for Option { - fn as_str(&self) -> Option<&'static str> { - self.as_ref().and_then(C::as_str) - } - - fn for_each(&self, f: F) { - if let Some(s) = self.as_ref() { - s.for_each(f); - } - } -} - -macro_rules! tuple_class_value { - ($($a:ident),*) => { - #[allow(non_camel_case_types)] - impl<$($a: ClassValue),*> ClassValue for ($($a,)*) { - fn for_each(&self, mut _f: F) { - let ($($a,)*) = self; - $($a.for_each(&mut _f);)* - } - } - }; -} - -tuple_class_value!(); -tuple_class_value!(a); -tuple_class_value!(a, b); -tuple_class_value!(a, b, c); -tuple_class_value!(a, b, c, d); -tuple_class_value!(a, b, c, d, e); -tuple_class_value!(a, b, c, d, e, f); -tuple_class_value!(a, b, c, d, e, f, g); -tuple_class_value!(a, b, c, d, e, f, g, h); - -/// `class` attribute. -pub struct AttrClass { - value: Value, -} - -impl AttrClass { - fn set_on(self, parent: &web_sys::Element) -> Value { - let mut s = String::new(); - - parent - .set_attribute( - "class", - match self.value.as_str() { - Some(s) => s, - None => { - self.value.for_each(|c| { - if !s.is_empty() { - s.push(' '); - } - - s.push_str(c); - }); - &s - } - }, - ) - .unwrap_throw(); - - self.value - } -} - -impl Builder for AttrClass { - type State = AttrClassState; - - fn build(self, cx: BuildCx) -> Self::State { - AttrClassState { - value: self.set_on(cx.position.parent), - } - } - - fn rebuild(self, cx: RebuildCx, state: &mut Self::State) { - if state.value != self.value { - state.value = self.set_on(cx.parent); - } - } -} - -/// The state of an [`AttrClass`]. -pub struct AttrClassState { - value: Value, -} - -impl State for AttrClassState { - fn run(&mut self, _: &mut Output) {} -} - -/// `class` attribute. -pub fn class(value: Value) -> AttrClass { - AttrClass { value } -} - -/// An arbitrary boolean attribute. -pub struct BooleanAttr { - value: bool, - kind: PhantomData, -} - -impl Builder for BooleanAttr { - type State = BooleanAttrState; - - fn build(self, cx: BuildCx) -> Self::State { - BooleanAttrState::build(cx, Kind::NAME, self.value) - } - - fn rebuild(self, cx: RebuildCx, state: &mut Self::State) { - state.rebuild(cx.parent, Kind::NAME, self.value) - } -} - -/// The state of a [`BooleanAttr`]. -pub struct BooleanAttrState { - value: bool, -} - -impl BooleanAttrState { - fn build(cx: BuildCx, name: &'static str, value: bool) -> Self { - if value { - cx.position.parent.set_attribute(name, "").unwrap_throw() - } - - Self { value } - } - - fn rebuild( - &mut self, - parent: &web_sys::Element, - name: &'static str, - value: bool, - ) { - if value && !self.value { - parent.set_attribute(name, "").unwrap_throw() - } else if !value && self.value { - parent.remove_attribute(name).unwrap_throw() - } - self.value = value; - } -} - -impl State for BooleanAttrState { - fn run(&mut self, _: &mut Output) {} -} - -/// An arbitrary boolean attribute. -pub fn boolean_attr(_: Kind, value: bool) -> BooleanAttr { - BooleanAttr { - value, - kind: PhantomData, - } -} - -macro_rules! attr_kind { - ($t:ident, $name:expr) => { - #[doc = concat!("`", $name, "` attribute.")] - #[derive(Copy, Clone)] - pub struct $t; - - impl AttrKind for $t { - const NAME: &'static str = $name; - } - }; -} - -macro_rules! make_attr { - ($name:ident, $t:ident) => { - make_attr!(stringify!($name), $name, $t); - }; - ($name:expr, $f:ident, $t:ident) => { - attr_kind!($t, $name); - - #[doc = concat!("`", $name, "` attribute.")] - pub fn $f(value: Value) -> Attr<$t, Value> { - attr($t, value) - } - }; -} - -make_attr!("aria-hidden", aria_hidden, AriaHidden); // TODO: typed -make_attr!("for", for_, For); -make_attr!(href, Href); -make_attr!(id, Id); -make_attr!(max, Max); -make_attr!(min, Min); -make_attr!("value", value_, Value_); -make_attr!(placeholder, Placeholder); -make_attr!(style, Style); -make_attr!("type", type_, Type); - -macro_rules! make_boolean_attr { - ($name:ident, $t:ident) => { - make_boolean_attr!(stringify!($name), $name, $t); - }; - ($name:expr, $f:ident, $t:ident) => { - #[doc = concat!("`", $name, "` attribute.")] - #[repr(transparent)] - #[derive(Copy, Clone, Debug)] - pub struct $t(bool); - - impl Builder for $t { - type State = BooleanAttrState; - - fn build(self, cx: BuildCx) -> Self::State { - BooleanAttrState::build(cx, $name, self.0) - } - - fn rebuild(self, cx: RebuildCx, state: &mut Self::State) { - state.rebuild(cx.parent, $name, self.0) - } - } - - #[doc = concat!("`", $name, "` attribute.")] - pub fn $f(value: bool) -> $t { - $t(value) - } - }; -} - -make_boolean_attr!(autofocus, Autofocus); -make_boolean_attr!(checked, Checked); diff --git a/ravel-web/src/attr/mod.rs b/ravel-web/src/attr/mod.rs new file mode 100644 index 0000000..369272b --- /dev/null +++ b/ravel-web/src/attr/mod.rs @@ -0,0 +1,30 @@ +//! HTML attributes. + +use std::marker::PhantomData; + +use self::types::*; + +pub mod types; + +// TODO: Dedup with `Text`/`text`? It's the same thing for text nodes. +/// A string type which is cloned to [`String`] to use as an attribute value. +/// +/// This wrapepr type exists to draw attention to the fact that using a borrowed +/// string value requires cloning to a persistent string in the attribute state. +/// This also permits a more efficient implementation of `&'static str`, which +/// does not need this wrapper. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct CloneString>(pub V); + +/// An arbitrary attribute. +pub fn attr( + _: Kind, + value: Value, +) -> Attr { + Attr { + value, + kind: PhantomData, + } +} + +include!(concat!(env!("OUT_DIR"), "/gen_attr.rs")); diff --git a/ravel-web/src/attr/types.rs b/ravel-web/src/attr/types.rs new file mode 100644 index 0000000..918a48c --- /dev/null +++ b/ravel-web/src/attr/types.rs @@ -0,0 +1,377 @@ +//! HTML attribute types. +//! +//! Usually you shouldn't need to import or reference these directly. + +use std::marker::PhantomData; + +use ravel::{Builder, State}; +use wasm_bindgen::UnwrapThrowExt; + +use crate::{BuildCx, RebuildCx, Web}; + +use super::CloneString; + +/// Trait to identify attribute types. +pub trait AttrKind: 'static { + /// The name of the attribute. + const NAME: &'static str; +} + +pub trait AttrValue { + type Saved: 'static; + + fn save(self) -> Self::Saved; + + fn changed(&self, saved: &Self::Saved) -> bool; + + fn with_str(&self, f: F) -> R + where + F: FnOnce(Option<&str>) -> R; +} + +impl AttrValue for Option { + type Saved = Option; + + fn save(self) -> Self::Saved { + self.map(AttrValue::save) + } + + fn changed(&self, saved: &Self::Saved) -> bool { + match (self, saved) { + (None, None) => false, + (None, Some(_)) => true, + (Some(_), None) => true, + (Some(v), Some(saved)) => v.changed(saved), + } + } + + fn with_str(&self, f: F) -> R + where + F: FnOnce(Option<&str>) -> R, + { + match self { + Some(v) => v.with_str(f), + None => f(None), + } + } +} + +impl> AttrValue for CloneString { + type Saved = String; + + fn save(self) -> Self::Saved { + self.0.as_ref().to_string() + } + + fn changed(&self, saved: &Self::Saved) -> bool { + self.0.as_ref() != saved.as_str() + } + + fn with_str(&self, f: F) -> R + where + F: FnOnce(Option<&str>) -> R, + { + f(Some(self.0.as_ref())) + } +} + +impl AttrValue for &'static str { + type Saved = Self; + + fn save(self) -> Self::Saved { + self + } + + fn changed(&self, saved: &Self::Saved) -> bool { + !std::ptr::eq(*self, *saved) + } + + fn with_str(&self, f: F) -> R + where + F: FnOnce(Option<&str>) -> R, + { + f(Some(self)) + } +} + +#[doc(hidden)] +#[derive(Clone, Copy, Debug)] +pub struct BooleanAttrValue(pub bool); + +impl AttrValue for BooleanAttrValue { + type Saved = bool; + + fn save(self) -> Self::Saved { + self.0 + } + + fn changed(&self, saved: &Self::Saved) -> bool { + self.0 != *saved + } + + fn with_str(&self, f: F) -> R + where + F: FnOnce(Option<&str>) -> R, + { + f(if self.0 { Some("") } else { None }) + } +} + +/// Trait for `class` attribute values. +/// +/// In HTML, `class` is a space separated list. Rather than requiring you to +/// construct the string by hand, this trait allows easily constructing it from +/// various types: +/// +/// * A [`String`] or `&'static str` is just a class name. +/// * A tuple of `ClassValue`s is the union of the component class names. +/// * An [`Option`] is an optional set of classes. +pub trait ClassValue: 'static + PartialEq { + /// If the value is available as a static string, providing it allows a more + /// efficient implementation. The default implementation returns [`None`]. + fn as_str(&self) -> Option<&'static str> { + None + } + + /// Calls a callback for each class name. + fn for_each(&self, f: F); +} + +impl ClassValue for &'static str { + fn as_str(&self) -> Option<&'static str> { + Some(self) + } + + fn for_each(&self, mut f: F) { + f(self) + } +} + +impl ClassValue for Option { + fn as_str(&self) -> Option<&'static str> { + self.as_ref().and_then(C::as_str) + } + + fn for_each(&self, f: F) { + if let Some(s) = self.as_ref() { + s.for_each(f); + } + } +} + +macro_rules! tuple_class_value { + ($($a:ident),*) => { + #[allow(non_camel_case_types)] + impl<$($a: ClassValue),*> ClassValue for ($($a,)*) { + fn for_each(&self, mut _f: F) { + let ($($a,)*) = self; + $($a.for_each(&mut _f);)* + } + } + }; +} + +tuple_class_value!(); +tuple_class_value!(a); +tuple_class_value!(a, b); +tuple_class_value!(a, b, c); +tuple_class_value!(a, b, c, d); +tuple_class_value!(a, b, c, d, e); +tuple_class_value!(a, b, c, d, e, f); +tuple_class_value!(a, b, c, d, e, f, g); +tuple_class_value!(a, b, c, d, e, f, g, h); + +#[doc(hidden)] +pub struct Classes(pub V); + +impl AttrValue for Classes { + type Saved = V; // TODO: Associated saved type + + fn save(self) -> Self::Saved { + self.0 + } + + fn changed(&self, saved: &Self::Saved) -> bool { + self.0 != *saved + } + + fn with_str(&self, f: F) -> R + where + F: FnOnce(Option<&str>) -> R, + { + match self.0.as_str() { + Some(s) => f(Some(s)), + None => { + let mut s = String::new(); + + self.0.for_each(|c| { + if !s.is_empty() { + s.push(' '); + } + + s.push_str(c); + }); + + f(if s.is_empty() { None } else { Some(&s) }) + } + } + } +} + +/// The state of an [`Attr`]. +pub struct AttrState { + value: Saved, +} + +impl AttrState { + fn build>( + parent: &web_sys::Element, + name: &'static str, + value: V, + ) -> Self { + value.with_str(|value| { + if let Some(value) = value { + parent.set_attribute(name, value).unwrap_throw() + } + }); + + Self { + value: value.save(), + } + } + + fn rebuild>( + &mut self, + parent: &web_sys::Element, + name: &'static str, + value: V, + ) { + if !value.changed(&self.value) { + return; + } + + value.with_str(|value| { + if let Some(value) = value { + parent.set_attribute(name, value).unwrap_throw() + } else { + parent.remove_attribute(name).unwrap_throw() + } + }); + } +} + +impl State for AttrState { + fn run(&mut self, _: &mut Output) {} +} + +/// An arbitrary attribute. +#[repr(transparent)] +#[derive(Copy, Clone, Debug)] +pub struct Attr { + pub(crate) value: Value, + pub(crate) kind: PhantomData, +} + +impl Builder for Attr { + type State = AttrState; + + fn build(self, cx: BuildCx) -> Self::State { + AttrState::build(cx.position.parent, Kind::NAME, self.value) + } + + fn rebuild(self, cx: RebuildCx, state: &mut Self::State) { + state.rebuild(cx.parent, Kind::NAME, self.value) + } +} + +macro_rules! make_attr_value_type { + ($name:literal, $t:ident, $value_type:ty) => { + make_attr_value_type_state!( + $name, + $t, + $value_type, + std::convert::identity, + ::Saved + ); + }; + ($name:literal, $t:ident, $value_type:ty, $value_wrapper:ident) => { + make_attr_value_type_state!( + $name, + $t, + $value_type, + $value_wrapper, + <$value_wrapper as AttrValue>::Saved + ); + }; +} + +macro_rules! make_attr_value_type_state { + ($name:literal, $t:ident, $value_type:ty, $value_wrapper:expr, $state_value:ty) => { + #[doc = concat!("`", $name, "` attribute.")] + #[derive(Copy, Clone)] + pub struct $t(pub $value_type); + + impl Builder for $t { + type State = AttrState<$state_value>; + + fn build(self, cx: BuildCx) -> Self::State { + AttrState::build( + cx.position.parent, + $name, + $value_wrapper(self.0), + ) + } + + fn rebuild(self, cx: RebuildCx, state: &mut Self::State) { + state.rebuild(cx.parent, $name, $value_wrapper(self.0)) + } + } + }; +} + +macro_rules! make_attr_value_trait { + ($name:literal, $t:ident, $value_trait:ident) => { + make_attr_value_trait_state!( + $name, + $t, + $value_trait, + std::convert::identity, + ::Saved + ); + }; + ($name:literal, $t:ident, $value_trait:ident, $value_wrapper:ident) => { + make_attr_value_trait_state!( + $name, + $t, + $value_trait, + $value_wrapper, + <$value_wrapper as AttrValue>::Saved + ); + }; +} + +macro_rules! make_attr_value_trait_state { + ($name:literal, $t:ident, $value_trait:ident, $value_wrapper:expr, $state_value:ty) => { + #[doc = concat!("`", $name, "` attribute.")] + #[derive(Copy, Clone)] + pub struct $t(pub V); + + impl Builder for $t { + type State = AttrState<$state_value>; + + fn build(self, cx: BuildCx) -> Self::State { + AttrState::build( + cx.position.parent, + $name, + $value_wrapper(self.0), + ) + } + + fn rebuild(self, cx: RebuildCx, state: &mut Self::State) { + state.rebuild(cx.parent, $name, $value_wrapper(self.0)) + } + } + }; +} + +include!(concat!(env!("OUT_DIR"), "/gen_attr_types.rs")); diff --git a/ravel-web/src/el/mod.rs b/ravel-web/src/el/mod.rs index 06d1a5a..be519a3 100644 --- a/ravel-web/src/el/mod.rs +++ b/ravel-web/src/el/mod.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; -use self::types::{El, ElKind}; +use self::types::*; pub mod types;