Dodrio support with a bespoke macro.

This commit is contained in:
Bodil Stokke 2019-03-16 03:18:49 +00:00
parent dbb4ba8738
commit 813121b3a7
13 changed files with 416 additions and 5 deletions

View File

@ -4,5 +4,6 @@ members = [
"macros",
"examples/stdweb",
"examples/rocket",
"examples/dodrio",
"ui",
]

View File

@ -0,0 +1,34 @@
[package]
name = "dodrio-counter"
version = "0.1.0"
authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
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"

21
examples/dodrio/README.md Normal file
View File

@ -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
```

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Counter</title>
</head>
<body>
<script src="pkg/dodrio_counter.js"></script>
<script>
wasm_bindgen("pkg/dodrio_counter_bg.wasm");
</script>
</body>
</html>

View File

@ -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,
<div>
<button onclick={|root, vdom, _event| {
// Cast the root render component to a `Counter`, since
// we know that's what it is.
let counter = root.unwrap_mut::<Counter>();
// Increment the counter.
counter.increment();
// Since the count has updated, we should re-render the
// counter on the next animation frame.
vdom.schedule_render();
}}>"+"</button>
{ text(count.into_bump_str()) }
<button onclick={|root, vdom, _event| {
// Same as above, but decrementing instead of incrementing.
root.unwrap_mut::<Counter>().decrement();
vdom.schedule_render();
}}>"-"</button>
</div>
)
}
}
#[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 `<body>`.
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 `<body>`.
let vdom = dodrio::Vdom::new(&body, counter);
// Run the virtual DOM and its listeners forever.
vdom.forget();
}

View File

@ -25,3 +25,6 @@ quote = "0.6.10"
[build-dependencies]
lalrpop = "0.16.1"
version_check = "0.1.5"
[features]
dodrio = []

View File

@ -174,6 +174,10 @@ pub NodeWithType: (Node, Option<Vec<Token>>) = {
},
};
pub NodeWithBump: (Ident, Node) = {
<Ident> "," <Node>,
};
// The declare macro

View File

@ -60,6 +60,26 @@ impl Node {
}
}
}
pub fn into_dodrio_token_stream(
self,
bump: &Ident,
is_req_child: bool,
) -> Result<TokenStream, TokenStream> {
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<Vec<Token>>) -> Result<TokenStream, TokenStream> {
let name = self.name;
@ -249,9 +278,184 @@ impl Element {
}
))
}
fn into_dodrio_token_stream(
mut self,
bump: &Ident,
is_req_child: bool,
) -> Result<TokenStream, TokenStream> {
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::<Result<Vec<TokenStream>, TokenStream>>()?;
let req_children = self
.children
.into_iter()
.map(|node| node.into_dodrio_token_stream(bump, true))
.collect::<Result<Vec<TokenStream>, 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::output::dodrio::Dodrio> = 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<Vec<Token>>), 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))
}

View File

@ -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]

View File

@ -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"]

View File

@ -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;

View File

@ -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!()
}
}

View File

@ -1,2 +1,4 @@
#[cfg(feature = "stdweb")]
pub mod stdweb;
#[cfg(feature = "dodrio_macro")]
pub mod dodrio;