diff --git a/macros/src/config.rs b/macros/src/config.rs index 8957c1b..80d8aef 100644 --- a/macros/src/config.rs +++ b/macros/src/config.rs @@ -31,6 +31,75 @@ pub fn global_attrs(span: Span) -> StringyMap { insert("style", "String"); insert("tabindex", "isize"); insert("title", "String"); + + // FIXME ARIA and XML attrs missing } attrs } + +// 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 a623e6d..e5a1a5e 100644 --- a/macros/src/declare.rs +++ b/macros/src/declare.rs @@ -1,6 +1,6 @@ -use proc_macro::{quote, Ident, Literal, TokenStream, TokenTree}; +use proc_macro::{quote, Ident, Literal, Span, TokenStream, TokenTree}; -use config::global_attrs; +use config::{global_attrs, ATTR_EVENTS}; use error::ParseError; use lexer::{Lexer, Token}; use map::StringyMap; @@ -52,8 +52,7 @@ impl Declare { self.req_children.iter().map(|child| { let child_name: TokenTree = Ident::new(&format!("child_{}", child.to_string()), child.span()).into(); - let child_type: TokenTree = - Ident::new(&format!("{}", child.to_string()), child.span()).into(); + let child_type: TokenTree = Ident::new(&child.to_string(), child.span()).into(); let child_str = Literal::string(&child.to_string()).into(); (child_name, child_type, child_str) }) @@ -103,7 +102,8 @@ impl Declare { quote!( pub struct $elem_name { pub attrs: $attr_type_name, - pub data_attributes: Vec<(String, String)>, + pub data_attributes: Vec<(&'static str, String)>, + pub events: ::events::Events, $body } ) @@ -128,6 +128,7 @@ impl Declare { attrs: $attr_type_name { $attrs }, )); body.extend(quote!(data_attributes: Vec::new(),)); + for (child_name, _, _) in self.req_children() { body.extend(quote!( $child_name, )); } @@ -139,6 +140,7 @@ impl Declare { impl $elem_name { pub fn new($args) -> Self { $elem_name { + events: ::events::Events::default(), $body } } @@ -156,7 +158,7 @@ impl Declare { } let mut opt_children = TokenStream::new(); if self.opt_children.is_some() { - opt_children.extend(quote!(for child in &self.children { + opt_children.extend(quote!(for child in &mut self.children { children.push(child.vnode()); })); } @@ -165,7 +167,7 @@ impl Declare { for (attr_name, _, attr_str) in self.attrs() { push_attrs.extend(quote!( if let Some(ref value) = self.attrs.$attr_name { - attributes.push(($attr_str.to_string(), value.to_string())); + attributes.push(($attr_str, value.to_string())); } )); } @@ -173,9 +175,7 @@ impl Declare { quote!( let mut attributes = Vec::new(); $push_attrs - for (key, value) in &self.data_attributes { - attributes.push((format!("data-{}", key), value.to_string())); - } + attributes.extend(self.data_attributes.clone()); let mut children = Vec::new(); $req_children @@ -184,6 +184,7 @@ impl Declare { ::dom::VNode::Element(::dom::VElement { name: $elem_name, attributes, + events: &mut self.events, children }) ) @@ -194,7 +195,7 @@ impl Declare { let vnode = self.impl_vnode(); quote!( impl ::dom::Node for $elem_name { - fn vnode(&self) -> ::dom::VNode { + fn vnode<'a>(&'a mut self) -> ::dom::VNode<'a> { $vnode } } @@ -215,7 +216,7 @@ impl Declare { for (attr_name, _, attr_str) in self.attrs() { push_attrs.extend(quote!( if let Some(ref value) = self.attrs.$attr_name { - out.push(($attr_str.to_string(), value.to_string())); + out.push(($attr_str, value.to_string())); } )); } @@ -234,11 +235,11 @@ impl Declare { &[ $reqs ] } - fn attributes(&self) -> Vec<(String, String)> { + fn attributes(&self) -> Vec<(&'static str, String)> { let mut out = Vec::new(); $push_attrs for (key, value) in &self.data_attributes { - out.push((format!("data-{}", key), value.to_string())); + out.push((key, value.to_string())); } out } @@ -308,6 +309,18 @@ 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()))?; + } + )); + } + quote!( impl std::fmt::Display for $elem_name { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { @@ -317,6 +330,7 @@ impl Declare { write!(f, " data-{}=\"{}\"", key, ::htmlescape::encode_attribute(&value))?; } + $print_events $print_children } } diff --git a/macros/src/grammar.lalrpop b/macros/src/grammar.lalrpop index fe5d2b1..f44e74e 100644 --- a/macros/src/grammar.lalrpop +++ b/macros/src/grammar.lalrpop @@ -198,7 +198,28 @@ TypeArgList: Vec = "<" TypeArgs ">" => { args }; -TypeSpec: Vec = Reference? TypePath TypeArgList? => { +FnReturnType: Vec = "-" ">" TypeSpec => { + let (dash, right, spec) = (<>); + let mut out = vec![dash, right]; + out.extend(spec); + out +}; + +FnArgList: Vec = ParenGroupToken FnReturnType? => { + let (args, rt) = (<>); + let mut out = vec![args]; + if let Some(rt) = rt { + out.extend(rt); + } + out +}; + +TypeArgSpec = { + TypeArgList, + FnArgList, +}; + +TypeSpec: Vec = Reference? TypePath TypeArgSpec? => { let (reference, path, args) = (<>); let mut out = Vec::new(); if let Some(reference) = reference { diff --git a/macros/src/html.rs b/macros/src/html.rs index 625eb6c..843383d 100644 --- a/macros/src/html.rs +++ b/macros/src/html.rs @@ -2,7 +2,7 @@ use proc_macro::{ quote, Delimiter, Diagnostic, Group, Ident, Level, Literal, TokenStream, TokenTree, }; -use config::required_children; +use config::{required_children, ATTR_EVENTS}; use error::ParseError; use lexer::{Lexer, Token}; use map::StringyMap; @@ -68,12 +68,31 @@ fn extract_data_attrs(attrs: &mut StringyMap) -> StringyMap, +) -> StringyMap { + let mut events = StringyMap::new(); + let keys: Vec = attrs.keys().cloned().collect(); + for key in keys { + let key_name = key.to_string(); + 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); + } + } + } + events +} + fn process_value(value: &TokenTree) -> TokenStream { match value { TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => { @@ -113,6 +132,7 @@ impl Element { .emit(); panic!(); } + 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)| { ( @@ -164,11 +184,19 @@ impl Element { .map(|(k, v)| (TokenTree::from(Literal::string(&k)), v.clone())) { body.extend(quote!( - element.data_attributes.push(($key.into(), $value.into())); + element.data_attributes.push(($key, $value.into())); )); } body.extend(opt_children); + for (key, value) in events.iter() { + let key = TokenTree::Ident(key.clone()); + let value = process_value(value); + body.extend(quote!( + element.events.$key = Some(Box::new($value)); + )); + } + let mut args = TokenStream::new(); for arg in req_children { args.extend(quote!( $arg, )); diff --git a/macros/src/lexer.rs b/macros/src/lexer.rs index 009f4a9..8b6436e 100644 --- a/macros/src/lexer.rs +++ b/macros/src/lexer.rs @@ -117,7 +117,7 @@ pub fn unroll_stream(stream: TokenStream, deep: bool) -> Vec { TokenTree::Ident(ident) => vec.push(ident.into()), TokenTree::Literal(literal) => vec.push(literal.into()), TokenTree::Punct(punct) => vec.push(punct.into()), - TokenTree::Group(ref group) if deep => { + TokenTree::Group(ref group) if deep && group.delimiter() != Delimiter::Parenthesis => { vec.push(Token::GroupOpen(group.delimiter(), group.span())); let sub = unroll_stream(group.stream(), deep); vec.extend(sub); diff --git a/rocket/src/main.rs b/rocket/src/main.rs index 8cf8a29..81ba954 100644 --- a/rocket/src/main.rs +++ b/rocket/src/main.rs @@ -31,6 +31,7 @@ fn index() -> Content { }) }

""

+ ).to_string()) diff --git a/typed-html/Cargo.toml b/typed-html/Cargo.toml index 8e68f30..ccee029 100644 --- a/typed-html/Cargo.toml +++ b/typed-html/Cargo.toml @@ -12,3 +12,4 @@ language-tags = "0.2.2" enumset = "0.3.12" http = "0.1.13" htmlescape = "0.3.1" +stdweb = "0.4.10" diff --git a/typed-html/src/bin/main.rs b/typed-html/src/bin/main.rs index 469e656..f44948a 100644 --- a/typed-html/src/bin/main.rs +++ b/typed-html/src/bin/main.rs @@ -35,9 +35,9 @@ fn main() { }) }

