From a39ccf737fd0ad7ef8644b258fbb4708c1beedee Mon Sep 17 00:00:00 2001 From: Bodil Stokke Date: Sat, 16 Mar 2019 20:59:56 +0000 Subject: [PATCH] Support boolean attrs, child blocks, and port TodoMVC example. --- Cargo.toml | 3 +- examples/dodrio/{ => counter}/Cargo.toml | 2 +- examples/dodrio/{ => counter}/README.md | 0 examples/dodrio/{ => counter}/index.html | 0 examples/dodrio/{ => counter}/src/lib.rs | 2 +- examples/dodrio/todomvc/Cargo.toml | 49 ++++ examples/dodrio/todomvc/README.md | 33 +++ examples/dodrio/todomvc/index.html | 21 ++ examples/dodrio/todomvc/src/controller.rs | 140 +++++++++++ examples/dodrio/todomvc/src/keys.rs | 7 + examples/dodrio/todomvc/src/lib.rs | 61 +++++ examples/dodrio/todomvc/src/router.rs | 66 +++++ examples/dodrio/todomvc/src/todo.rs | 154 ++++++++++++ examples/dodrio/todomvc/src/todos.rs | 280 ++++++++++++++++++++++ examples/dodrio/todomvc/src/utils.rs | 32 +++ examples/dodrio/todomvc/src/visibility.rs | 51 ++++ macros/src/config.rs | 6 +- macros/src/html.rs | 113 +++++---- macros/src/lib.rs | 1 + macros/src/map.rs | 5 + 20 files changed, 980 insertions(+), 46 deletions(-) rename examples/dodrio/{ => counter}/Cargo.toml (87%) rename examples/dodrio/{ => counter}/README.md (100%) rename examples/dodrio/{ => counter}/index.html (100%) rename examples/dodrio/{ => counter}/src/lib.rs (97%) create mode 100644 examples/dodrio/todomvc/Cargo.toml create mode 100644 examples/dodrio/todomvc/README.md create mode 100644 examples/dodrio/todomvc/index.html create mode 100644 examples/dodrio/todomvc/src/controller.rs create mode 100644 examples/dodrio/todomvc/src/keys.rs create mode 100755 examples/dodrio/todomvc/src/lib.rs create mode 100644 examples/dodrio/todomvc/src/router.rs create mode 100644 examples/dodrio/todomvc/src/todo.rs create mode 100644 examples/dodrio/todomvc/src/todos.rs create mode 100644 examples/dodrio/todomvc/src/utils.rs create mode 100644 examples/dodrio/todomvc/src/visibility.rs diff --git a/Cargo.toml b/Cargo.toml index f72f5c4..c73cdc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "macros", "examples/stdweb", "examples/rocket", - "examples/dodrio", + "examples/dodrio/counter", + "examples/dodrio/todomvc", "ui", ] diff --git a/examples/dodrio/Cargo.toml b/examples/dodrio/counter/Cargo.toml similarity index 87% rename from examples/dodrio/Cargo.toml rename to examples/dodrio/counter/Cargo.toml index 7d9171e..5de2d14 100644 --- a/examples/dodrio/Cargo.toml +++ b/examples/dodrio/counter/Cargo.toml @@ -15,7 +15,7 @@ console_log = "0.1.2" dodrio = "0.1.0" log = "0.4.6" wasm-bindgen = "0.2.38" -typed-html = { path = "../../typed-html", features = ["dodrio_macro"] } +typed-html = { path = "../../../typed-html", features = ["dodrio_macro"] } [dependencies.web-sys] version = "0.3.15" diff --git a/examples/dodrio/README.md b/examples/dodrio/counter/README.md similarity index 100% rename from examples/dodrio/README.md rename to examples/dodrio/counter/README.md diff --git a/examples/dodrio/index.html b/examples/dodrio/counter/index.html similarity index 100% rename from examples/dodrio/index.html rename to examples/dodrio/counter/index.html diff --git a/examples/dodrio/src/lib.rs b/examples/dodrio/counter/src/lib.rs similarity index 97% rename from examples/dodrio/src/lib.rs rename to examples/dodrio/counter/src/lib.rs index eddfb7f..44d2841 100644 --- a/examples/dodrio/src/lib.rs +++ b/examples/dodrio/counter/src/lib.rs @@ -53,7 +53,7 @@ impl Render for Counter { // counter on the next animation frame. vdom.schedule_render(); }}>"+" - { text(count.into_bump_str()) } + { vec![text(count.into_bump_str())] } + + ) + } + + fn visibility_swap<'a, 'bump>( + &'a self, + bump: &'bump Bump, + url: &'static str, + target_vis: Visibility, + ) -> Node<'bump> + where + 'a: 'bump, + { + dodrio!(bump, +
  • + { bumpalo::vec![in ≎ text(target_vis.label())] } +
  • + ) + } +} + +impl Render for Todos { + fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump> + where + 'a: 'bump, + { + dodrio!(bump, +
    { bumpalo::vec![in ≎ + self.header(bump), self.todos_list(bump), self.footer(bump) + ] }
    + ) + } +} diff --git a/examples/dodrio/todomvc/src/utils.rs b/examples/dodrio/todomvc/src/utils.rs new file mode 100644 index 0000000..0eaced2 --- /dev/null +++ b/examples/dodrio/todomvc/src/utils.rs @@ -0,0 +1,32 @@ +//! Small utility functions. + +use wasm_bindgen::UnwrapThrowExt; + +/// Get the top-level window. +pub fn window() -> web_sys::Window { + web_sys::window().unwrap_throw() +} + +/// Get the current location hash, if any. +pub fn hash() -> Option { + window() + .location() + .hash() + .ok() + .and_then(|h| if h.is_empty() { None } else { Some(h) }) +} + +/// Set the current location hash. +pub fn set_hash(hash: &str) { + window().location().set_hash(hash).unwrap_throw(); +} + +/// Get the top-level document. +pub fn document() -> web_sys::Document { + window().document().unwrap_throw() +} + +/// Get the top-level window's local storage. +pub fn local_storage() -> web_sys::Storage { + window().local_storage().unwrap_throw().unwrap_throw() +} diff --git a/examples/dodrio/todomvc/src/visibility.rs b/examples/dodrio/todomvc/src/visibility.rs new file mode 100644 index 0000000..ad53c14 --- /dev/null +++ b/examples/dodrio/todomvc/src/visibility.rs @@ -0,0 +1,51 @@ +//! Visibility filtering. + +use std::fmt; +use std::str::FromStr; + +/// The visibility filtering for todo items. +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum Visibility { + /// Show all todos. + All, + /// Show only active, incomplete todos. + Active, + /// Show only inactive, completed todos. + Completed, +} + +impl Default for Visibility { + fn default() -> Visibility { + Visibility::All + } +} + +impl FromStr for Visibility { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "all" => Ok(Visibility::All), + "active" => Ok(Visibility::Active), + "completed" => Ok(Visibility::Completed), + _ => Err(()), + } + } +} + +impl fmt::Display for Visibility { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.label().to_lowercase()) + } +} + +impl Visibility { + /// Get a string label for this visibility. + pub fn label(self) -> &'static str { + match self { + Visibility::All => "All", + Visibility::Active => "Active", + Visibility::Completed => "Completed", + } + } +} diff --git a/macros/src/config.rs b/macros/src/config.rs index 15b8876..ee47176 100644 --- a/macros/src/config.rs +++ b/macros/src/config.rs @@ -21,11 +21,11 @@ pub fn global_attrs(span: Span) -> StringyMap { insert("accesskey", "String"); insert("autocapitalize", "String"); - insert("contenteditable", "bool"); + insert("contenteditable", "crate::types::Bool"); insert("contextmenu", "crate::types::Id"); insert("dir", "crate::types::TextDirection"); - insert("draggable", "bool"); - insert("hidden", "bool"); + insert("draggable", "crate::types::Bool"); + insert("hidden", "crate::types::Bool"); insert("is", "String"); insert("lang", "crate::types::LanguageTag"); insert("style", "String"); diff --git a/macros/src/html.rs b/macros/src/html.rs index 37e230e..a1db35d 100644 --- a/macros/src/html.rs +++ b/macros/src/html.rs @@ -69,15 +69,14 @@ impl Node { ) -> Result { match self { Node::Element(el) => el.into_dodrio_token_stream(bump, is_req_child), - Node::Text(text) => { - let text = TokenTree::Literal(text); - Ok(quote!(dodrio::builder::text(#text))) - } + Node::Text(text) => Ok(dodrio_text_node(text)), Node::Block(group) => { - let group: TokenTree = group.into(); - Ok(quote!( - #group - )) + let span = group.span(); + let error = + "you cannot use a block as a top level element or a required child element"; + Err(quote_spanned! { span=> + compile_error! { #error } + }) } } } @@ -90,6 +89,12 @@ pub struct Element { pub children: Vec, } +#[cfg(feature = "dodrio")] +fn dodrio_text_node(text: Literal) -> TokenStream { + let text = TokenTree::Literal(text); + quote!(dodrio::builder::text(#text)) +} + fn extract_data_attrs(attrs: &mut StringyMap) -> StringyMap { let mut data = StringyMap::new(); let keys: Vec = attrs.keys().cloned().collect(); @@ -140,6 +145,7 @@ fn is_string_literal(literal: &Literal) -> bool { literal.to_string().starts_with('"') } +#[allow(dead_code)] fn stringify_ident(ident: &Ident) -> String { let s = ident.to_string(); if s.starts_with("r#") { @@ -288,7 +294,7 @@ impl Element { ) -> Result { let name = self.name; let name_str = stringify_ident(&name); - let typename: TokenTree = Ident::new(&name_str, name.span()).into(); + let typename: TokenTree = ident::new_raw(&name_str, name.span()).into(); let tag_name = TokenTree::from(Literal::string(&name_str)); let req_names = required_children(&name_str); if req_names.len() > self.children.len() { @@ -312,12 +318,7 @@ impl Element { value, ) }); - let opt_children = self - .children - .split_off(req_names.len()) - .into_iter() - .map(|node| node.into_dodrio_token_stream(bump, false)) - .collect::, TokenStream>>()?; + let opt_children = self.children.split_off(req_names.len()); let req_children = self .children .into_iter() @@ -369,38 +370,41 @@ impl Element { } } + let attr_max_len = self.attributes.len() + data_attrs.len(); let mut builder = quote!( - dodrio::builder::ElementBuilder::new(#bump, #tag_name) + let mut attr_list = dodrio::bumpalo::collections::Vec::with_capacity_in(#attr_max_len, #bump); ); - // Build an array of attributes. - let mut attr_array = TokenStream::new(); + // Build the attributes. for (key, _) in self.attributes.iter() { - let key_str = TokenTree::from(Literal::string(&stringify_ident(key))); - attr_array.extend(quote!( - dodrio::builder::attr( - #key_str, - dodrio::bumpalo::format!( - in &#bump, "{}", element.attrs.#key.unwrap() - ).into_bump_str() - ), + let key_str = stringify_ident(key); + let key = ident::new_raw(&key_str, key.span()); + let key_str = TokenTree::from(Literal::string(&key_str)); + builder.extend(quote!( + let attr_value = dodrio::bumpalo::format!( + in &#bump, "{}", element.attrs.#key.unwrap()); + if !attr_value.is_empty() { + attr_list.push(dodrio::builder::attr(#key_str, attr_value.into_bump_str())); + } )); } for (key, value) in data_attrs .iter() .map(|(k, v)| (TokenTree::from(Literal::string(&k)), v.clone())) { - attr_array.extend(quote!( - dodrio::builder::attr( + builder.extend(quote!( + attr_list.push(dodrio::builder::attr( #key, dodrio::bumpalo::format!( in &#bump, "{}", #value ).into_bump_str() - ) + )); )); } + builder.extend(quote!( - .attributes([#attr_array]) + let mut node = dodrio::builder::ElementBuilder::new(#bump, #tag_name) + .attributes(attr_list) )); // Build an array of event listeners. @@ -413,21 +417,24 @@ impl Element { )); } builder.extend(quote!( - .listeners([#event_array]) + .listeners([#event_array]); )); - // And finally an array of children. + // And finally an array of children, or a stream of builder commands + // if we have a group inside the child list. let mut child_array = TokenStream::new(); + let mut child_builder = TokenStream::new(); + let mut static_children = true; // Walk through required children and build them inline. let mut make_req_children = TokenStream::new(); let mut arg_list = Vec::new(); for (index, child) in req_children.into_iter().enumerate() { - let req_child = TokenTree::from(Ident::new( + let req_child = TokenTree::from(ident::new_raw( &format!("req_child_{}", index), Span::call_site(), )); - let child_node = TokenTree::from(Ident::new( + let child_node = TokenTree::from(ident::new_raw( &format!("child_node_{}", index), Span::call_site(), )); @@ -437,23 +444,48 @@ impl Element { child_array.extend(quote!( #child_node, )); + child_builder.extend(quote!( + node = node.child(#child_node); + )); arg_list.push(req_child); } - for child in opt_children { + // Build optional children, test if we have groups. + for child_node in opt_children { + let child = match child_node { + Node::Text(text) => dodrio_text_node(text), + Node::Element(el) => el.into_dodrio_token_stream(bump, false)?, + Node::Block(group) => { + static_children = false; + let group: TokenTree = group.into(); + child_builder.extend(quote!( + for child in #group.into_iter() { + node = node.child(child); + } + )); + continue; + } + }; child_array.extend(quote!( #child, )); + child_builder.extend(quote!( + node = node.child(#child); + )); } - builder.extend(quote!( - .children([#child_array]) - .finish() - )); + if static_children { + builder.extend(quote!( + let node = node.children([#child_array]); + )); + } else { + builder.extend(child_builder); + } + builder.extend(quote!(node.finish())); if is_req_child { builder = quote!( - (element, #builder) + (element, {#builder}) ); } @@ -479,6 +511,7 @@ pub fn expand_html(input: &[Token]) -> Result<(Node, Option>), ParseE grammar::NodeWithTypeParser::new().parse(Lexer::new(input)) } +#[allow(dead_code)] pub fn expand_dodrio(input: &[Token]) -> Result<(Ident, Node), ParseError> { grammar::NodeWithBumpParser::new().parse(Lexer::new(input)) } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 1a309a3..c9e7893 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -48,6 +48,7 @@ pub fn dodrio(input: TokenStream) -> TokenStream { Err(err) => error::parse_error(&stream, &err), Ok((bump, node)) => match node.into_dodrio_token_stream(&bump, false) { Err(err) => err, + // Ok(success) => {println!("{}", success); panic!()}, Ok(success) => success, }, }) diff --git a/macros/src/map.rs b/macros/src/map.rs index f5ea70a..68339ce 100644 --- a/macros/src/map.rs +++ b/macros/src/map.rs @@ -28,6 +28,11 @@ where pub fn keys(&self) -> impl Iterator { self.0.values().map(|(k, _)| k) } + + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.0.len() + } } impl From> for StringyMap