diff --git a/.travis.yml b/.travis.yml index f8c01c9..f377484 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,6 @@ matrix: - rust: nightly script: cargo test - rust: beta - script: cargo check --manifest-path examples/wasm/Cargo.toml + script: cargo check --manifest-path examples/stdweb/Cargo.toml - rust: stable - script: cargo check --manifest-path examples/wasm/Cargo.toml + script: cargo check --manifest-path examples/stdweb/Cargo.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d61a7..73faf77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + ## [0.1.1] - 2018-22-29 ### Added diff --git a/Cargo.toml b/Cargo.toml index 110140b..77c44e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = [ "typed-html", "macros", - "examples/wasm", + "examples/stdweb", "examples/rocket", "ui", ] diff --git a/examples/rocket/src/main.rs b/examples/rocket/src/main.rs index 2116227..440403b 100644 --- a/examples/rocket/src/main.rs +++ b/examples/rocket/src/main.rs @@ -9,8 +9,8 @@ use rocket::http::{ContentType, Status}; use rocket::response::{Responder, Result}; use rocket::{get, routes, Request, Response}; use std::io::Cursor; -use typed_html::types::LinkType; use typed_html::elements::FlowContent; +use typed_html::types::LinkType; use typed_html::{dom::DOMTree, html, text, OutputType}; struct Html(DOMTree); @@ -64,7 +64,7 @@ fn index() -> Html {

""

- ))) + : String))) } fn main() { diff --git a/examples/stdweb/Cargo.toml b/examples/stdweb/Cargo.toml new file mode 100644 index 0000000..3a4bf9e --- /dev/null +++ b/examples/stdweb/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "typed-html-stdweb-test" +version = "0.1.0" +authors = ["Bodil Stokke "] + +[dependencies] +typed-html = { path = "../../typed-html", features = ["stdweb"] } +stdweb = "0.4.10" diff --git a/examples/wasm/README.md b/examples/stdweb/README.md similarity index 100% rename from examples/wasm/README.md rename to examples/stdweb/README.md diff --git a/examples/wasm/Web.toml b/examples/stdweb/Web.toml similarity index 100% rename from examples/wasm/Web.toml rename to examples/stdweb/Web.toml diff --git a/examples/stdweb/src/main.rs b/examples/stdweb/src/main.rs new file mode 100644 index 0000000..5c6e43b --- /dev/null +++ b/examples/stdweb/src/main.rs @@ -0,0 +1,31 @@ +#![recursion_limit = "256"] + +extern crate stdweb; +extern crate typed_html; + +use stdweb::web::{self, INode}; +use typed_html::dom::Node; +use typed_html::html; +use typed_html::output::stdweb::Stdweb; + +fn main() { + let mut doc = html!( +
+

"Hello Kitty"

+

+ "She is not a ""cat" + ". She is a ""human girl""." +

+

+ +

+
+ : Stdweb); + let vdom = doc.vnode(); + let document = web::document(); + let body = document.body().expect("no body element in doc"); + let tree = Stdweb::build(&document, vdom).unwrap(); + body.append_child(&tree); +} diff --git a/examples/wasm/www/.gitignore b/examples/stdweb/www/.gitignore similarity index 100% rename from examples/wasm/www/.gitignore rename to examples/stdweb/www/.gitignore diff --git a/examples/wasm/www/index.html b/examples/stdweb/www/index.html similarity index 100% rename from examples/wasm/www/index.html rename to examples/stdweb/www/index.html diff --git a/examples/wasm/www/index.js b/examples/stdweb/www/index.js similarity index 100% rename from examples/wasm/www/index.js rename to examples/stdweb/www/index.js diff --git a/examples/wasm/www/package.json b/examples/stdweb/www/package.json similarity index 100% rename from examples/wasm/www/package.json rename to examples/stdweb/www/package.json diff --git a/examples/wasm/www/yarn.lock b/examples/stdweb/www/yarn.lock similarity index 100% rename from examples/wasm/www/yarn.lock rename to examples/stdweb/www/yarn.lock diff --git a/examples/wasm/Cargo.toml b/examples/wasm/Cargo.toml deleted file mode 100644 index 3d868ee..0000000 --- a/examples/wasm/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "typed-html-wasm-test" -version = "0.1.0" -authors = ["Bodil Stokke "] - -[dependencies] -typed-html-macros = { path = "../../macros" } -typed-html = { path = "../../typed-html" } -stdweb = "0.4.10" diff --git a/examples/wasm/src/main.rs b/examples/wasm/src/main.rs deleted file mode 100644 index 4ad6b25..0000000 --- a/examples/wasm/src/main.rs +++ /dev/null @@ -1,59 +0,0 @@ -#![recursion_limit = "256"] - -extern crate stdweb; -extern crate typed_html; -extern crate typed_html_macros; - -use stdweb::web::{self, Element, IElement, INode}; -use typed_html::dom::{Node, VNode}; -use typed_html::events::Events; -use typed_html::{for_events, html, DOM}; - -fn install_handlers(target: &Element, handlers: &mut Events) { - for_events!(handler in handlers => { - handler.attach(target); - }); -} - -fn build( - document: &web::Document, - vnode: VNode<'_, DOM>, -) -> Result { - match vnode { - VNode::Text(text) => Ok(document.create_text_node(&text).into()), - VNode::Element(element) => { - let mut node = document.create_element(element.name)?; - for (key, value) in element.attributes { - node.set_attribute(&key, &value)?; - } - install_handlers(&node, element.events); - for child in element.children { - let child_node = build(document, child)?; - node.append_child(&child_node); - } - Ok(node.into()) - } - } -} - -fn main() { - let mut doc = html!( -
-