""

+ ); println!("{}", doc.to_string()); - println!("{:?}", doc.vnode()); } diff --git a/typed-html/src/dom.rs b/typed-html/src/dom.rs index ebb8a20..523a54e 100644 --- a/typed-html/src/dom.rs +++ b/typed-html/src/dom.rs @@ -1,11 +1,9 @@ //! DOM and virtual DOM types. -#![allow(non_camel_case_types)] -#![allow(dead_code)] - use std::fmt::Display; use elements::{FlowContent, PhrasingContent}; +use events::Events; use htmlescape::encode_minimal; /// An untyped representation of an HTML node. @@ -23,18 +21,17 @@ use htmlescape::encode_minimal; /// ``` /// /// [Node]: trait.Node.html -#[derive(Clone, Debug)] -pub enum VNode { - Text(String), - Element(VElement), +pub enum VNode<'a> { + Text(&'a str), + Element(VElement<'a>), } /// An untyped representation of an HTML element. -#[derive(Clone, Debug)] -pub struct VElement { +pub struct VElement<'a> { pub name: &'static str, - pub attributes: Vec<(String, String)>, - pub children: Vec, + pub attributes: Vec<(&'static str, String)>, + pub events: &'a mut Events, + pub children: Vec>, } /// Trait for rendering a typed HTML node. @@ -53,7 +50,7 @@ pub trait Node: Display { /// Render the node into a [`VNode`][VNode] tree. /// /// [VNode]: enum.VNode.html - fn vnode(&self) -> VNode; + fn vnode<'a>(&'a mut self) -> VNode<'a>; } /// Trait for querying a typed HTML element. @@ -79,7 +76,7 @@ pub trait Element: Node { /// /// This will convert attribute values into strings and return a vector of /// key/value pairs. - fn attributes(&self) -> Vec<(String, String)>; + fn attributes(&self) -> Vec<(&'static str, String)>; } /// An HTML text node. @@ -134,10 +131,11 @@ impl Display for TextNode { } impl Node for TextNode { - fn vnode(&self) -> VNode { - VNode::Text(self.0.clone()) + fn vnode<'a>(&'a mut self) -> VNode<'a> { + VNode::Text(&self.0) } } + impl IntoIterator for TextNode { type Item = TextNode; type IntoIter = std::vec::IntoIter; diff --git a/typed-html/src/events.rs b/typed-html/src/events.rs new file mode 100644 index 0000000..a1669a1 --- /dev/null +++ b/typed-html/src/events.rs @@ -0,0 +1,150 @@ +use std::marker::PhantomData; +use stdweb::web::event::*; +use stdweb::web::{Element, EventListenerHandle, IEventTarget}; + +macro_rules! declare_events { + ($($name:ident : $type:ty ,)*) => { + #[derive(Default)] + pub struct Events { + $( + pub $name: Option>>, + )* + } + + #[macro_export] + macro_rules! for_events { + ($event:ident in $events:expr => $body:block) => { + $( + if let Some(ref mut $event) = $events.$name $body + )* + } + } + } +} + +// TODO? these are all the "on*" attributes defined in the HTML5 standard, with +// the ones I've been unable to match to stdweb event types commented out. +// +// This needs review. + +declare_events! { + abort: ResourceAbortEvent, + // autocomplete: Event, + // autocompleteerror: Event, + blur: BlurEvent, + // cancel: Event, + // canplay: Event, + // canplaythrough: Event, + change: ChangeEvent, + click: ClickEvent, + // close: Event, + contextmenu: ContextMenuEvent, + // cuechange: Event, + dblclick: DoubleClickEvent, + drag: DragEvent, + dragend: DragEndEvent, + dragenter: DragEnterEvent, + dragexit: DragExitEvent, + dragleave: DragLeaveEvent, + dragover: DragOverEvent, + dragstart: DragStartEvent, + drop: DragDropEvent, + // durationchange: Event, + // emptied: Event, + // ended: Event, + error: ResourceErrorEvent, + focus: FocusEvent, + input: InputEvent, + // invalid: Event, + keydown: KeyDownEvent, + keypress: KeyPressEvent, + keyup: KeyUpEvent, + load: ResourceLoadEvent, + // loadeddata: Event, + // loadedmetadata: Event, + loadstart: LoadStartEvent, + mousedown: MouseDownEvent, + mouseenter: MouseEnterEvent, + mouseleave: MouseLeaveEvent, + mousemove: MouseMoveEvent, + mouseout: MouseOutEvent, + mouseover: MouseOverEvent, + mouseup: MouseUpEvent, + mousewheel: MouseWheelEvent, + // pause: Event, + // play: Event, + // playing: Event, + progress: ProgressEvent, + // ratechange: Event, + // reset: Event, + resize: ResizeEvent, + scroll: ScrollEvent, + // seeked: Event, + // seeking: Event, + // select: Event, + // show: Event, + // sort: Event, + // stalled: Event, + submit: SubmitEvent, + // suspend: Event, + // timeupdate: Event, + // toggle: Event, + // volumechange: Event, + // waiting: Event, +} + +/// Trait for event handlers. +pub trait EventHandler { + /// Build a callback function from this event handler. + /// + /// Returns `None` is this event handler can't be used to build a callback + /// function. This is usually the case if the event handler is a string + /// intended for server side rendering. + // fn build(self) -> Option>; + + fn attach(&mut self, target: &Element) -> EventListenerHandle; + + /// Render this event handler as a string. + /// + /// Returns `None` if this event handler cannot be rendered. Normally, the + /// only event handlers that can be rendered are string values intended for + /// server side rendering. + fn render(&self) -> Option; +} + +pub struct EFn(Option, PhantomData); + +impl EFn +where + F: FnMut(E) + 'static, + E: ConcreteEvent, +{ + pub fn new(f: F) -> Self { + EFn(Some(f), PhantomData) + } +} + +impl EventHandler for EFn +where + F: FnMut(E) + 'static, + E: ConcreteEvent, +{ + fn attach(&mut self, target: &Element) -> EventListenerHandle { + let handler = self.0.take().unwrap(); + target.add_event_listener(handler) + } + + fn render(&self) -> Option { + None + } +} + +impl<'a, EventType> EventHandler for &'a str { + fn attach(&mut self, _target: &Element) -> EventListenerHandle { + panic!("Silly wabbit, strings as event handlers are only for printing."); + } + + fn render(&self) -> Option { + Some(self.to_string()) + } +} diff --git a/typed-html/src/lib.rs b/typed-html/src/lib.rs index bec4ad0..704bc84 100644 --- a/typed-html/src/lib.rs +++ b/typed-html/src/lib.rs @@ -9,9 +9,11 @@ pub extern crate htmlescape; extern crate http; extern crate language_tags; extern crate mime; +extern crate stdweb; extern crate strum; extern crate typed_html_macros; pub mod dom; pub mod elements; +pub mod events; pub mod types; diff --git a/wasm/src/main.rs b/wasm/src/main.rs index d7ac32c..b8a3663 100644 --- a/wasm/src/main.rs +++ b/wasm/src/main.rs @@ -5,10 +5,18 @@ extern crate stdweb; extern crate typed_html; extern crate typed_html_macros; -use stdweb::web::{self, IElement, INode}; +use stdweb::web::{self, Element, IElement, INode}; +use typed_html::for_events; use typed_html::dom::{Node, VNode}; +use typed_html::events::{EFn, Events}; use typed_html_macros::html; +fn install_handlers(target: &Element, handlers: &mut Events) { + for_events!(handler in handlers => { + handler.attach(target); + }); +} + fn build( document: &web::Document, vnode: VNode, @@ -20,6 +28,7 @@ fn build( 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); @@ -30,13 +39,18 @@ fn build( } fn main() { - let doc = html!( + let mut doc = html!(

"Hello Kitty"

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

+

+ +

); let vdom = doc.vnode();