Dodrio support with a bespoke macro.
This commit is contained in:
parent
dbb4ba8738
commit
813121b3a7
|
@ -4,5 +4,6 @@ members = [
|
||||||
"macros",
|
"macros",
|
||||||
"examples/stdweb",
|
"examples/stdweb",
|
||||||
"examples/rocket",
|
"examples/rocket",
|
||||||
|
"examples/dodrio",
|
||||||
"ui",
|
"ui",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
|
@ -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
|
||||||
|
```
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
|
@ -25,3 +25,6 @@ quote = "0.6.10"
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
lalrpop = "0.16.1"
|
lalrpop = "0.16.1"
|
||||||
version_check = "0.1.5"
|
version_check = "0.1.5"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
dodrio = []
|
||||||
|
|
|
@ -174,6 +174,10 @@ pub NodeWithType: (Node, Option<Vec<Token>>) = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub NodeWithBump: (Ident, Node) = {
|
||||||
|
<Ident> "," <Node>,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// The declare macro
|
// The declare macro
|
||||||
|
|
||||||
|
|
|
@ -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)]
|
#[derive(Clone)]
|
||||||
|
@ -119,6 +139,15 @@ fn is_string_literal(literal: &Literal) -> bool {
|
||||||
literal.to_string().starts_with('"')
|
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 {
|
impl Element {
|
||||||
fn into_token_stream(mut self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
|
fn into_token_stream(mut self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
|
||||||
let name = self.name;
|
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
|
// 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> {
|
pub fn expand_html(input: &[Token]) -> Result<(Node, Option<Vec<Token>>), ParseError> {
|
||||||
grammar::NodeWithTypeParser::new().parse(Lexer::new(input))
|
grammar::NodeWithTypeParser::new().parse(Lexer::new(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn expand_dodrio(input: &[Token]) -> Result<(Ident, Node), ParseError> {
|
||||||
|
grammar::NodeWithBumpParser::new().parse(Lexer::new(input))
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
#![recursion_limit = "128"]
|
#![recursion_limit = "128"]
|
||||||
#![cfg_attr(can_show_location_of_runtime_parse_error, feature(proc_macro_span))]
|
#![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_macro;
|
||||||
extern crate proc_macro2;
|
|
||||||
extern crate proc_macro_hack;
|
|
||||||
extern crate quote;
|
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro_hack::proc_macro_hack;
|
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
|
/// This macro is used by `typed_html` internally to generate types and
|
||||||
/// implementations for HTML elements.
|
/// implementations for HTML elements.
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
|
|
|
@ -25,3 +25,8 @@ htmlescape = "0.3.1"
|
||||||
proc-macro-hack = "0.5.4"
|
proc-macro-hack = "0.5.4"
|
||||||
proc-macro-nested = "0.1.3"
|
proc-macro-nested = "0.1.3"
|
||||||
stdweb = { version = "0.4.14", optional = true }
|
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"]
|
||||||
|
|
|
@ -199,6 +199,10 @@ use std::fmt::Display;
|
||||||
#[proc_macro_hack(support_nested)]
|
#[proc_macro_hack(support_nested)]
|
||||||
pub use typed_html_macros::html;
|
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 dom;
|
||||||
pub mod elements;
|
pub mod elements;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
|
|
@ -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!()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,4 @@
|
||||||
#[cfg(feature = "stdweb")]
|
#[cfg(feature = "stdweb")]
|
||||||
pub mod stdweb;
|
pub mod stdweb;
|
||||||
|
#[cfg(feature = "dodrio_macro")]
|
||||||
|
pub mod dodrio;
|
||||||
|
|
Loading…
Reference in New Issue