From 813121b3a7785fdfb31e65652ee7e25e770c20cf Mon Sep 17 00:00:00 2001 From: Bodil Stokke Date: Sat, 16 Mar 2019 03:18:49 +0000 Subject: [PATCH 1/6] Dodrio support with a bespoke macro. --- Cargo.toml | 1 + examples/dodrio/Cargo.toml | 34 ++++++ examples/dodrio/README.md | 21 ++++ examples/dodrio/index.html | 13 ++ examples/dodrio/src/lib.rs | 86 ++++++++++++++ macros/Cargo.toml | 3 + macros/src/grammar.lalrpop | 4 + macros/src/html.rs | 204 ++++++++++++++++++++++++++++++++ macros/src/lib.rs | 24 +++- typed-html/Cargo.toml | 5 + typed-html/src/lib.rs | 4 + typed-html/src/output/dodrio.rs | 20 ++++ typed-html/src/output/mod.rs | 2 + 13 files changed, 416 insertions(+), 5 deletions(-) create mode 100644 examples/dodrio/Cargo.toml create mode 100644 examples/dodrio/README.md create mode 100644 examples/dodrio/index.html create mode 100644 examples/dodrio/src/lib.rs create mode 100644 typed-html/src/output/dodrio.rs diff --git a/Cargo.toml b/Cargo.toml index 77c44e7..f72f5c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ members = [ "macros", "examples/stdweb", "examples/rocket", + "examples/dodrio", "ui", ] diff --git a/examples/dodrio/Cargo.toml b/examples/dodrio/Cargo.toml new file mode 100644 index 0000000..7d9171e --- /dev/null +++ b/examples/dodrio/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "dodrio-counter" +version = "0.1.0" +authors = ["Nick Fitzgerald "] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[features] + +[dependencies] +console_error_panic_hook = "0.1.6" +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"] } + +[dependencies.web-sys] +version = "0.3.15" +features = [ + "console", + "Document", + "Event", + "EventTarget", + "HtmlElement", + "MouseEvent", + "Node", + "Window", +] + +[dev-dependencies] +wasm-bindgen-test = "0.2.38" diff --git a/examples/dodrio/README.md b/examples/dodrio/README.md new file mode 100644 index 0000000..8701b81 --- /dev/null +++ b/examples/dodrio/README.md @@ -0,0 +1,21 @@ +# Counter + +A counter that can be incremented and decremented. + +## Source + +See `src/lib.rs`. + +## Build + +``` +wasm-pack build --target no-modules +``` + +## Serve + +Use any HTTP server, for example: + +``` +python -m SimpleHTTPServer +``` diff --git a/examples/dodrio/index.html b/examples/dodrio/index.html new file mode 100644 index 0000000..78ec376 --- /dev/null +++ b/examples/dodrio/index.html @@ -0,0 +1,13 @@ + + + + + Counter + + + + + + diff --git a/examples/dodrio/src/lib.rs b/examples/dodrio/src/lib.rs new file mode 100644 index 0000000..6380dbf --- /dev/null +++ b/examples/dodrio/src/lib.rs @@ -0,0 +1,86 @@ +#![recursion_limit = "128"] + +use dodrio::builder::text; +use dodrio::bumpalo::{self, Bump}; +use dodrio::Render; +use log::*; +use typed_html::dodrio; +use wasm_bindgen::prelude::*; + +/// A counter that can be incremented and decrmented! +struct Counter { + count: isize, +} + +impl Counter { + /// Construct a new, zeroed counter. + fn new() -> Counter { + Counter { count: 0 } + } + + /// Increment this counter's count. + fn increment(&mut self) { + self.count += 1; + } + + /// Decrement this counter's count. + fn decrement(&mut self) { + self.count -= 1; + } +} + +// The `Render` implementation for `Counter`s displays the current count and has +// buttons to increment and decrement the count. +impl Render for Counter { + fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump> + where + 'a: 'bump, + { + // Stringify the count as a bump-allocated string. + let count = bumpalo::format!(in bump, "{}", self.count); + + dodrio!(bump, +
+ + { text(count.into_bump_str()) } + +
+ ) + } +} + +#[wasm_bindgen(start)] +pub fn run() { + // Initialize debug logging for if/when things go wrong. + console_error_panic_hook::set_once(); + console_log::init_with_level(Level::Trace).expect("should initialize logging OK"); + + // Get the document's ``. + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + // Construct a new counter component. + let counter = Counter::new(); + + // Mount our counter component to the ``. + let vdom = dodrio::Vdom::new(&body, counter); + + // Run the virtual DOM and its listeners forever. + vdom.forget(); +} diff --git a/macros/Cargo.toml b/macros/Cargo.toml index e81e109..bbf2811 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -25,3 +25,6 @@ quote = "0.6.10" [build-dependencies] lalrpop = "0.16.1" version_check = "0.1.5" + +[features] +dodrio = [] diff --git a/macros/src/grammar.lalrpop b/macros/src/grammar.lalrpop index f339325..172313e 100644 --- a/macros/src/grammar.lalrpop +++ b/macros/src/grammar.lalrpop @@ -174,6 +174,10 @@ pub NodeWithType: (Node, Option>) = { }, }; +pub NodeWithBump: (Ident, Node) = { + "," , +}; + // The declare macro diff --git a/macros/src/html.rs b/macros/src/html.rs index 684116d..e7b29c5 100644 --- a/macros/src/html.rs +++ b/macros/src/html.rs @@ -60,6 +60,26 @@ impl Node { } } } + + pub fn into_dodrio_token_stream( + self, + bump: &Ident, + is_req_child: bool, + ) -> 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::Block(group) => { + let group: TokenTree = group.into(); + Ok(quote!( + #group + )) + } + } + } } #[derive(Clone)] @@ -119,6 +139,15 @@ fn is_string_literal(literal: &Literal) -> bool { literal.to_string().starts_with('"') } +fn stringify_ident(ident: &Ident) -> String { + let s = ident.to_string(); + if s.starts_with("r#") { + s[2..].to_string() + } else { + s + } +} + impl Element { fn into_token_stream(mut self, ty: &Option>) -> Result { let name = self.name; @@ -249,9 +278,184 @@ impl Element { } )) } + + fn into_dodrio_token_stream( + mut self, + bump: &Ident, + is_req_child: bool, + ) -> Result { + let name = self.name; + let name_str = stringify_ident(&name); + let typename: TokenTree = Ident::new(&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() { + let span = name.span(); + let error = format!( + "<{}> requires {} children but there are only {}", + name_str, + req_names.len(), + self.children.len() + ); + return Err(quote_spanned! {span=> + compile_error! { #error } + }); + } + let events = extract_event_handlers(&mut self.attributes); + let data_attrs = extract_data_attrs(&mut self.attributes); + let attrs = self.attributes.iter().map(|(key, value)| { + ( + key.to_string(), + TokenTree::Ident(ident::new_raw(&key.to_string(), key.span())), + 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 req_children = self + .children + .into_iter() + .map(|node| node.into_dodrio_token_stream(bump, true)) + .collect::, TokenStream>>()?; + + let mut set_attrs = TokenStream::new(); + + for (attr_str, key, value) in attrs { + match value { + TokenTree::Literal(lit) if is_string_literal(lit) => { + let mut eprintln_msg = "ERROR: ".to_owned(); + #[cfg(can_show_location_of_runtime_parse_error)] + { + let span = lit.span(); + eprintln_msg += &format!( + "{}:{}:{}: ", + span.unstable() + .source_file() + .path() + .to_str() + .unwrap_or("unknown"), + span.unstable().start().line, + span.unstable().start().column + ); + } + eprintln_msg += &format!( + "<{} {}={}> failed to parse attribute value: {{}}", + name_str, attr_str, lit, + ); + #[cfg(not(can_show_location_of_runtime_parse_error))] + { + eprintln_msg += "\nERROR: rebuild with nightly to print source location"; + } + + set_attrs.extend(quote!( + element.attrs.#key = Some(#lit.parse().unwrap_or_else(|err| { + eprintln!(#eprintln_msg, err); + panic!("failed to parse string literal"); + })); + )); + } + value => { + let value = process_value(value); + set_attrs.extend(quote!( + element.attrs.#key = Some(std::convert::Into::into(#value)); + )); + } + } + } + + let mut builder = TokenStream::new(); + builder.extend(quote!( + dodrio::builder::ElementBuilder::new(#bump, #tag_name) + )); + + for (key, _) in self.attributes.iter() { + let key_str = TokenTree::from(Literal::string(&stringify_ident(key))); + builder.extend(quote!( + .attr(#key_str, dodrio::bumpalo::format!(in &#bump, "{}", + element.attrs.#key.unwrap()).into_bump_str()) + )); + } + + for (key, value) in data_attrs + .iter() + .map(|(k, v)| (TokenTree::from(Literal::string(&k)), v.clone())) + { + builder.extend(quote!( + .attr(#key, #value.into()) + )); + } + + for (key, value) in events.iter() { + let key = TokenTree::from(Literal::string(&stringify_ident(key))); + let value = process_value(value); + builder.extend(quote!( + .on(#key, #value) + )); + } + + let mut make_req_children = TokenStream::new(); + let mut arg_list = Vec::new(); + let mut req_nodes = Vec::new(); + for (index, child) in req_children.into_iter().enumerate() { + let req_child = TokenTree::from(Ident::new( + &format!("req_child_{}", index), + Span::call_site(), + )); + let child_node = TokenTree::from(Ident::new( + &format!("child_node_{}", index), + Span::call_site(), + )); + make_req_children.extend(quote!( + let (#req_child, #child_node) = #child; + )); + builder.extend(quote!( + .child(#child_node) + )); + arg_list.push(req_child); + req_nodes.push(child_node); + } + + for child in opt_children { + builder.extend(quote!( + .child(#child) + )); + } + + builder.extend(quote!( + .finish() + )); + + if is_req_child { + builder = quote!( + (element, #builder) + ); + } + + let mut args = TokenStream::new(); + for arg in arg_list { + args.extend(quote!( #arg, )); + } + + Ok(quote!( + { + #make_req_children + let mut element: typed_html::elements::#typename = typed_html::elements::#typename::new(#args); + #set_attrs + #builder + } + )) + } } // FIXME report a decent error when the macro contains multiple top level elements pub fn expand_html(input: &[Token]) -> Result<(Node, Option>), ParseError> { grammar::NodeWithTypeParser::new().parse(Lexer::new(input)) } + +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 32b4633..1a309a3 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,12 +1,7 @@ #![recursion_limit = "128"] #![cfg_attr(can_show_location_of_runtime_parse_error, feature(proc_macro_span))] -extern crate ansi_term; -extern crate lalrpop_util; extern crate proc_macro; -extern crate proc_macro2; -extern crate proc_macro_hack; -extern crate quote; use proc_macro::TokenStream; use proc_macro_hack::proc_macro_hack; @@ -39,6 +34,25 @@ pub fn html(input: TokenStream) -> TokenStream { }) } +/// Construct a Dodrio node. +/// +/// See the crate documentation for [`typed_html`][typed_html]. +/// +/// [typed_html]: ../typed_html/index.html +#[cfg(feature = "dodrio")] +#[proc_macro_hack] +pub fn dodrio(input: TokenStream) -> TokenStream { + let stream = lexer::unroll_stream(input.into(), false); + let result = html::expand_dodrio(&stream); + TokenStream::from(match result { + Err(err) => error::parse_error(&stream, &err), + Ok((bump, node)) => match node.into_dodrio_token_stream(&bump, false) { + Err(err) => err, + Ok(success) => success, + }, + }) +} + /// This macro is used by `typed_html` internally to generate types and /// implementations for HTML elements. #[proc_macro] diff --git a/typed-html/Cargo.toml b/typed-html/Cargo.toml index bbecfef..add8a04 100644 --- a/typed-html/Cargo.toml +++ b/typed-html/Cargo.toml @@ -25,3 +25,8 @@ htmlescape = "0.3.1" proc-macro-hack = "0.5.4" proc-macro-nested = "0.1.3" stdweb = { version = "0.4.14", optional = true } +dodrio = { version = "0.1.0", optional = true } +web-sys = { version = "0.3.16", optional = true, features = ["Event", "Element"] } + +[features] +dodrio_macro = ["web-sys", "dodrio", "typed-html-macros/dodrio"] diff --git a/typed-html/src/lib.rs b/typed-html/src/lib.rs index 2f66881..40b4dc0 100644 --- a/typed-html/src/lib.rs +++ b/typed-html/src/lib.rs @@ -199,6 +199,10 @@ use std::fmt::Display; #[proc_macro_hack(support_nested)] pub use typed_html_macros::html; +#[cfg(feature = "dodrio_macro")] +#[proc_macro_hack(support_nested)] +pub use typed_html_macros::dodrio; + pub mod dom; pub mod elements; pub mod events; diff --git a/typed-html/src/output/dodrio.rs b/typed-html/src/output/dodrio.rs new file mode 100644 index 0000000..5e8bfbf --- /dev/null +++ b/typed-html/src/output/dodrio.rs @@ -0,0 +1,20 @@ +use std::fmt::{Display, Error, Formatter}; + +use crate::OutputType; + +/// DOM output using the Dodrio virtual DOM +pub struct Dodrio; +impl OutputType for Dodrio { + type Events = Events; + type EventTarget = (); + type EventListenerHandle = (); +} + +#[derive(Default)] +pub struct Events; + +impl Display for Events { + fn fmt(&self, _: &mut Formatter) -> Result<(), Error> { + unimplemented!() + } +} diff --git a/typed-html/src/output/mod.rs b/typed-html/src/output/mod.rs index 75eaf35..6243b47 100644 --- a/typed-html/src/output/mod.rs +++ b/typed-html/src/output/mod.rs @@ -1,2 +1,4 @@ #[cfg(feature = "stdweb")] pub mod stdweb; +#[cfg(feature = "dodrio_macro")] +pub mod dodrio; From c892a9f27e6b5a23848c5bb35554374790caa8dd Mon Sep 17 00:00:00 2001 From: Bodil Stokke Date: Sat, 16 Mar 2019 16:24:23 +0000 Subject: [PATCH 2/6] Pre-build static arrays of attrs etc to save on allocations. --- examples/dodrio/src/lib.rs | 2 +- macros/src/html.rs | 59 ++++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/examples/dodrio/src/lib.rs b/examples/dodrio/src/lib.rs index 6380dbf..eddfb7f 100644 --- a/examples/dodrio/src/lib.rs +++ b/examples/dodrio/src/lib.rs @@ -40,7 +40,7 @@ impl Render for Counter { let count = bumpalo::format!(in bump, "{}", self.count); dodrio!(bump, -
+
- { text(count.into_bump_str()) } + { vec![text(count.into_bump_str())] }
+ (); + C::update_edits(root, vdom, id, input.value()); + }} onblur={move |root, vdom, _event| { + C::finish_edits(root, vdom, id) + }} onkeydown={move |root, vdom, event| { + let event = event.unchecked_into::(); + match event.key_code() { + keys::ENTER => C::finish_edits(root, vdom, id), + keys::ESCAPE => C::cancel_edits(root, vdom, id), + _ => {} + } + }}/> + + ) + } +} diff --git a/examples/dodrio/todomvc/src/todos.rs b/examples/dodrio/todomvc/src/todos.rs new file mode 100644 index 0000000..71b598f --- /dev/null +++ b/examples/dodrio/todomvc/src/todos.rs @@ -0,0 +1,280 @@ +//! Type definitions and `dodrio::Render` implementation for a collection of +//! todo items. + +use crate::controller::Controller; +use crate::todo::{Todo, TodoActions}; +use crate::visibility::Visibility; +use crate::{keys, utils}; +use dodrio::{ + builder::text, + bumpalo::{self, Bump}, + Node, Render, RootRender, VdomWeak, +}; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; +use std::mem; +use typed_html::dodrio; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; + +/// A collection of todos. +#[derive(Default, Serialize, Deserialize)] +#[serde(rename = "todos-dodrio", bound = "")] +pub struct Todos { + todos: Vec>, + + #[serde(skip)] + draft: String, + + #[serde(skip)] + visibility: Visibility, + + #[serde(skip)] + _controller: PhantomData, +} + +/// Actions for `Todos` that can be triggered by UI interactions. +pub trait TodosActions: TodoActions { + /// Toggle the completion state of all todo items. + fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak); + + /// Update the draft todo item's text. + fn update_draft(root: &mut dyn RootRender, vdom: VdomWeak, draft: String); + + /// Finish the current draft todo item and add it to the collection of + /// todos. + fn finish_draft(root: &mut dyn RootRender, vdom: VdomWeak); + + /// Change the todo item visibility filtering to the given `Visibility`. + fn change_visibility(root: &mut dyn RootRender, vdom: VdomWeak, vis: Visibility); + + /// Delete all completed todo items. + fn delete_completed(root: &mut dyn RootRender, vdom: VdomWeak); +} + +impl Todos { + /// Construct a new todos set. + /// + /// If an existing set is available in local storage, then us that, + /// otherwise create a new set. + pub fn new() -> Self + where + C: Default, + { + Self::from_local_storage().unwrap_or_default() + } + + /// Deserialize a set of todos from local storage. + pub fn from_local_storage() -> Option { + utils::local_storage() + .get("todomvc-dodrio") + .ok() + .and_then(|opt| opt) + .and_then(|json| serde_json::from_str(&json).ok()) + } + + /// Serialize this set of todos to local storage. + pub fn save_to_local_storage(&self) { + let serialized = serde_json::to_string(self).unwrap_throw(); + utils::local_storage() + .set("todomvc-dodrio", &serialized) + .unwrap_throw(); + } + + /// Add a new todo item to this collection. + pub fn add_todo(&mut self, todo: Todo) { + self.todos.push(todo); + } + + /// Delete the todo with the given id. + pub fn delete_todo(&mut self, id: usize) { + self.todos.remove(id); + self.fix_ids(); + } + + /// Delete all completed todo items. + pub fn delete_completed(&mut self) { + self.todos.retain(|t| !t.is_complete()); + self.fix_ids(); + } + + // Fix all todo identifiers so that they match their index once again. + fn fix_ids(&mut self) { + for (id, todo) in self.todos.iter_mut().enumerate() { + todo.set_id(id); + } + } + + /// Get a shared slice of the underlying set of todo items. + pub fn todos(&self) -> &[Todo] { + &self.todos + } + + /// Get an exclusive slice of the underlying set of todo items. + pub fn todos_mut(&mut self) -> &mut [Todo] { + &mut self.todos + } + + /// Set the draft todo item text. + pub fn set_draft>(&mut self, draft: S) { + self.draft = draft.into(); + } + + /// Take the current draft text and replace it with an empty string. + pub fn take_draft(&mut self) -> String { + mem::replace(&mut self.draft, String::new()) + } + + /// Get the current visibility for these todos. + pub fn visibility(&self) -> Visibility { + self.visibility + } + + /// Set the visibility for these todoS. + pub fn set_visibility(&mut self, vis: Visibility) { + self.visibility = vis; + } +} + +/// Rendering helpers. +impl Todos { + fn header<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump> + where + 'a: 'bump, + { + dodrio!(bump, +
+

