diff --git a/Cargo.toml b/Cargo.toml index 3d62b5ead8f..4730eb8b73b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ toml = { version = "0.4", optional = true } serde_yaml = { version = "0.8.3", optional = true } rmp-serde = { version = "0.13.7", optional = true } serde_cbor = { version = "0.9.0", optional = true } +yew-macro = { path = "crates/macro", optional = true } +yew-shared = { path = "crates/shared" } [dev-dependencies] serde_derive = "1" @@ -36,3 +38,32 @@ web_test = [] yaml = ["serde_yaml"] msgpack = ["rmp-serde"] cbor = ["serde_cbor"] +proc_macro = ["yew-macro"] + +[workspace] +members = [ + "crates/macro", + "crates/macro-impl", + "crates/shared", + "examples/counter", + "examples/crm", + "examples/custom_components", + "examples/dashboard", + "examples/file_upload", + "examples/fragments", + "examples/game_of_life", + "examples/inner_html", + "examples/js_callback", + "examples/large_table", + "examples/minimal", + "examples/mount_point", + "examples/multi_thread", + "examples/npm_and_rest", + "examples/routing", + "examples/server", + "examples/showcase", + "examples/textarea", + "examples/timer", + "examples/todomvc", + "examples/two_apps", +] diff --git a/crates/macro-impl/Cargo.toml b/crates/macro-impl/Cargo.toml new file mode 100644 index 00000000000..47ed1efd375 --- /dev/null +++ b/crates/macro-impl/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "yew-macro-impl" +version = "0.7.0" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +boolinator = "2.4.0" +lazy_static = "1.3.0" +proc-macro-hack = "0.5" +proc-macro2 = "0.4" +quote = "0.6" +syn = { version = "^0.15.34", features = ["full"] } diff --git a/crates/macro-impl/src/html_tree/html_block.rs b/crates/macro-impl/src/html_tree/html_block.rs new file mode 100644 index 00000000000..6e83e593b02 --- /dev/null +++ b/crates/macro-impl/src/html_tree/html_block.rs @@ -0,0 +1,51 @@ +use super::html_iterable::HtmlIterable; +use super::html_node::HtmlNode; +use crate::Peek; +use proc_macro2::Delimiter; +use quote::{quote, quote_spanned, ToTokens}; +use syn::braced; +use syn::buffer::Cursor; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::token; + +pub struct HtmlBlock { + content: BlockContent, + brace: token::Brace, +} + +enum BlockContent { + Node(HtmlNode), + Iterable(HtmlIterable), +} + +impl Peek<()> for HtmlBlock { + fn peek(cursor: Cursor) -> Option<()> { + cursor.group(Delimiter::Brace).map(|_| ()) + } +} + +impl Parse for HtmlBlock { + fn parse(input: ParseStream) -> ParseResult { + let content; + let brace = braced!(content in input); + let content = if HtmlIterable::peek(content.cursor()).is_some() { + BlockContent::Iterable(content.parse()?) + } else { + BlockContent::Node(content.parse()?) + }; + + Ok(HtmlBlock { brace, content }) + } +} + +impl ToTokens for HtmlBlock { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlBlock { content, brace } = self; + let new_tokens = match content { + BlockContent::Iterable(html_iterable) => quote! {#html_iterable}, + BlockContent::Node(html_node) => quote! {#html_node}, + }; + + tokens.extend(quote_spanned! {brace.span=> #new_tokens}); + } +} diff --git a/crates/macro-impl/src/html_tree/html_component.rs b/crates/macro-impl/src/html_tree/html_component.rs new file mode 100644 index 00000000000..7290906732c --- /dev/null +++ b/crates/macro-impl/src/html_tree/html_component.rs @@ -0,0 +1,215 @@ +use super::HtmlProp; +use super::HtmlPropSuffix; +use crate::Peek; +use boolinator::Boolinator; +use proc_macro2::Span; +use quote::{quote, quote_spanned, ToTokens}; +use syn::buffer::Cursor; +use syn::parse; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::spanned::Spanned; +use syn::{Ident, Token, Type}; + +pub struct HtmlComponent(HtmlComponentInner); + +impl Peek<()> for HtmlComponent { + fn peek(cursor: Cursor) -> Option<()> { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '<').as_option()?; + + HtmlComponent::peek_type(cursor) + } +} + +impl Parse for HtmlComponent { + fn parse(input: ParseStream) -> ParseResult { + let lt = input.parse::()?; + let HtmlPropSuffix { stream, div, gt } = input.parse()?; + if div.is_none() { + return Err(syn::Error::new_spanned( + HtmlComponentTag { lt, gt }, + "expected component tag be of form `< .. />`", + )); + } + + match parse(stream) { + Ok(comp) => Ok(HtmlComponent(comp)), + Err(err) => { + if err.to_string().starts_with("unexpected end of input") { + Err(syn::Error::new_spanned(div, err.to_string())) + } else { + Err(err) + } + } + } + } +} + +impl ToTokens for HtmlComponent { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlComponentInner { ty, props } = &self.0; + let vcomp = Ident::new("__yew_vcomp", Span::call_site()); + let vcomp_props = Ident::new("__yew_vcomp_props", Span::call_site()); + let override_props = props.iter().map(|props| match props { + Props::List(ListProps(vec_props)) => { + let check_props = vec_props.iter().map(|HtmlProp { name, .. }| { + quote_spanned! { name.span()=> #vcomp_props.#name; } + }); + + let set_props = vec_props.iter().map(|HtmlProp { name, value }| { + quote_spanned! { value.span()=> + #vcomp_props.#name = ::yew::virtual_dom::vcomp::Transformer::transform(&mut #vcomp, #value); + } + }); + + quote! { + #(#check_props#set_props)* + } + } + Props::With(WithProps(props)) => { + quote_spanned! { props.span()=> #vcomp_props = #props; } + } + }); + + tokens.extend(quote_spanned! { ty.span()=> { + let (mut #vcomp_props, mut #vcomp) = ::yew::virtual_dom::VComp::lazy::<#ty>(); + #(#override_props)* + #vcomp.set_props(#vcomp_props); + ::yew::virtual_dom::VNode::VComp(#vcomp) + }}); + } +} + +impl HtmlComponent { + fn double_colon(mut cursor: Cursor) -> Option { + for _ in 0..2 { + let (punct, c) = cursor.punct()?; + (punct.as_char() == ':').as_option()?; + cursor = c; + } + + Some(cursor) + } + + fn peek_type(mut cursor: Cursor) -> Option<()> { + let mut type_str: String = "".to_owned(); + let mut colons_optional = true; + + loop { + let mut found_colons = false; + let mut post_colons_cursor = cursor; + if let Some(c) = Self::double_colon(post_colons_cursor) { + found_colons = true; + post_colons_cursor = c; + } else if !colons_optional { + break; + } + + if let Some((ident, c)) = post_colons_cursor.ident() { + cursor = c; + if found_colons { + type_str += "::"; + } + type_str += &ident.to_string(); + } else { + break; + } + + // only first `::` is optional + colons_optional = false; + } + + (!type_str.is_empty()).as_option()?; + (type_str.to_lowercase() != type_str).as_option() + } +} + +pub struct HtmlComponentInner { + ty: Type, + props: Option, +} + +impl Parse for HtmlComponentInner { + fn parse(input: ParseStream) -> ParseResult { + let ty = input.parse()?; + // backwards compat + let _ = input.parse::(); + + let props = if let Some(prop_type) = Props::peek(input.cursor()) { + match prop_type { + PropType::List => input.parse().map(Props::List).map(Some)?, + PropType::With => input.parse().map(Props::With).map(Some)?, + } + } else { + None + }; + + Ok(HtmlComponentInner { ty, props }) + } +} + +struct HtmlComponentTag { + lt: Token![<], + gt: Token![>], +} + +impl ToTokens for HtmlComponentTag { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlComponentTag { lt, gt } = self; + tokens.extend(quote! {#lt#gt}); + } +} + +enum PropType { + List, + With, +} + +enum Props { + List(ListProps), + With(WithProps), +} + +impl Peek for Props { + fn peek(cursor: Cursor) -> Option { + let (ident, _) = cursor.ident()?; + let prop_type = if ident.to_string() == "with" { + PropType::With + } else { + PropType::List + }; + + Some(prop_type) + } +} + +struct ListProps(Vec); +impl Parse for ListProps { + fn parse(input: ParseStream) -> ParseResult { + let mut props: Vec = Vec::new(); + while HtmlProp::peek(input.cursor()).is_some() { + props.push(input.parse::()?); + } + + for prop in &props { + if prop.name.to_string() == "type" { + return Err(syn::Error::new_spanned(&prop.name, "expected identifier")); + } + } + + Ok(ListProps(props)) + } +} + +struct WithProps(Ident); +impl Parse for WithProps { + fn parse(input: ParseStream) -> ParseResult { + let with = input.parse::()?; + if with.to_string() != "with" { + return Err(input.error("expected to find `with` token")); + } + let props = input.parse::()?; + let _ = input.parse::(); + Ok(WithProps(props)) + } +} diff --git a/crates/macro-impl/src/html_tree/html_iterable.rs b/crates/macro-impl/src/html_tree/html_iterable.rs new file mode 100644 index 00000000000..ffff4c49704 --- /dev/null +++ b/crates/macro-impl/src/html_tree/html_iterable.rs @@ -0,0 +1,53 @@ +use crate::Peek; +use boolinator::Boolinator; +use proc_macro2::TokenStream; +use quote::{quote_spanned, ToTokens}; +use syn::buffer::Cursor; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::spanned::Spanned; +use syn::{Expr, Token}; + +pub struct HtmlIterable(Expr); + +impl Peek<()> for HtmlIterable { + fn peek(cursor: Cursor) -> Option<()> { + let (ident, _) = cursor.ident()?; + (ident.to_string() == "for").as_option() + } +} + +impl Parse for HtmlIterable { + fn parse(input: ParseStream) -> ParseResult { + let for_token = input.parse::()?; + + match input.parse() { + Ok(expr) => Ok(HtmlIterable(expr)), + Err(err) => { + if err.to_string().starts_with("unexpected end of input") { + Err(syn::Error::new_spanned( + for_token, + "expected expression after `for`", + )) + } else { + Err(err) + } + } + } + } +} + +impl ToTokens for HtmlIterable { + fn to_tokens(&self, tokens: &mut TokenStream) { + let expr = &self.0; + let new_tokens = quote_spanned! {expr.span()=> { + let mut __yew_vlist = ::yew::virtual_dom::VList::new(); + let __yew_nodes: &mut ::std::iter::Iterator = &mut(#expr); + for __yew_node in __yew_nodes.into_iter() { + __yew_vlist.add_child(__yew_node.into()); + } + ::yew::virtual_dom::VNode::from(__yew_vlist) + }}; + + tokens.extend(new_tokens); + } +} diff --git a/crates/macro-impl/src/html_tree/html_list.rs b/crates/macro-impl/src/html_tree/html_list.rs new file mode 100644 index 00000000000..d461cbcea3f --- /dev/null +++ b/crates/macro-impl/src/html_tree/html_list.rs @@ -0,0 +1,151 @@ +use super::HtmlTree; +use crate::Peek; +use boolinator::Boolinator; +use quote::{quote, ToTokens}; +use syn::buffer::Cursor; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::Token; + +pub struct HtmlList(pub Vec); + +impl Peek<()> for HtmlList { + fn peek(cursor: Cursor) -> Option<()> { + HtmlListOpen::peek(cursor) + .or_else(|| HtmlListClose::peek(cursor)) + .map(|_| ()) + } +} + +impl Parse for HtmlList { + fn parse(input: ParseStream) -> ParseResult { + if HtmlListClose::peek(input.cursor()).is_some() { + return match input.parse::() { + Ok(close) => Err(syn::Error::new_spanned( + close, + "this close tag has no corresponding open tag", + )), + Err(err) => Err(err), + }; + } + + let open = input.parse::()?; + if !HtmlList::verify_end(input.cursor()) { + return Err(syn::Error::new_spanned( + open, + "this open tag has no corresponding close tag", + )); + } + + let mut children: Vec = vec![]; + while HtmlListClose::peek(input.cursor()).is_none() { + children.push(input.parse()?); + } + + input.parse::()?; + + Ok(HtmlList(children)) + } +} + +impl ToTokens for HtmlList { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let html_trees = &self.0; + tokens.extend(quote! { + ::yew::virtual_dom::VNode::VList( + ::yew::virtual_dom::vlist::VList { + childs: vec![#(#html_trees,)*], + } + ) + }); + } +} + +impl HtmlList { + fn verify_end(mut cursor: Cursor) -> bool { + let mut list_stack_count = 1; + loop { + if HtmlListOpen::peek(cursor).is_some() { + list_stack_count += 1; + } else if HtmlListClose::peek(cursor).is_some() { + list_stack_count -= 1; + if list_stack_count == 0 { + break; + } + } + if let Some((_, next)) = cursor.token_tree() { + cursor = next; + } else { + break; + } + } + + list_stack_count == 0 + } +} + +struct HtmlListOpen { + lt: Token![<], + gt: Token![>], +} + +impl Peek<()> for HtmlListOpen { + fn peek(cursor: Cursor) -> Option<()> { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '<').as_option()?; + + let (punct, _) = cursor.punct()?; + (punct.as_char() == '>').as_option() + } +} + +impl Parse for HtmlListOpen { + fn parse(input: ParseStream) -> ParseResult { + Ok(HtmlListOpen { + lt: input.parse()?, + gt: input.parse()?, + }) + } +} + +impl ToTokens for HtmlListOpen { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlListOpen { lt, gt } = self; + tokens.extend(quote! {#lt#gt}); + } +} + +struct HtmlListClose { + lt: Token![<], + div: Token![/], + gt: Token![>], +} + +impl Peek<()> for HtmlListClose { + fn peek(cursor: Cursor) -> Option<()> { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '<').as_option()?; + + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '/').as_option()?; + + let (punct, _) = cursor.punct()?; + (punct.as_char() == '>').as_option() + } +} + +impl Parse for HtmlListClose { + fn parse(input: ParseStream) -> ParseResult { + Ok(HtmlListClose { + lt: input.parse()?, + div: input.parse()?, + gt: input.parse()?, + }) + } +} + +impl ToTokens for HtmlListClose { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlListClose { lt, div, gt } = self; + tokens.extend(quote! {#lt#div#gt}); + } +} diff --git a/crates/macro-impl/src/html_tree/html_node.rs b/crates/macro-impl/src/html_tree/html_node.rs new file mode 100644 index 00000000000..2f4d678689e --- /dev/null +++ b/crates/macro-impl/src/html_tree/html_node.rs @@ -0,0 +1,64 @@ +use crate::Peek; +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned, ToTokens}; +use syn::buffer::Cursor; +use syn::parse::{Parse, ParseStream, Result}; +use syn::spanned::Spanned; +use syn::Lit; + +pub struct HtmlNode(Node); + +impl Parse for HtmlNode { + fn parse(input: ParseStream) -> Result { + let node = if HtmlNode::peek(input.cursor()).is_some() { + let lit: Lit = input.parse()?; + match lit { + Lit::Str(_) | Lit::Char(_) | Lit::Int(_) | Lit::Float(_) | Lit::Bool(_) => {} + _ => return Err(syn::Error::new(lit.span(), "unsupported type")), + } + Node::Literal(lit) + } else { + Node::Raw(input.parse()?) + }; + + Ok(HtmlNode(node)) + } +} + +impl Peek<()> for HtmlNode { + fn peek(cursor: Cursor) -> Option<()> { + cursor.literal().map(|_| ()).or_else(|| { + let (ident, _) = cursor.ident()?; + match ident.to_string().as_str() { + "true" | "false" => Some(()), + _ => None, + } + }) + } +} + +impl ToTokens for HtmlNode { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl ToTokens for Node { + fn to_tokens(&self, tokens: &mut TokenStream) { + let node_token = match &self { + Node::Literal(lit) => quote! { + ::yew::virtual_dom::VNode::from(#lit) + }, + Node::Raw(stream) => quote_spanned! {stream.span()=> + ::yew::virtual_dom::VNode::from({#stream}) + }, + }; + + tokens.extend(node_token); + } +} + +enum Node { + Literal(Lit), + Raw(TokenStream), +} diff --git a/crates/macro-impl/src/html_tree/html_prop.rs b/crates/macro-impl/src/html_tree/html_prop.rs new file mode 100644 index 00000000000..9f8a6499a25 --- /dev/null +++ b/crates/macro-impl/src/html_tree/html_prop.rs @@ -0,0 +1,86 @@ +use crate::Peek; +use boolinator::Boolinator; +use proc_macro::TokenStream; +use proc_macro2::TokenTree; +use syn::buffer::Cursor; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::{Expr, Ident, Token}; + +pub struct HtmlProp { + pub name: Ident, + pub value: Expr, +} + +impl Peek<()> for HtmlProp { + fn peek(cursor: Cursor) -> Option<()> { + let (_, cursor) = cursor.ident()?; + let (punct, _) = cursor.punct()?; + (punct.as_char() == '=').as_option() + } +} + +impl Parse for HtmlProp { + fn parse(input: ParseStream) -> ParseResult { + let name = if let Ok(ty) = input.parse::() { + Ident::new("type", ty.span) + } else { + input.parse::()? + }; + + input.parse::()?; + let value = input.parse::()?; + // backwards compat + let _ = input.parse::(); + Ok(HtmlProp { name, value }) + } +} + +pub struct HtmlPropSuffix { + pub div: Option, + pub gt: Token![>], + pub stream: TokenStream, +} + +impl Parse for HtmlPropSuffix { + fn parse(input: ParseStream) -> ParseResult { + let mut trees: Vec = vec![]; + let mut div: Option = None; + let mut angle_count = 1; + let gt: Option]>; + + loop { + let next = input.parse()?; + if let TokenTree::Punct(punct) = &next { + match punct.as_char() { + '>' => { + angle_count -= 1; + if angle_count == 0 { + gt = Some(syn::token::Gt { + spans: [punct.span()], + }); + break; + } + } + '<' => angle_count += 1, + '/' => { + if angle_count == 1 && input.peek(Token![>]) { + div = Some(syn::token::Div { + spans: [punct.span()], + }); + gt = Some(input.parse()?); + break; + } + } + _ => {} + }; + } + trees.push(next); + } + + let gt: Token![>] = gt.ok_or_else(|| input.error("missing tag close"))?; + let stream: proc_macro2::TokenStream = trees.into_iter().collect(); + let stream = TokenStream::from(stream); + + Ok(HtmlPropSuffix { div, gt, stream }) + } +} diff --git a/crates/macro-impl/src/html_tree/html_tag/mod.rs b/crates/macro-impl/src/html_tree/html_tag/mod.rs new file mode 100644 index 00000000000..742a858df0f --- /dev/null +++ b/crates/macro-impl/src/html_tree/html_tag/mod.rs @@ -0,0 +1,285 @@ +mod tag_attributes; + +use super::HtmlProp as TagAttribute; +use super::HtmlPropSuffix as TagSuffix; +use super::HtmlTree; +use crate::Peek; +use boolinator::Boolinator; +use proc_macro2::Span; +use quote::{quote, quote_spanned, ToTokens}; +use syn::buffer::Cursor; +use syn::parse; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::spanned::Spanned; +use syn::{Ident, Token}; +use tag_attributes::{ClassesForm, TagAttributes}; + +pub struct HtmlTag { + ident: Ident, + attributes: TagAttributes, + children: Vec, +} + +impl Peek<()> for HtmlTag { + fn peek(cursor: Cursor) -> Option<()> { + HtmlTagOpen::peek(cursor) + .or_else(|| HtmlTagClose::peek(cursor)) + .map(|_| ()) + } +} + +impl Parse for HtmlTag { + fn parse(input: ParseStream) -> ParseResult { + if HtmlTagClose::peek(input.cursor()).is_some() { + return match input.parse::() { + Ok(close) => Err(syn::Error::new_spanned( + close, + "this close tag has no corresponding open tag", + )), + Err(err) => Err(err), + }; + } + + let open = input.parse::()?; + if open.div.is_some() { + return Ok(HtmlTag { + ident: open.ident, + attributes: open.attributes, + children: Vec::new(), + }); + } + + if !HtmlTag::verify_end(input.cursor(), &open.ident) { + return Err(syn::Error::new_spanned( + open, + "this open tag has no corresponding close tag", + )); + } + + let mut children: Vec = vec![]; + loop { + if let Some(next_close_ident) = HtmlTagClose::peek(input.cursor()) { + if open.ident.to_string() == next_close_ident.to_string() { + break; + } + } + + children.push(input.parse()?); + } + + input.parse::()?; + + Ok(HtmlTag { + ident: open.ident, + attributes: open.attributes, + children, + }) + } +} + +impl ToTokens for HtmlTag { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlTag { + ident, + attributes, + children, + } = self; + + let name = ident.to_string(); + + let TagAttributes { + classes, + attributes, + kind, + value, + checked, + disabled, + selected, + href, + listeners, + } = &attributes; + + let vtag = Ident::new("__yew_vtag", ident.span()); + let attr_names = attributes.iter().map(|attr| attr.name.to_string()); + let attr_values = attributes.iter().map(|attr| &attr.value); + let set_kind = kind.iter().map(|kind| { + quote_spanned! {kind.span()=> #vtag.set_kind(&(#kind)); } + }); + let set_value = value.iter().map(|value| { + quote_spanned! {value.span()=> #vtag.set_value(&(#value)); } + }); + let add_href = href.iter().map(|href| { + quote_spanned! {href.span()=> + let __yew_href: ::yew::html::Href = (#href).into(); + #vtag.add_attribute("href", &__yew_href); + } + }); + let set_checked = checked.iter().map(|checked| { + quote_spanned! {checked.span()=> #vtag.set_checked(#checked); } + }); + let add_disabled = disabled.iter().map(|disabled| { + quote_spanned! {disabled.span()=> + if #disabled { + #vtag.add_attribute("disabled", &"true"); + } + } + }); + let add_selected = selected.iter().map(|selected| { + quote_spanned! {selected.span()=> + if #selected { + #vtag.add_attribute("selected", &"selected"); + } + } + }); + let set_classes = classes.iter().map(|classes_form| match classes_form { + ClassesForm::Tuple(classes) => quote! { + #vtag.add_classes(vec![#(&(#classes)),*]); + }, + ClassesForm::Single(classes) => quote! { + #vtag.set_classes(&(#classes)); + }, + }); + + tokens.extend(quote! {{ + let mut #vtag = ::yew::virtual_dom::vtag::VTag::new(#name); + #(#set_kind)* + #(#set_value)* + #(#add_href)* + #(#set_checked)* + #(#add_disabled)* + #(#add_selected)* + #(#set_classes)* + #vtag.add_attributes(vec![#((#attr_names.to_owned(), (#attr_values).to_string())),*]); + #vtag.add_listeners(vec![#(::std::boxed::Box::new(#listeners)),*]); + #vtag.add_children(vec![#(#children),*]); + ::yew::virtual_dom::VNode::VTag(#vtag) + }}); + } +} + +impl HtmlTag { + fn verify_end(mut cursor: Cursor, open_ident: &Ident) -> bool { + let mut tag_stack_count = 1; + loop { + if let Some(next_open_ident) = HtmlTagOpen::peek(cursor) { + if open_ident.to_string() == next_open_ident.to_string() { + tag_stack_count += 1; + } + } else if let Some(next_close_ident) = HtmlTagClose::peek(cursor) { + if open_ident.to_string() == next_close_ident.to_string() { + tag_stack_count -= 1; + if tag_stack_count == 0 { + break; + } + } + } + if let Some((_, next)) = cursor.token_tree() { + cursor = next; + } else { + break; + } + } + + tag_stack_count == 0 + } +} + +struct HtmlTagOpen { + lt: Token![<], + ident: Ident, + attributes: TagAttributes, + div: Option, + gt: Token![>], +} + +impl Peek for HtmlTagOpen { + fn peek(cursor: Cursor) -> Option { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '<').as_option()?; + + let (ident, _) = cursor.ident()?; + (ident.to_string().to_lowercase() == ident.to_string()).as_option()?; + + Some(ident) + } +} + +impl Parse for HtmlTagOpen { + fn parse(input: ParseStream) -> ParseResult { + let lt = input.parse::()?; + let ident = input.parse::()?; + let TagSuffix { stream, div, gt } = input.parse()?; + let mut attributes: TagAttributes = parse(stream)?; + + // Don't treat value as special for non input / textarea fields + match ident.to_string().as_str() { + "input" | "textarea" => {} + _ => { + if let Some(value) = attributes.value.take() { + attributes.attributes.push(TagAttribute { + name: Ident::new("value", Span::call_site()), + value, + }); + } + } + } + + Ok(HtmlTagOpen { + lt, + ident, + attributes, + div, + gt, + }) + } +} + +impl ToTokens for HtmlTagOpen { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlTagOpen { lt, gt, .. } = self; + tokens.extend(quote! {#lt#gt}); + } +} + +struct HtmlTagClose { + lt: Token![<], + div: Option, + ident: Ident, + gt: Token![>], +} + +impl Peek for HtmlTagClose { + fn peek(cursor: Cursor) -> Option { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '<').as_option()?; + + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '/').as_option()?; + + let (ident, cursor) = cursor.ident()?; + (ident.to_string().to_lowercase() == ident.to_string()).as_option()?; + + let (punct, _) = cursor.punct()?; + (punct.as_char() == '>').as_option()?; + + Some(ident) + } +} + +impl Parse for HtmlTagClose { + fn parse(input: ParseStream) -> ParseResult { + Ok(HtmlTagClose { + lt: input.parse()?, + div: input.parse()?, + ident: input.parse()?, + gt: input.parse()?, + }) + } +} + +impl ToTokens for HtmlTagClose { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlTagClose { lt, div, ident, gt } = self; + tokens.extend(quote! {#lt#div#ident#gt}); + } +} diff --git a/crates/macro-impl/src/html_tree/html_tag/tag_attributes.rs b/crates/macro-impl/src/html_tree/html_tag/tag_attributes.rs new file mode 100644 index 00000000000..35271ada724 --- /dev/null +++ b/crates/macro-impl/src/html_tree/html_tag/tag_attributes.rs @@ -0,0 +1,219 @@ +use crate::html_tree::HtmlProp as TagAttribute; +use crate::Peek; +use lazy_static::lazy_static; +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; +use std::collections::HashMap; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::{Expr, ExprClosure, ExprTuple, Ident}; + +pub struct TagAttributes { + pub attributes: Vec, + pub listeners: Vec, + pub classes: Option, + pub value: Option, + pub kind: Option, + pub checked: Option, + pub disabled: Option, + pub selected: Option, + pub href: Option, +} + +pub enum ClassesForm { + Tuple(Vec), + Single(Expr), +} + +pub struct TagListener { + name: Ident, + handler: Expr, + event_name: String, +} + +lazy_static! { + static ref LISTENER_MAP: HashMap<&'static str, &'static str> = { + let mut m = HashMap::new(); + m.insert("onclick", "ClickEvent"); + m.insert("ondoubleclick", "DoubleClickEvent"); + m.insert("onkeypress", "KeyPressEvent"); + m.insert("onkeydown", "KeyDownEvent"); + m.insert("onkeyup", "KeyUpEvent"); + m.insert("onmousedown", "MouseDownEvent"); + m.insert("onmousemove", "MouseMoveEvent"); + m.insert("onmouseout", "MouseOutEvent"); + m.insert("onmouseenter", "MouseEnterEvent"); + m.insert("onmouseleave", "MouseLeaveEvent"); + m.insert("onmousewheel", "MouseWheelEvent"); + m.insert("onmouseover", "MouseOverEvent"); + m.insert("onmouseup", "MouseUpEvent"); + m.insert("ongotpointercapture", "GotPointerCaptureEvent"); + m.insert("onlostpointercapture", "LostPointerCaptureEvent"); + m.insert("onpointercancel", "PointerCancelEvent"); + m.insert("onpointerdown", "PointerDownEvent"); + m.insert("onpointerenter", "PointerEnterEvent"); + m.insert("onpointerleave", "PointerLeaveEvent"); + m.insert("onpointermove", "PointerMoveEvent"); + m.insert("onpointerout", "PointerOutEvent"); + m.insert("onpointerover", "PointerOverEvent"); + m.insert("onpointerup", "PointerUpEvent"); + m.insert("onscroll", "ScrollEvent"); + m.insert("onblur", "BlurEvent"); + m.insert("onfocus", "FocusEvent"); + m.insert("onsubmit", "SubmitEvent"); + m.insert("oninput", "InputData"); + m.insert("onchange", "ChangeData"); + m.insert("ondrag", "DragEvent"); + m.insert("ondragstart", "DragStartEvent"); + m.insert("ondragend", "DragEndEvent"); + m.insert("ondragenter", "DragEnterEvent"); + m.insert("ondragleave", "DragLeaveEvent"); + m.insert("ondragover", "DragOverEvent"); + m.insert("ondragexit", "DragExitEvent"); + m.insert("ondrop", "DragDropEvent"); + m.insert("oncontextmenu", "ContextMenuEvent"); + m + }; +} + +impl TagAttributes { + fn drain_listeners(attrs: &mut Vec) -> Vec { + let mut i = 0; + let mut drained = Vec::new(); + while i < attrs.len() { + let name_str = attrs[i].name.to_string(); + if let Some(event_type) = LISTENER_MAP.get(&name_str.as_str()) { + let TagAttribute { name, value } = attrs.remove(i); + drained.push(TagListener { + name, + handler: value, + event_name: event_type.to_owned().to_string(), + }); + } else { + i += 1; + } + } + drained + } + + fn remove_attr(attrs: &mut Vec, name: &str) -> Option { + let mut i = 0; + while i < attrs.len() { + if attrs[i].name.to_string() == name { + return Some(attrs.remove(i).value); + } else { + i += 1; + } + } + None + } + + fn map_classes(class_expr: Expr) -> ClassesForm { + match class_expr { + Expr::Tuple(ExprTuple { elems, .. }) => ClassesForm::Tuple(elems.into_iter().collect()), + expr => ClassesForm::Single(expr), + } + } + + fn map_listener(listener: TagListener) -> ParseResult { + let TagListener { + name, + event_name, + handler, + } = listener; + + match handler { + Expr::Closure(closure) => { + let ExprClosure { + inputs, + body, + or1_token, + or2_token, + .. + } = closure; + + let or_span = quote! {#or1_token#or2_token}; + if inputs.len() != 1 { + return Err(syn::Error::new_spanned( + or_span, + "there must be one closure argument", + )); + } + + let var = match inputs.first().unwrap().into_value() { + syn::FnArg::Inferred(pat) => pat, + _ => return Err(syn::Error::new_spanned(or_span, "invalid closure argument")), + }; + let handler = + Ident::new(&format!("__yew_{}_handler", name.to_string()), name.span()); + let listener = + Ident::new(&format!("__yew_{}_listener", name.to_string()), name.span()); + let segment = syn::PathSegment { + ident: Ident::new(&event_name, name.span()), + arguments: syn::PathArguments::None, + }; + let var_type = quote! { ::yew::events::#segment }; + let wrapper_type = quote! { ::yew::html::#name::Wrapper }; + let listener_stream = quote_spanned! {name.span()=> { + let #handler = move | #var: #var_type | #body; + let #listener = #wrapper_type::from(#handler); + #listener + }}; + + Ok(listener_stream) + } + _ => Err(syn::Error::new_spanned( + &name, + format!("`{}` attribute value should be a closure", name), + )), + } + } +} + +impl Parse for TagAttributes { + fn parse(input: ParseStream) -> ParseResult { + let mut attributes: Vec = Vec::new(); + while TagAttribute::peek(input.cursor()).is_some() { + attributes.push(input.parse::()?); + } + + let mut listeners = Vec::new(); + for listener in TagAttributes::drain_listeners(&mut attributes) { + listeners.push(TagAttributes::map_listener(listener)?); + } + + // Multiple listener attributes are allowed, but no others + attributes.sort_by(|a, b| a.name.to_string().partial_cmp(&b.name.to_string()).unwrap()); + let mut i = 0; + while i + 1 < attributes.len() { + if attributes[i].name.to_string() == attributes[i + 1].name.to_string() { + let name = &attributes[i + 1].name; + return Err(syn::Error::new_spanned( + name, + format!("only one `{}` attribute allowed", name), + )); + } + i += 1; + } + + let classes = + TagAttributes::remove_attr(&mut attributes, "class").map(TagAttributes::map_classes); + let value = TagAttributes::remove_attr(&mut attributes, "value"); + let kind = TagAttributes::remove_attr(&mut attributes, "type"); + let checked = TagAttributes::remove_attr(&mut attributes, "checked"); + let disabled = TagAttributes::remove_attr(&mut attributes, "disabled"); + let selected = TagAttributes::remove_attr(&mut attributes, "selected"); + let href = TagAttributes::remove_attr(&mut attributes, "href"); + + Ok(TagAttributes { + attributes, + classes, + listeners, + value, + kind, + checked, + disabled, + selected, + href, + }) + } +} diff --git a/crates/macro-impl/src/html_tree/mod.rs b/crates/macro-impl/src/html_tree/mod.rs new file mode 100644 index 00000000000..4a64e1b1191 --- /dev/null +++ b/crates/macro-impl/src/html_tree/mod.rs @@ -0,0 +1,119 @@ +pub mod html_block; +pub mod html_component; +pub mod html_iterable; +pub mod html_list; +pub mod html_node; +pub mod html_prop; +pub mod html_tag; + +use crate::Peek; +use html_block::HtmlBlock; +use html_component::HtmlComponent; +use html_iterable::HtmlIterable; +use html_list::HtmlList; +use html_node::HtmlNode; +use html_prop::HtmlProp; +use html_prop::HtmlPropSuffix; +use html_tag::HtmlTag; +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::buffer::Cursor; +use syn::parse::{Parse, ParseStream, Result}; + +pub enum HtmlType { + Block, + Component, + List, + Tag, + Empty, +} + +pub enum HtmlTree { + Block(HtmlBlock), + Component(HtmlComponent), + Iterable(HtmlIterable), + List(HtmlList), + Tag(HtmlTag), + Node(HtmlNode), + Empty, +} + +pub struct HtmlRoot(HtmlTree); +impl Parse for HtmlRoot { + fn parse(input: ParseStream) -> Result { + let html_root = if HtmlTree::peek(input.cursor()).is_some() { + HtmlRoot(input.parse()?) + } else if HtmlIterable::peek(input.cursor()).is_some() { + HtmlRoot(HtmlTree::Iterable(input.parse()?)) + } else { + HtmlRoot(HtmlTree::Node(input.parse()?)) + }; + + if !input.is_empty() { + let stream: TokenStream = input.parse()?; + Err(syn::Error::new_spanned( + stream, + "only one root html element allowed", + )) + } else { + Ok(html_root) + } + } +} + +impl ToTokens for HtmlRoot { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let HtmlRoot(html_tree) = self; + html_tree.to_tokens(tokens); + } +} + +impl Parse for HtmlTree { + fn parse(input: ParseStream) -> Result { + let html_type = HtmlTree::peek(input.cursor()) + .ok_or_else(|| input.error("expected valid html element"))?; + let html_tree = match html_type { + HtmlType::Empty => HtmlTree::Empty, + HtmlType::Component => HtmlTree::Component(input.parse()?), + HtmlType::Tag => HtmlTree::Tag(input.parse()?), + HtmlType::Block => HtmlTree::Block(input.parse()?), + HtmlType::List => HtmlTree::List(input.parse()?), + }; + Ok(html_tree) + } +} + +impl Peek for HtmlTree { + fn peek(cursor: Cursor) -> Option { + if cursor.eof() { + Some(HtmlType::Empty) + } else if HtmlComponent::peek(cursor).is_some() { + Some(HtmlType::Component) + } else if HtmlTag::peek(cursor).is_some() { + Some(HtmlType::Tag) + } else if HtmlBlock::peek(cursor).is_some() { + Some(HtmlType::Block) + } else if HtmlList::peek(cursor).is_some() { + Some(HtmlType::List) + } else { + None + } + } +} + +impl ToTokens for HtmlTree { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let empty_html_el = HtmlList(Vec::new()); + let html_tree_el: &dyn ToTokens = match self { + HtmlTree::Empty => &empty_html_el, + HtmlTree::Component(comp) => comp, + HtmlTree::Tag(tag) => tag, + HtmlTree::List(list) => list, + HtmlTree::Node(node) => node, + HtmlTree::Iterable(iterable) => iterable, + HtmlTree::Block(block) => block, + }; + + html_tree_el.to_tokens(tokens); + } +} diff --git a/crates/macro-impl/src/lib.rs b/crates/macro-impl/src/lib.rs new file mode 100644 index 00000000000..68932115ec5 --- /dev/null +++ b/crates/macro-impl/src/lib.rs @@ -0,0 +1,21 @@ +#![recursion_limit = "128"] +extern crate proc_macro; + +mod html_tree; + +use html_tree::HtmlRoot; +use proc_macro::TokenStream; +use proc_macro_hack::proc_macro_hack; +use quote::quote; +use syn::buffer::Cursor; +use syn::parse_macro_input; + +trait Peek { + fn peek(cursor: Cursor) -> Option; +} + +#[proc_macro_hack] +pub fn html(input: TokenStream) -> TokenStream { + let root = parse_macro_input!(input as HtmlRoot); + TokenStream::from(quote! {#root}) +} diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml new file mode 100644 index 00000000000..94e586892e8 --- /dev/null +++ b/crates/macro/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "yew-macro" +version = "0.7.0" +edition = "2018" +autotests = false + +[[test]] +name = "tests" +path = "tests/cases.rs" + +[dev-dependencies] +trybuild = "1.0" +yew = { path = "../.." } + +[dependencies] +proc-macro-hack = "0.5" +proc-macro-nested = "0.1" +yew-macro-impl = { path = "../macro-impl" } +yew-shared = { path = "../shared" } diff --git a/crates/macro/src/helpers.rs b/crates/macro/src/helpers.rs new file mode 100644 index 00000000000..cb588d7856a --- /dev/null +++ b/crates/macro/src/helpers.rs @@ -0,0 +1,39 @@ +#[macro_export] +macro_rules! test_html_block { + ( |$tc:ident| $($view:tt)* ) => { + test_html! { @ gen $tc { $($view)* } } + }; +} + +#[macro_export] +macro_rules! test_html { + ( |$tc:ident| $($view:tt)* ) => { + test_html! { @ gen $tc { html! { $($view)* } } } + }; + ( @gen $tc:ident $view:block ) => { + mod $tc { + use ::yew_shared::prelude::*; + use super::*; + + struct TestComponent {} + impl Component for TestComponent { + type Message = (); + type Properties = (); + + fn create(_: Self::Properties, _: ComponentLink) -> Self { + TestComponent {} + } + + fn update(&mut self, _: Self::Message) -> ShouldRender { + true + } + } + + impl Renderable for TestComponent { + fn view(&self) -> Html { + $view + } + } + } + }; +} diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs new file mode 100644 index 00000000000..c046202da35 --- /dev/null +++ b/crates/macro/src/lib.rs @@ -0,0 +1,8 @@ +use proc_macro_hack::proc_macro_hack; + +#[macro_use] +pub mod helpers; + +/// Generate html tree +#[proc_macro_hack(support_nested)] +pub use yew_macro_impl::html; diff --git a/crates/macro/tests/cases.rs b/crates/macro/tests/cases.rs new file mode 100644 index 00000000000..b483baa8964 --- /dev/null +++ b/crates/macro/tests/cases.rs @@ -0,0 +1,22 @@ +#[test] +fn tests() { + let t = trybuild::TestCases::new(); + + t.pass("tests/html-block-pass.rs"); + t.compile_fail("tests/html-block-fail.rs"); + + t.pass("tests/html-component-pass.rs"); + t.compile_fail("tests/html-component-fail.rs"); + + t.pass("tests/html-iterable-pass.rs"); + t.compile_fail("tests/html-iterable-fail.rs"); + + t.pass("tests/html-list-pass.rs"); + t.compile_fail("tests/html-list-fail.rs"); + + t.pass("tests/html-node-pass.rs"); + t.compile_fail("tests/html-node-fail.rs"); + + t.pass("tests/html-tag-pass.rs"); + t.compile_fail("tests/html-tag-fail.rs"); +} diff --git a/crates/macro/tests/html-block-fail.rs b/crates/macro/tests/html-block-fail.rs new file mode 100644 index 00000000000..61bf121a763 --- /dev/null +++ b/crates/macro/tests/html-block-fail.rs @@ -0,0 +1,23 @@ +use yew_macro::{html, test_html, test_html_block}; + +test_html! { |t1| + { () } +} + +test_html_block! { |t2| + let not_tree = || (); + + html! { +
{ not_tree() }
+ } +} + +test_html_block! { |t3| + let not_tree = || (); + + html! { + <>{ for (0..3).map(|_| not_tree()) } + } +} + +fn main() {} diff --git a/crates/macro/tests/html-block-fail.stderr b/crates/macro/tests/html-block-fail.stderr new file mode 100644 index 00000000000..6da5c834db3 --- /dev/null +++ b/crates/macro/tests/html-block-fail.stderr @@ -0,0 +1,37 @@ +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> $DIR/html-block-fail.rs:11:16 + | +11 |
{ not_tree() }
+ | ^^^^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required because of the requirements on the impl of `std::string::ToString` for `()` + = note: required because of the requirements on the impl of `std::convert::From<()>` for `yew_shared::virtual_dom::vnode::VNode<_>` + = note: required by `std::convert::From::from` + +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> $DIR/html-block-fail.rs:19:17 + | +19 | <>{ for (0..3).map(|_| not_tree()) } + | ^^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required because of the requirements on the impl of `std::string::ToString` for `()` + = note: required because of the requirements on the impl of `std::convert::From<()>` for `yew_shared::virtual_dom::vnode::VNode<_>` + = note: required because of the requirements on the impl of `std::convert::Into>` for `()` + +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> $DIR/html-block-fail.rs:4:7 + | +4 | { () } + | ^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required because of the requirements on the impl of `std::string::ToString` for `()` + = note: required because of the requirements on the impl of `std::convert::From<()>` for `yew_shared::virtual_dom::vnode::VNode<_>` + = note: required by `std::convert::From::from` + +For more information about this error, try `rustc --explain E0277`. diff --git a/crates/macro/tests/html-block-pass.rs b/crates/macro/tests/html-block-pass.rs new file mode 100644 index 00000000000..afcca1a8634 --- /dev/null +++ b/crates/macro/tests/html-block-pass.rs @@ -0,0 +1,47 @@ +use yew_macro::{html, test_html, test_html_block}; + +test_html! { |t1| <>{ "Hi" } } +test_html! { |t2| <>{ format!("Hello") } } +test_html! { |t3| <>{ String::from("Hello") } } + +test_html_block! { |t10| + let msg = "Hello"; + + html! { +
{ msg }
+ } +} + +test_html_block! { |t11| + let subview = html! { "subview!" }; + + html! { +
{ subview }
+ } +} + +test_html_block! { |t12| + let subview = || html! { "subview!" }; + + html! { +
{ subview() }
+ } +} + +test_html! { |t20| +
    + { for (0..3).map(|num| { html! { {num} }}) } +
+} + +test_html_block! { |t21| + let item = |num| html! {
  • {format!("item {}!", num)}
  • }; + + html! { +
      + { for (0..3).map(item) } +
    + } +} + +fn main() {} diff --git a/crates/macro/tests/html-component-fail.rs b/crates/macro/tests/html-component-fail.rs new file mode 100644 index 00000000000..c13d5012c2d --- /dev/null +++ b/crates/macro/tests/html-component-fail.rs @@ -0,0 +1,92 @@ +#![recursion_limit = "128"] + +use yew_macro::{html, test_html}; +use yew_shared::prelude::*; + +#[derive(Clone, Default, PartialEq)] +pub struct ChildProperties { + pub string: String, + pub int: i32, +} + +pub struct ChildComponent; +impl Component for ChildComponent { + type Message = (); + type Properties = ChildProperties; + + fn create(props: Self::Properties, _: ComponentLink) -> Self { + ChildComponent + } + + fn update(&mut self, _: Self::Message) -> ShouldRender { + unimplemented!() + } +} + +impl Renderable for ChildComponent { + fn view(&self) -> Html { + unimplemented!() + } +} + +test_html! { |t1| + +} + +test_html! { |t2| + +} + +test_html! { |t3| + +} + +test_html! { |t4| + +} + +test_html! { |t5| + +} + +test_html! { |t6| + +} + +test_html! { |t7| + +} + +test_html! { |t8| + +} + +test_html! { |t10| + +} + +test_html! { |t11| + +} + +test_html! { |t12| + +} + +test_html! { |t13| + +} + +test_html! { |t14| + +} + +test_html! { |t15| + +} + +test_html! { |t16| + +} + +fn main() {} diff --git a/crates/macro/tests/html-component-fail.stderr b/crates/macro/tests/html-component-fail.stderr new file mode 100644 index 00000000000..97b3b485146 --- /dev/null +++ b/crates/macro/tests/html-component-fail.stderr @@ -0,0 +1,119 @@ +error: unexpected token + --> $DIR/html-component-fail.rs:61:32 + | +61 | + | ^^ + +error: unexpected end of input, expected identifier + --> $DIR/html-component-fail.rs:57:23 + | +57 | + | ^ + +error: expected identifier + --> $DIR/html-component-fail.rs:53:21 + | +53 | + | ^^^^ + +error: unexpected end of input, expected expression + --> $DIR/html-component-fail.rs:49:29 + | +49 | + | ^ + +error: expected component tag be of form `< .. />` + --> $DIR/html-component-fail.rs:45:5 + | +45 | + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unexpected token + --> $DIR/html-component-fail.rs:41:21 + | +41 | + | ^^^^^ + +error: unexpected end of input, expected identifier + --> $DIR/html-component-fail.rs:37:26 + | +37 | + | ^ + +error: expected component tag be of form `< .. />` + --> $DIR/html-component-fail.rs:33:5 + | +33 | + | ^^^^^^^^^^^^^^^^ + +error[E0425]: cannot find value `blah` in this scope + --> $DIR/html-component-fail.rs:69:26 + | +69 | + | ^^^^ not found in this scope + +error[E0277]: the trait bound `std::string::String: yew_shared::html::Component` is not satisfied + --> $DIR/html-component-fail.rs:65:6 + | +65 | + | ^^^^^^ the trait `yew_shared::html::Component` is not implemented for `std::string::String` + | + = note: required by `yew_shared::virtual_dom::vcomp::VComp::::lazy` + +error[E0277]: the trait bound `std::string::String: yew_shared::html::Renderable` is not satisfied + --> $DIR/html-component-fail.rs:65:6 + | +65 | + | ^^^^^^ the trait `yew_shared::html::Renderable` is not implemented for `std::string::String` + | + = note: required by `yew_shared::virtual_dom::vcomp::VComp::::lazy` + +error[E0609]: no field `unknown` on type `ChildProperties` + --> $DIR/html-component-fail.rs:73:21 + | +73 | + | ^^^^^^^ unknown field + | + = note: available fields are: `string`, `int` + +error[E0308]: mismatched types + --> $DIR/html-component-fail.rs:77:28 + | +77 | + | ^^ expected struct `std::string::String`, found () + | + = note: expected type `std::string::String` + found type `()` + +error[E0308]: mismatched types + --> $DIR/html-component-fail.rs:81:28 + | +81 | + | ^ + | | + | expected struct `std::string::String`, found integer + | help: try using a conversion method: `3.to_string()` + | + = note: expected type `std::string::String` + found type `{integer}` + +error[E0308]: mismatched types + --> $DIR/html-component-fail.rs:85:28 + | +85 | + | ^^^ + | | + | expected struct `std::string::String`, found integer + | help: try using a conversion method: `{3}.to_string()` + | + = note: expected type `std::string::String` + found type `{integer}` + +error[E0308]: mismatched types + --> $DIR/html-component-fail.rs:89:25 + | +89 | + | ^^^^ expected i32, found u32 + +Some errors occurred: E0277, E0308, E0425, E0609. +For more information about an error, try `rustc --explain E0277`. diff --git a/crates/macro/tests/html-component-pass.rs b/crates/macro/tests/html-component-pass.rs new file mode 100644 index 00000000000..9e997e2f365 --- /dev/null +++ b/crates/macro/tests/html-component-pass.rs @@ -0,0 +1,100 @@ +#![recursion_limit = "128"] + +use yew_macro::{html, test_html, test_html_block}; +use yew_shared::prelude::*; + +#[derive(Clone, Default, PartialEq)] +pub struct ChildProperties { + pub string: String, + pub int: i32, + pub vec: Vec, +} + +pub struct ChildComponent { + props: ChildProperties, +} + +impl Component for ChildComponent { + type Message = (); + type Properties = ChildProperties; + + fn create(props: Self::Properties, _: ComponentLink) -> Self { + ChildComponent { props } + } + + fn update(&mut self, _: Self::Message) -> ShouldRender { + true + } +} + +impl Renderable for ChildComponent { + fn view(&self) -> Html { + let ChildProperties { string, .. } = &self.props; + html! { + { string } + } + } +} + +mod scoped { + pub use super::ChildComponent; +} + +test_html! { |t1| + +} + +// backwards compat +test_html! { |t2| + +} + +test_html! { |t3| + <> + + + + + // backwards compat + + + + +} + +test_html_block! { |t4| + let props = ::Properties::default(); + let props2 = ::Properties::default(); + + html! { + <> + + + // backwards compat + + + } +} + +test_html! { |t5| + <> + + + + + + + // backwards compat + + +} + +test_html_block! { |t6| + let name_expr = "child"; + + html! { + + } +} + +fn main() {} diff --git a/crates/macro/tests/html-iterable-fail.rs b/crates/macro/tests/html-iterable-fail.rs new file mode 100644 index 00000000000..a54c9d1ae14 --- /dev/null +++ b/crates/macro/tests/html-iterable-fail.rs @@ -0,0 +1,18 @@ +use yew_macro::{html, test_html, test_html_block}; + +test_html! { |t1| for } +test_html! { |t2| for () } +test_html! { |t3| for {()} } +test_html! { |t4| for Vec::<()>::new().into_iter() } + +test_html_block! { |t10| + let empty = Vec::<()>::new().into_iter(); + html! { for empty } +} + +test_html_block! { |t11| + let empty = Vec::<()>::new(); + html! { for empty.iter() } +} + +fn main() {} diff --git a/crates/macro/tests/html-iterable-fail.stderr b/crates/macro/tests/html-iterable-fail.stderr new file mode 100644 index 00000000000..8310c892b83 --- /dev/null +++ b/crates/macro/tests/html-iterable-fail.stderr @@ -0,0 +1,62 @@ +error: expected expression after `for` + --> $DIR/html-iterable-fail.rs:3:19 + | +3 | test_html! { |t1| for } + | ^^^ + +error[E0277]: `()` is not an iterator + --> $DIR/html-iterable-fail.rs:4:23 + | +4 | test_html! { |t2| for () } + | ^^ `()` is not an iterator + | + = help: the trait `std::iter::Iterator` is not implemented for `()` + = note: required for the cast to the object type `dyn std::iter::Iterator` + +error[E0277]: `()` is not an iterator + --> $DIR/html-iterable-fail.rs:5:23 + | +5 | test_html! { |t3| for {()} } + | ^^^^ `()` is not an iterator + | + = help: the trait `std::iter::Iterator` is not implemented for `()` + = note: required for the cast to the object type `dyn std::iter::Iterator` + +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> $DIR/html-iterable-fail.rs:6:23 + | +6 | test_html! { |t4| for Vec::<()>::new().into_iter() } + | ^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required because of the requirements on the impl of `std::string::ToString` for `()` + = note: required because of the requirements on the impl of `std::convert::From<()>` for `yew_shared::virtual_dom::vnode::VNode<_>` + = note: required because of the requirements on the impl of `std::convert::Into>` for `()` + +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> $DIR/html-iterable-fail.rs:10:17 + | +10 | html! { for empty } + | ^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required because of the requirements on the impl of `std::string::ToString` for `()` + = note: required because of the requirements on the impl of `std::convert::From<()>` for `yew_shared::virtual_dom::vnode::VNode<_>` + = note: required because of the requirements on the impl of `std::convert::Into>` for `()` + +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> $DIR/html-iterable-fail.rs:15:17 + | +15 | html! { for empty.iter() } + | ^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required because of the requirements on the impl of `std::fmt::Display` for `&()` + = note: required because of the requirements on the impl of `std::string::ToString` for `&()` + = note: required because of the requirements on the impl of `std::convert::From<&()>` for `yew_shared::virtual_dom::vnode::VNode<_>` + = note: required because of the requirements on the impl of `std::convert::Into>` for `&()` + +For more information about this error, try `rustc --explain E0277`. diff --git a/crates/macro/tests/html-iterable-pass.rs b/crates/macro/tests/html-iterable-pass.rs new file mode 100644 index 00000000000..b75b0ad7d91 --- /dev/null +++ b/crates/macro/tests/html-iterable-pass.rs @@ -0,0 +1,15 @@ +use std::iter; +use yew_macro::{html, test_html, test_html_block}; + +test_html! { |t1| for iter::empty::>() } +test_html! { |t2| for Vec::>::new().into_iter() } +test_html! { |t3| for (0..3).map(|num| { html! { {num} } }) } +test_html! { |t4| for {iter::empty::>()} } + +test_html_block! { |t5| + let empty: Vec> = Vec::new(); + + html! { for empty.into_iter() } +} + +fn main() {} diff --git a/crates/macro/tests/html-list-fail.rs b/crates/macro/tests/html-list-fail.rs new file mode 100644 index 00000000000..25e4bde927d --- /dev/null +++ b/crates/macro/tests/html-list-fail.rs @@ -0,0 +1,11 @@ +use yew_macro::{html, test_html}; + +test_html! { |t1| <> } +test_html! { |t2| } +test_html! { |t3| <><> } +test_html! { |t4| } +test_html! { |t5| <><> } +test_html! { |t6| <><> } +test_html! { |t7| <>invalid } + +fn main() {} diff --git a/crates/macro/tests/html-list-fail.stderr b/crates/macro/tests/html-list-fail.stderr new file mode 100644 index 00000000000..8fd6e5fda87 --- /dev/null +++ b/crates/macro/tests/html-list-fail.stderr @@ -0,0 +1,41 @@ +error: expected valid html element + --> $DIR/html-list-fail.rs:9:21 + | +9 | test_html! { |t7| <>invalid } + | ^^^^^^^ + +error: only one root html element allowed + --> $DIR/html-list-fail.rs:8:24 + | +8 | test_html! { |t6| <><> } + | ^^^^^ + +error: this open tag has no corresponding close tag + --> $DIR/html-list-fail.rs:7:19 + | +7 | test_html! { |t5| <><> } + | ^^ + +error: this close tag has no corresponding open tag + --> $DIR/html-list-fail.rs:6:19 + | +6 | test_html! { |t4| } + | ^^^ + +error: this open tag has no corresponding close tag + --> $DIR/html-list-fail.rs:5:19 + | +5 | test_html! { |t3| <><> } + | ^^ + +error: this close tag has no corresponding open tag + --> $DIR/html-list-fail.rs:4:19 + | +4 | test_html! { |t2| } + | ^^^ + +error: this open tag has no corresponding close tag + --> $DIR/html-list-fail.rs:3:19 + | +3 | test_html! { |t1| <> } + | ^^ diff --git a/crates/macro/tests/html-list-pass.rs b/crates/macro/tests/html-list-pass.rs new file mode 100644 index 00000000000..4c5f4b4f3db --- /dev/null +++ b/crates/macro/tests/html-list-pass.rs @@ -0,0 +1,12 @@ +use yew_macro::{html, test_html}; + +test_html! { |t0| } +test_html! { |t1| <> } +test_html! { |t2| + <> + <> + <> + +} + +fn main() {} diff --git a/crates/macro/tests/html-node-fail.rs b/crates/macro/tests/html-node-fail.rs new file mode 100644 index 00000000000..4a271e2f6f5 --- /dev/null +++ b/crates/macro/tests/html-node-fail.rs @@ -0,0 +1,24 @@ +use yew_macro::{html, test_html, test_html_block}; + +test_html! { |t1| "valid" "invalid" } +test_html! { |t2| { "valid" "invalid" } } +test_html! { |t3| () } +test_html! { |t4| invalid } + +// unsupported literals +test_html! { |t10| b'a' } +test_html! { |t11| b"str" } +test_html! { |t12| 1111111111111111111111111111111111111111111111111111111111111111111111111111 } +test_html! { |t13| { b'a' } } +test_html! { |t14| { b"str" } } +test_html! { |t15| { 1111111111111111111111111111111111111111111111111111111111111111111111111111 } } + +test_html_block! { |t20| + let not_node = || (); + + html! { + not_node() + } +} + +fn main() {} diff --git a/crates/macro/tests/html-node-fail.stderr b/crates/macro/tests/html-node-fail.stderr new file mode 100644 index 00000000000..193c413327f --- /dev/null +++ b/crates/macro/tests/html-node-fail.stderr @@ -0,0 +1,80 @@ +error: unsupported type + --> $DIR/html-node-fail.rs:14:28 + | +14 | test_html! { |t15| { 1111111111111111111111111111111111111111111111111111111111111111111111111111 } } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unsupported type + --> $DIR/html-node-fail.rs:13:28 + | +13 | test_html! { |t14| { b"str" } } + | ^^^^^^ + +error: unsupported type + --> $DIR/html-node-fail.rs:12:28 + | +12 | test_html! { |t13| { b'a' } } + | ^^^^ + +error: unsupported type + --> $DIR/html-node-fail.rs:11:20 + | +11 | test_html! { |t12| 1111111111111111111111111111111111111111111111111111111111111111111111111111 } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unsupported type + --> $DIR/html-node-fail.rs:10:20 + | +10 | test_html! { |t11| b"str" } + | ^^^^^^ + +error: unsupported type + --> $DIR/html-node-fail.rs:9:20 + | +9 | test_html! { |t10| b'a' } + | ^^^^ + +error: unexpected token + --> $DIR/html-node-fail.rs:4:35 + | +4 | test_html! { |t2| { "valid" "invalid" } } + | ^^^^^^^^^ + +error: only one root html element allowed + --> $DIR/html-node-fail.rs:3:27 + | +3 | test_html! { |t1| "valid" "invalid" } + | ^^^^^^^^^ + +error[E0425]: cannot find value `invalid` in this scope + --> $DIR/html-node-fail.rs:6:19 + | +6 | test_html! { |t4| invalid } + | ^^^^^^^ not found in this scope + +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> $DIR/html-node-fail.rs:20:9 + | +20 | not_node() + | ^^^^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required because of the requirements on the impl of `std::string::ToString` for `()` + = note: required because of the requirements on the impl of `std::convert::From<()>` for `yew_shared::virtual_dom::vnode::VNode<_>` + = note: required by `std::convert::From::from` + +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> $DIR/html-node-fail.rs:5:19 + | +5 | test_html! { |t3| () } + | ^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required because of the requirements on the impl of `std::string::ToString` for `()` + = note: required because of the requirements on the impl of `std::convert::From<()>` for `yew_shared::virtual_dom::vnode::VNode<_>` + = note: required by `std::convert::From::from` + +Some errors occurred: E0277, E0425. +For more information about an error, try `rustc --explain E0277`. diff --git a/crates/macro/tests/html-node-pass.rs b/crates/macro/tests/html-node-pass.rs new file mode 100644 index 00000000000..a379353921b --- /dev/null +++ b/crates/macro/tests/html-node-pass.rs @@ -0,0 +1,24 @@ +use yew_macro::{html, test_html, test_html_block}; + +test_html! { |t1| "" } +test_html! { |t2| 'a' } +test_html! { |t3| "hello" } +test_html! { |t4| 42 } +test_html! { |t5| 1.234 } +test_html! { |t6| true } + +test_html! { |t10| { "" } } +test_html! { |t11| { 'a' } } +test_html! { |t12| { "hello" } } +test_html! { |t13| { 42 } } +test_html! { |t14| { 1.234 } } +test_html! { |t15| { true } } + +test_html! { |t20| format!("Hello") } +test_html! { |t21| String::from("Hello") } +test_html_block! { |t22| + let msg = "Hello"; + html! { msg } +} + +fn main() {} diff --git a/crates/macro/tests/html-tag-fail.rs b/crates/macro/tests/html-tag-fail.rs new file mode 100644 index 00000000000..be88fdb98d3 --- /dev/null +++ b/crates/macro/tests/html-tag-fail.rs @@ -0,0 +1,33 @@ +use yew_macro::{html, test_html}; + +test_html! { |t1|
    } +test_html! { |t2|
    } +test_html! { |t3|
    } +test_html! { |t4|
    } +test_html! { |t5|
    } +test_html! { |t6|
    } +test_html! { |t7|
    } +test_html! { |t8| } +test_html! { |t9|
    Invalid
    } + +test_html! { |t20| } +test_html! { |t21| } +test_html! { |t22| } +test_html! { |t23| } +test_html! { |t24| } +test_html! { |t25|