"Hello Kitty"

-

- "She is not a ""cat" - ". She is a ""human girl""." -

-

- -

-
- ); - let vdom = doc.vnode(); - let document = web::document(); - let body = document.body().expect("no body element in doc"); - let tree = build(&document, vdom).unwrap(); - body.append_child(&tree); -} diff --git a/macros/src/config.rs b/macros/src/config.rs index 07ec0a5..aee38b4 100644 --- a/macros/src/config.rs +++ b/macros/src/config.rs @@ -55,70 +55,3 @@ pub static SELF_CLOSING: &[&str] = &[ "track", "wbr", ]; - -// This NEEDS to be a sorted list! -pub static ATTR_EVENTS: &[&str] = &[ - "abort", - // "autocomplete", - // "autocompleteerror", - "blur", - // "cancel", - // "canplay", - // "canplaythrough", - "change", - "click", - // "close", - "contextmenu", - // "cuechange", - "dblclick", - "drag", - "dragend", - "dragenter", - "dragexit", - "dragleave", - "dragover", - "dragstart", - "drop", - // "durationchange", - // "emptied", - // "ended", - "error", - "focus", - "input", - // "invalid", - "keydown", - "keypress", - "keyup", - "load", - // "loadeddata", - // "loadedmetadata", - "loadstart", - "mousedown", - "mouseenter", - "mouseleave", - "mousemove", - "mouseout", - "mouseover", - "mouseup", - "mousewheel", - // "pause", - // "play", - // "playing", - "progress", - // "ratechange", - // "reset", - "resize", - "scroll", - // "seeked", - // "seeking", - // "select", - // "show", - // "sort", - // "stalled", - "submit", - // "suspend", - // "timeupdate", - // "toggle", - // "volumechange", - // "waiting", -]; diff --git a/macros/src/declare.rs b/macros/src/declare.rs index f461cb9..fd3efd9 100644 --- a/macros/src/declare.rs +++ b/macros/src/declare.rs @@ -1,7 +1,7 @@ -use proc_macro2::{Ident, Literal, Span, TokenStream, TokenTree}; +use proc_macro2::{Ident, Literal, TokenStream, TokenTree}; use quote::quote; -use config::{global_attrs, ATTR_EVENTS, SELF_CLOSING}; +use config::{global_attrs, SELF_CLOSING}; use error::ParseError; use ident; use lexer::{Lexer, Token}; @@ -104,10 +104,9 @@ impl Declare { quote!( pub struct #elem_name where T: ::OutputType { - phantom_output: std::marker::PhantomData, pub attrs: #attr_type_name, pub data_attributes: Vec<(&'static str, String)>, - pub events: ::events::Events, + pub events: T::Events, #body } ) @@ -144,8 +143,7 @@ impl Declare { impl #elem_name where T: ::OutputType { pub fn new(#args) -> Self { #elem_name { - phantom_output: std::marker::PhantomData, - events: ::events::Events::default(), + events: T::Events::default(), #body } } @@ -307,13 +305,11 @@ impl Declare { let print_children = if self.req_children.is_empty() { if self.opt_children.is_some() { if !SELF_CLOSING.contains(&elem_name.to_string().as_str()) { - quote!(if self.children.is_empty() { - write!(f, ">", #name) - } else { + quote!( write!(f, ">")?; #print_opt_children write!(f, "", #name) - }) + ) } else { quote!(if self.children.is_empty() { write!(f, " />") @@ -323,12 +319,10 @@ impl Declare { write!(f, "", #name) }) } + } else if !SELF_CLOSING.contains(&elem_name.to_string().as_str()) { + quote!(write!(f, ">", #name)) } else { - if !SELF_CLOSING.contains(&elem_name.to_string().as_str()) { - quote!(write!(f, ">", #name)) - } else { - quote!(write!(f, "/>")) - } + quote!(write!(f, "/>")) } } else { quote!( @@ -349,20 +343,11 @@ impl Declare { )); } - let mut print_events = TokenStream::new(); - for event in ATTR_EVENTS { - let event_name = TokenTree::Ident(Ident::new(event, Span::call_site())); - let event_str = TokenTree::Literal(Literal::string(event)); - print_events.extend(quote!( - if let Some(ref value) = self.events.#event_name { - write!(f, " on{}=\"{}\"", #event_str, - ::htmlescape::encode_attribute(value.render().unwrap().as_str()))?; - } - )); - } - quote!( - impl std::fmt::Display for #elem_name where T: ::OutputType { + impl std::fmt::Display for #elem_name + where + T: ::OutputType, + { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "<{}", #name)?; #print_attrs @@ -370,7 +355,7 @@ impl Declare { write!(f, " data-{}=\"{}\"", key, ::htmlescape::encode_attribute(&value))?; } - #print_events + write!(f, "{}", self.events)?; #print_children } } diff --git a/macros/src/grammar.lalrpop b/macros/src/grammar.lalrpop index 51110d8..99c5da7 100644 --- a/macros/src/grammar.lalrpop +++ b/macros/src/grammar.lalrpop @@ -160,12 +160,19 @@ CodeBlock: Group = BraceGroupToken => match <> { _ => unreachable!() }; -pub Node: Node = { +Node: Node = { Element => Node::Element(<>), TextNode => Node::Text(<>), CodeBlock => Node::Block(<>), }; +pub NodeWithType: (Node, Option>) = { + Node => (<>, None), + ":" => { + let (node, spec) = (<>); + (node, Some(spec)) + }, +}; // The declare macro diff --git a/macros/src/html.rs b/macros/src/html.rs index 6f2bae4..9ce92ff 100644 --- a/macros/src/html.rs +++ b/macros/src/html.rs @@ -1,10 +1,10 @@ -use proc_macro2::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree}; +use proc_macro2::{Delimiter, Group, Ident, Literal, Span, TokenStream, TokenTree}; use quote::{quote, quote_spanned}; -use config::{required_children, ATTR_EVENTS}; +use config::required_children; use error::ParseError; use ident; -use lexer::{Lexer, Token}; +use lexer::{to_stream, Lexer, Token}; use map::StringyMap; use parser::grammar; @@ -18,33 +18,34 @@ pub enum Node { } impl Node { - pub fn into_token_stream(self) -> Result { + pub fn into_token_stream(self, ty: &Option>) -> Result { match self { - Node::Element(el) => el.into_token_stream(), + Node::Element(el) => el.into_token_stream(ty), Node::Text(text) => { let text = TokenTree::Literal(text); Ok(quote!(Box::new(typed_html::dom::TextNode::new(#text.to_string())))) } Node::Block(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=> + let error = + "you cannot use a block as a top level element or a required child element"; + Err(quote_spanned! { span=> compile_error! { #error } }) } } } - fn into_child_stream(self) -> Result { + fn into_child_stream(self, ty: &Option>) -> Result { match self { Node::Element(el) => { - let el = el.into_token_stream()?; + let el = el.into_token_stream(ty)?; Ok(quote!( element.children.push(#el); )) } tx @ Node::Text(_) => { - let tx = tx.into_token_stream()?; + let tx = tx.into_token_stream(ty)?; Ok(quote!( element.children.push(#tx); )) @@ -92,10 +93,8 @@ fn extract_event_handlers( let prefix = "on"; if key_name.starts_with(prefix) { let event_name = &key_name[prefix.len()..]; - if ATTR_EVENTS.binary_search(&event_name).is_ok() { - let value = attrs.remove(&key).unwrap(); - events.insert(ident::new_raw(event_name, key.span()), value); - } + let value = attrs.remove(&key).unwrap(); + events.insert(ident::new_raw(event_name, key.span()), value); } } events @@ -121,7 +120,7 @@ fn is_string_literal(literal: &Literal) -> bool { } impl Element { - fn into_token_stream(mut self) -> Result { + fn into_token_stream(mut self, ty: &Option>) -> Result { let name = self.name; let name_str = name.to_string(); let typename: TokenTree = Ident::new(&name_str, name.span()).into(); @@ -151,12 +150,12 @@ impl Element { .children .split_off(req_names.len()) .into_iter() - .map(Node::into_child_stream) + .map(|node| node.into_child_stream(ty)) .collect::, TokenStream>>()?; let req_children = self .children .into_iter() - .map(Node::into_token_stream) + .map(|node| node.into_token_stream(ty)) .collect::, TokenStream>>()?; let mut body = TokenStream::new(); @@ -214,6 +213,16 @@ impl Element { body.extend(opt_children); for (key, value) in events.iter() { + if ty.is_none() { + let mut err = quote_spanned! { key.span() => + compile_error! { "when using event handlers, you must declare the output type inside the html! macro" } + }; + let hint = quote_spanned! { Span::call_site() => + compile_error! { "for example: change html!(
...
) to html!(
...
: String)" } + }; + err.extend(hint); + return Err(err); + } let key = TokenTree::Ident(key.clone()); let value = process_value(value); body.extend(quote!( @@ -226,9 +235,15 @@ impl Element { args.extend(quote!( #arg, )); } + let mut type_annotation = TokenStream::new(); + if let Some(ty) = ty { + let type_var = to_stream(ty.clone()); + type_annotation.extend(quote!(: typed_html::elements::#typename<#type_var>)); + } + Ok(quote!( { - let mut element = typed_html::elements::#typename::new(#args); + let mut element #type_annotation = typed_html::elements::#typename::new(#args); #body Box::new(element) } @@ -237,6 +252,6 @@ impl Element { } // FIXME report a decent error when the macro contains multiple top level elements -pub fn expand_html(input: &[Token]) -> Result { - grammar::NodeParser::new().parse(Lexer::new(input)) +pub fn expand_html(input: &[Token]) -> Result<(Node, Option>), ParseError> { + grammar::NodeWithTypeParser::new().parse(Lexer::new(input)) } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index fd5b315..32b4633 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -32,7 +32,7 @@ pub fn html(input: TokenStream) -> TokenStream { let result = html::expand_html(&stream); TokenStream::from(match result { Err(err) => error::parse_error(&stream, &err), - Ok(node) => match node.into_token_stream() { + Ok((node, ty)) => match node.into_token_stream(&ty) { Err(err) => err, Ok(success) => success, }, diff --git a/typed-html/Cargo.toml b/typed-html/Cargo.toml index 3eaa603..9c05306 100644 --- a/typed-html/Cargo.toml +++ b/typed-html/Cargo.toml @@ -15,13 +15,13 @@ travis-ci = { repository = "bodil/typed-html" } maintenance = { status = "actively-developed" } [dependencies] -typed-html-macros = "0.1.1" +typed-html-macros = { path = "../macros" } strum = "0.11.0" strum_macros = "0.11.0" mime = "0.3.12" language-tags = "0.2.2" http = "0.1.13" htmlescape = "0.3.1" -stdweb = "0.4.10" proc-macro-hack = "0.5.2" proc-macro-nested = "0.1.0" +stdweb = { version = "0.4.10", optional = true } diff --git a/typed-html/src/dom.rs b/typed-html/src/dom.rs index 596e29a..b6bf9a4 100644 --- a/typed-html/src/dom.rs +++ b/typed-html/src/dom.rs @@ -5,7 +5,6 @@ use std::marker::PhantomData; use super::OutputType; use elements::{FlowContent, PhrasingContent}; -use events::Events; use htmlescape::encode_minimal; /// A boxed DOM tree, as returned from the `html!` macro. @@ -50,7 +49,7 @@ pub enum VNode<'a, T: OutputType + 'a> { pub struct VElement<'a, T: OutputType + 'a> { pub name: &'static str, pub attributes: Vec<(&'static str, String)>, - pub events: &'a mut Events, + pub events: &'a mut T::Events, pub children: Vec>, } @@ -73,7 +72,10 @@ pub trait Node: Display { fn vnode(&mut self) -> VNode; } -impl IntoIterator for Box> where T: OutputType { +impl IntoIterator for Box> +where + T: OutputType, +{ type Item = Box>; type IntoIter = std::vec::IntoIter>>; diff --git a/typed-html/src/events.rs b/typed-html/src/events.rs index 9ed90bb..23daefe 100644 --- a/typed-html/src/events.rs +++ b/typed-html/src/events.rs @@ -1,129 +1,8 @@ //! Event handlers. -use std::marker::PhantomData; -use stdweb::web::event::*; -use stdweb::web::{Element, EventListenerHandle, IEventTarget}; - -use super::{OutputType, DOM}; - -macro_rules! declare_events { - ($($name:ident : $type:ty ,)*) => { - /// Container type for DOM events. - pub struct Events { - $( - pub $name: Option>>, - )* - } - - impl Default for Events { - fn default() -> Self { - Events { - $( - $name: None, - )* - } - } - } - - /// Iterate over the defined events on a DOM object. - /// - /// # Examples - /// - /// ``` - /// # use typed_html::{html, for_events}; - /// # use typed_html::dom::{DOMTree, VNode}; - /// # fn main() { - /// let mut doc: DOMTree = html!( - ///