"todos"

+ (); + C::update_draft(root, vdom, input.value()); + }} onkeydown={|root, vdom, event| { + let event = event.unchecked_into::(); + if event.key_code() == keys::ENTER { + C::finish_draft(root, vdom); + } + }} class="new-todo" placeholder="What needs to be done?" autofocus=true value={self.draft.as_str()}/> +
+ ) + } + + fn todos_list<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump> + where + 'a: 'bump, + { + use dodrio::bumpalo::collections::Vec; + + let mut todos = Vec::with_capacity_in(self.todos.len(), bump); + todos.extend( + self.todos + .iter() + .filter(|t| match self.visibility { + Visibility::All => true, + Visibility::Active => !t.is_complete(), + Visibility::Completed => t.is_complete(), + }) + .map(|t| t.render(bump)), + ); + + dodrio!(bump, +
+ + +
    + { todos } +
+
+ ) + } + + fn footer<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump> + where + 'a: 'bump, + { + let completed_count = self.todos.iter().filter(|t| t.is_complete()).count(); + let incomplete_count = self.todos.len() - completed_count; + let items_left = if incomplete_count == 1 { + " item left" + } else { + " items left" + }; + let incomplete_count = bumpalo::format!(in bump, "{}", incomplete_count); + + let clear_completed_text = bumpalo::format!( + in bump, + "Clear completed ({})", + self.todos.iter().filter(|t| t.is_complete()).count() + ); + + dodrio!(bump, +
+ + { + bumpalo::vec![in ≎ text(incomplete_count.into_bump_str())] + } + { bumpalo::vec![in ≎ text(items_left)] } + +
    + { bumpalo::vec![in ≎ + self.visibility_swap(bump, "#/", Visibility::All), + self.visibility_swap(bump, "#/active", Visibility::Active), + self.visibility_swap(bump, "#/completed", Visibility::Completed) + ] } +
+ +
+ ) + } + + fn visibility_swap<'a, 'bump>( + &'a self, + bump: &'bump Bump, + url: &'static str, + target_vis: Visibility, + ) -> Node<'bump> + where + 'a: 'bump, + { + dodrio!(bump, +
+ ) + } +} + +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
  • + { bumpalo::vec![in ≎ text(target_vis.label())] } +