Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dynamic tags #1266

Merged
merged 10 commits into from
May 28, 2020
Merged
2 changes: 1 addition & 1 deletion yew-macro/src/html_tree/html_dashed_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::Token;

#[derive(PartialEq)]
#[derive(Clone, PartialEq)]
jstarry marked this conversation as resolved.
Show resolved Hide resolved
pub struct HtmlDashedName {
pub name: Ident,
pub extended: Vec<(Token![-], Ident)>,
Expand Down
215 changes: 173 additions & 42 deletions yew-macro/src/html_tree/html_tag/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
mod tag_attributes;

use super::HtmlDashedName as TagName;
use super::HtmlDashedName;
use super::HtmlProp as TagAttribute;
use super::HtmlPropSuffix as TagSuffix;
use super::HtmlTree;
use crate::{non_capitalized_ascii, Peek, PeekValue};
use boolinator::Boolinator;
use proc_macro2::Span;
use proc_macro2::{Delimiter, 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 syn::{Block, Ident, Token};
use tag_attributes::{ClassesForm, TagAttributes};

pub struct HtmlTag {
Expand Down Expand Up @@ -51,16 +51,19 @@ impl Parse for HtmlTag {
});
}

// Void elements should not have children.
// See https://html.spec.whatwg.org/multipage/syntax.html#void-elements
match open.tag_name.to_string().as_str() {
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "meta"
| "param" | "source" | "track" | "wbr" => {
return Err(syn::Error::new_spanned(&open, format!("the tag `<{}>` is a void element and cannot have children (hint: rewrite this as `<{0}/>`)", open.tag_name)));
if let TagName::Lit(name) = &open.tag_name {
siku2 marked this conversation as resolved.
Show resolved Hide resolved
// Void elements should not have children.
// See https://html.spec.whatwg.org/multipage/syntax.html#void-elements
match name.to_string().as_str() {
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
| "meta" | "param" | "source" | "track" | "wbr" => {
return Err(syn::Error::new_spanned(&open, format!("the tag `<{}>` is a void element and cannot have children (hint: rewrite this as `<{0}/>`)", name)));
}
_ => {}
}
_ => {}
}

let open_key = open.tag_name.get_key();
let mut children: Vec<HtmlTree> = vec![];
loop {
if input.is_empty() {
Expand All @@ -69,8 +72,8 @@ impl Parse for HtmlTag {
"this opening tag has no corresponding closing tag",
));
}
if let Some(next_close_tag_name) = HtmlTagClose::peek(input.cursor()) {
if open.tag_name == next_close_tag_name {
if let Some(close_key) = HtmlTagClose::peek(input.cursor()) {
if open_key == close_key {
break;
}
}
Expand All @@ -97,7 +100,19 @@ impl ToTokens for HtmlTag {
children,
} = self;

let name = tag_name.to_string();
let name = match &tag_name {
TagName::Lit(name) => {
let name_str = name.to_string();
quote! {#name_str}
}
TagName::Expr(name) => {
let expr = &name.expr;
// this way we get a nice error message (with the correct span) when the expression doesn't return a valid value
quote_spanned! {expr.span()=>
::std::borrow::Cow::<'static, str>::from(#expr)
}
}
};

let TagAttributes {
classes,
Expand Down Expand Up @@ -195,6 +210,95 @@ impl ToTokens for HtmlTag {
}
}

struct DynamicName {
at: Token![@],
expr: Option<Block>,
}

impl Peek<'_, ()> for DynamicName {
fn peek(cursor: Cursor) -> Option<((), Cursor)> {
let (punct, cursor) = cursor.punct()?;
(punct.as_char() == '@').as_option()?;

// move cursor past block if there is one
let cursor = cursor
.group(Delimiter::Brace)
.map(|(_, _, cursor)| cursor)
.unwrap_or(cursor);

Some(((), cursor))
}
}

impl Parse for DynamicName {
fn parse(input: ParseStream) -> ParseResult<Self> {
let at = input.parse()?;
let expr = if input.cursor().group(Delimiter::Brace).is_some() {
Some(input.parse()?)
} else {
None
siku2 marked this conversation as resolved.
Show resolved Hide resolved
};

Ok(Self { at, expr })
}
}

impl ToTokens for DynamicName {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self { at, expr } = self;
tokens.extend(quote! {#at#expr});
}
}

#[derive(PartialEq)]
enum TagKey {
jstarry marked this conversation as resolved.
Show resolved Hide resolved
Lit(HtmlDashedName),
Expr,
}

enum TagName {
Lit(HtmlDashedName),
Expr(DynamicName),
}

impl TagName {
fn get_key(&self) -> TagKey {
match self {
TagName::Lit(name) => TagKey::Lit(name.clone()),
TagName::Expr(_) => TagKey::Expr,
}
}
}

impl Peek<'_, TagKey> for TagName {
fn peek(cursor: Cursor) -> Option<(TagKey, Cursor)> {
if let Some((_, cursor)) = DynamicName::peek(cursor) {
Some((TagKey::Expr, cursor))
} else {
HtmlDashedName::peek(cursor).map(|(name, cursor)| (TagKey::Lit(name), cursor))
}
}
}

impl Parse for TagName {
fn parse(input: ParseStream) -> ParseResult<Self> {
if DynamicName::peek(input.cursor()).is_some() {
DynamicName::parse(input).map(Self::Expr)
} else {
HtmlDashedName::parse(input).map(Self::Lit)
}
}
}

impl ToTokens for TagName {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
TagName::Lit(name) => name.to_tokens(tokens),
TagName::Expr(name) => name.to_tokens(tokens),
}
}
}

struct HtmlTagOpen {
lt: Token![<],
tag_name: TagName,
Expand All @@ -203,23 +307,22 @@ struct HtmlTagOpen {
gt: Token![>],
}

impl PeekValue<TagName> for HtmlTagOpen {
fn peek(cursor: Cursor) -> Option<TagName> {
impl PeekValue<TagKey> for HtmlTagOpen {
fn peek(cursor: Cursor) -> Option<TagKey> {
let (punct, cursor) = cursor.punct()?;
(punct.as_char() == '<').as_option()?;

let (name, cursor) = TagName::peek(cursor)?;
if name.to_string() == "key" {
let (punct, _) = cursor.punct()?;
if punct.as_char() == '=' {
None
let (tag_key, cursor) = TagName::peek(cursor)?;
if let TagKey::Lit(name) = &tag_key {
if name.to_string() == "key" {
let (punct, _) = cursor.punct()?;
(punct.as_char() != '=').as_option()?;
siku2 marked this conversation as resolved.
Show resolved Hide resolved
} else {
Some(name)
non_capitalized_ascii(&name.to_string()).as_option()?;
}
} else {
non_capitalized_ascii(&name.to_string()).as_option()?;
Some(name)
}

Some(tag_key)
}
}

Expand All @@ -230,15 +333,27 @@ impl Parse for HtmlTagOpen {
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 tag_name.to_string().as_str() {
"input" | "textarea" => {}
_ => {
if let Some(value) = attributes.value.take() {
attributes.attributes.push(TagAttribute {
label: TagName::new(Ident::new("value", Span::call_site())),
value,
});
match &tag_name {
TagName::Lit(name) => {
// Don't treat value as special for non input / textarea fields
match name.to_string().as_str() {
siku2 marked this conversation as resolved.
Show resolved Hide resolved
"input" | "textarea" => {}
_ => {
if let Some(value) = attributes.value.take() {
attributes.attributes.push(TagAttribute {
label: HtmlDashedName::new(Ident::new("value", Span::call_site())),
value,
});
}
}
}
}
TagName::Expr(name) => {
if name.expr.is_none() {
return Err(syn::Error::new_spanned(
tag_name,
"this dynamic tag is missing an expression block defining its value",
));
}
}
}
Expand Down Expand Up @@ -267,31 +382,47 @@ struct HtmlTagClose {
gt: Token![>],
}

impl PeekValue<TagName> for HtmlTagClose {
fn peek(cursor: Cursor) -> Option<TagName> {
impl PeekValue<TagKey> for HtmlTagClose {
fn peek(cursor: Cursor) -> Option<TagKey> {
let (punct, cursor) = cursor.punct()?;
(punct.as_char() == '<').as_option()?;

let (punct, cursor) = cursor.punct()?;
(punct.as_char() == '/').as_option()?;

let (name, cursor) = TagName::peek(cursor)?;
non_capitalized_ascii(&name.to_string()).as_option()?;
let (tag_key, cursor) = TagName::peek(cursor)?;
if let TagKey::Lit(name) = &tag_key {
non_capitalized_ascii(&name.to_string()).as_option()?;
}

let (punct, _) = cursor.punct()?;
(punct.as_char() == '>').as_option()?;

Some(name)
Some(tag_key)
}
}

impl Parse for HtmlTagClose {
fn parse(input: ParseStream) -> ParseResult<Self> {
let lt = input.parse()?;
let div = input.parse()?;
let tag_name = input.parse()?;
let gt = input.parse()?;

if let TagName::Expr(name) = &tag_name {
if let Some(expr) = &name.expr {
return Err(syn::Error::new_spanned(
expr,
"dynamic closing tags must not have a body",
siku2 marked this conversation as resolved.
Show resolved Hide resolved
));
}
}

Ok(HtmlTagClose {
lt: input.parse()?,
div: input.parse()?,
tag_name: input.parse()?,
gt: input.parse()?,
lt,
div,
tag_name,
gt,
})
}
}
Expand Down
5 changes: 5 additions & 0 deletions yew-macro/tests/macro/html-tag-fail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ fn compile_fail() {
html! { <input ref=() ref=() /> };

html! { <input type="text"></input> };

html! { <@></@> };
html! { <@{"test"}></@{"test"}> };
html! { <@{55}></@> };
html! { <@/> };
}

fn main() {}
39 changes: 39 additions & 0 deletions yew-macro/tests/macro/html-tag-fail.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,30 @@ error: the tag `<input>` is a void element and cannot have children (hint: rewri
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: this dynamic tag is missing an expression block defining its value
--> $DIR/html-tag-fail.rs:42:14
|
42 | html! { <@></@> };
| ^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: dynamic closing tags must not have a body
--> $DIR/html-tag-fail.rs:43:27
|
43 | html! { <@{"test"}></@{"test"}> };
| ^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: this dynamic tag is missing an expression block defining its value
--> $DIR/html-tag-fail.rs:45:14
|
45 | html! { <@/> };
| ^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0308]: mismatched types
--> $DIR/html-tag-fail.rs:25:28
|
Expand Down Expand Up @@ -253,3 +277,18 @@ error[E0308]: mismatched types
| ^^ expected struct `yew::html::NodeRef`, found `()`
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0277]: the trait bound `std::borrow::Cow<'static, str>: std::convert::From<{integer}>` is not satisfied
--> $DIR/html-tag-fail.rs:44:15
|
44 | html! { <@{55}></@> };
| ^^^^ the trait `std::convert::From<{integer}>` is not implemented for `std::borrow::Cow<'static, str>`
|
= help: the following implementations were found:
<std::borrow::Cow<'a, [T]> as std::convert::From<&'a [T]>>
<std::borrow::Cow<'a, [T]> as std::convert::From<&'a std::vec::Vec<T>>>
<std::borrow::Cow<'a, [T]> as std::convert::From<std::vec::Vec<T>>>
<std::borrow::Cow<'a, std::ffi::CStr> as std::convert::From<&'a std::ffi::CStr>>
and 11 others
= note: required by `std::convert::From::from`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
Loading