use proc_macro2::{Delimiter, Group, Ident, Literal, Span, TokenStream, TokenTree}; use quote::{quote, quote_spanned}; use crate::config::required_children; use crate::error::ParseError; use crate::ident; use crate::lexer::{to_stream, Lexer, Token}; use crate::map::StringyMap; use crate::parser::grammar; use std::iter::FromIterator; #[derive(Clone)] pub enum Node { Element(Element), Text(Literal), Block(Group), } impl Node { pub fn into_token_stream(self, ty: &Option>) -> Result { match self { 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=> compile_error! { #error } }) } } } fn into_child_stream(self, ty: &Option>) -> Result { match self { Node::Element(el) => { let el = el.into_token_stream(ty)?; Ok(quote!( element.children.push(#el); )) } tx @ Node::Text(_) => { let tx = tx.into_token_stream(ty)?; Ok(quote!( element.children.push(#tx); )) } Node::Block(group) => { let group: TokenTree = group.into(); Ok(quote!( for child in #group.into_iter() { element.children.push(child); } )) } } } #[cfg(feature = "dodrio")] 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) => Ok(dodrio_text_node(text)), 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=> compile_error! { #error } }) } } } } #[derive(Clone)] pub struct Element { pub name: Ident, pub attributes: StringyMap, 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(); for key in keys { let key_name = key.to_string(); let prefix = "data_"; if key_name.starts_with(prefix) { let value = attrs.remove(&key).unwrap(); data.insert(key_name[prefix.len()..].to_string(), value); } } data } fn extract_event_handlers( attrs: &mut 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()..]; 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 => { let content = g.stream(); quote!( [ #content ] ) } TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => { let content = g.stream(); quote!( ( #content ) ) } v => TokenStream::from_iter(vec![v.clone()]), } } fn is_string_literal(literal: &Literal) -> bool { // This is the worst API literal.to_string().starts_with('"') } #[allow(dead_code)] 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; let name_str = name.to_string(); let typename: TokenTree = Ident::new(&name_str, name.span()).into(); 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_child_stream(ty)) .collect::, TokenStream>>()?; let req_children = self .children .into_iter() .map(|node| node.into_token_stream(ty)) .collect::, TokenStream>>()?; let mut body = 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"; } body.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); body.extend(quote!( element.attrs.#key = Some(std::convert::Into::into(#value)); )); } } } for (key, value) in data_attrs .iter() .map(|(k, v)| (TokenTree::from(Literal::string(&k)), v.clone())) { body.extend(quote!( element.data_attributes.push((#key, #value.into())); )); } 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!( element.events.#key = Some(#value.into()); )); } let mut args = TokenStream::new(); for arg in req_children { 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 #type_annotation = typed_html::elements::#typename::new(#args); #body Box::new(element) } )) } #[cfg(feature = "dodrio")] 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_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() { 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()); 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 attr_max_len = self.attributes.len() + data_attrs.len(); let mut builder = quote!( let mut attr_list = dodrio::bumpalo::collections::Vec::with_capacity_in(#attr_max_len, #bump); ); // Build the attributes. for (key, _) in self.attributes.iter() { 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())) { builder.extend(quote!( attr_list.push(dodrio::builder::attr( #key, dodrio::bumpalo::format!( in &#bump, "{}", #value ).into_bump_str() )); )); } builder.extend(quote!( let mut node = dodrio::builder::ElementBuilder::new(#bump, #tag_name) .attributes(attr_list) )); // Build an array of event listeners. let mut event_array = TokenStream::new(); for (key, value) in events.iter() { let key = TokenTree::from(Literal::string(&stringify_ident(key))); let value = process_value(value); event_array.extend(quote!( dodrio::builder::on(&#bump, #key, #value), )); } builder.extend(quote!( .listeners([#event_array]); )); // 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_raw( &format!("req_child_{}", index), Span::call_site(), )); let child_node = TokenTree::from(ident::new_raw( &format!("child_node_{}", index), Span::call_site(), )); make_req_children.extend(quote!( let (#req_child, #child_node) = #child; )); child_array.extend(quote!( #child_node, )); child_builder.extend(quote!( node = node.child(#child_node); )); arg_list.push(req_child); } // 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); )); } 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}) ); } 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)) } #[allow(dead_code)] pub fn expand_dodrio(input: &[Token]) -> Result<(Ident, Node), ParseError> { grammar::NodeWithBumpParser::new().parse(Lexer::new(input)) }