Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 103 additions & 26 deletions packages/yew-macro/src/html_tree/html_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use quote::{quote, quote_spanned, ToTokens};
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{Expr, Ident, Lit, LitStr, Token};
use syn::{Expr, ExprLit, Ident, Lit, LitStr, Token};

use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
use crate::props::{ElementProps, Prop, PropDirective};
use crate::props::{ElementProps, Prop, PropDirective, PropLabel};
use crate::stringify::{Stringify, Value};
use crate::{is_ide_completion, non_capitalized_ascii, Peek, PeekValue};

Expand Down Expand Up @@ -138,6 +138,30 @@ impl ToTokens for HtmlElement {
// other attributes

let attributes = {
#[derive(Clone)]
enum Key {
Static(LitStr),
Dynamic(Expr),
}

impl From<&PropLabel> for Key {
fn from(value: &PropLabel) -> Self {
match value {
PropLabel::Static(dashed_name) => Self::Static(dashed_name.to_lit_str()),
PropLabel::Dynamic(expr) => Self::Dynamic(expr.clone()),
}
}
}

impl ToTokens for Key {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(match self {
Key::Static(dashed_name) => quote! { #dashed_name },
Key::Dynamic(expr) => quote! { #expr },
});
Comment on lines +158 to +161
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Suggested change
tokens.extend(match self {
Key::Static(dashed_name) => quote! { #dashed_name },
Key::Dynamic(expr) => quote! { #expr },
});
match self {
Key::Static(dashed_name) => dashed_name.to_tokens(tokens),
Key::Dynamic(expr) => expr.to_tokens(tokens),
}

}
}

let normal_attrs = attributes.iter().map(
|Prop {
label,
Expand All @@ -146,7 +170,7 @@ impl ToTokens for HtmlElement {
..
}| {
(
label.to_lit_str(),
Key::from(label),
value.optimize_literals_tagged(),
*directive,
)
Expand All @@ -159,26 +183,30 @@ impl ToTokens for HtmlElement {
directive,
..
}| {
let key = label.to_lit_str();
let key = Key::from(label);
let lit = match &key {
Key::Static(lit) => lit,
Key::Dynamic(_) => unreachable!(),
};
Some((
key.clone(),
match value {
Expr::Lit(e) => match &e.lit {
Lit::Bool(b) => Value::Static(if b.value {
quote! { #key }
quote! { #lit }
} else {
return None;
}),
_ => Value::Dynamic(quote_spanned! {value.span()=> {
::yew::utils::__ensure_type::<::std::primitive::bool>(#value);
#key
#lit
}}),
},
expr => Value::Dynamic(
quote_spanned! {expr.span().resolved_at(Span::call_site())=>
if #expr {
::std::option::Option::Some(
::yew::virtual_dom::AttrValue::Static(#key)
::yew::virtual_dom::AttrValue::Static(#lit)
)
} else {
::std::option::Option::None
Expand All @@ -200,7 +228,7 @@ impl ToTokens for HtmlElement {
None
} else {
Some((
LitStr::new("class", lit.span()),
Key::Static(LitStr::new("class", lit.span())),
Value::Static(quote! { #lit }),
None,
))
Expand All @@ -209,7 +237,7 @@ impl ToTokens for HtmlElement {
None => {
let expr = &classes.value;
Some((
LitStr::new("class", classes.label.span()),
Key::Static(LitStr::new("class", classes.label.span())),
Value::Dynamic(quote! {
::std::convert::Into::<::yew::html::Classes>::into(#expr)
}),
Expand All @@ -219,15 +247,13 @@ impl ToTokens for HtmlElement {
});

/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static`
fn try_into_static(
src: &[(LitStr, Value, Option<PropDirective>)],
) -> Option<TokenStream> {
if src
.iter()
.any(|(_, _, d)| matches!(d, Some(PropDirective::ApplyAsProperty(_))))
{
fn try_into_static(src: &[(Key, Value, Option<PropDirective>)]) -> Option<TokenStream> {
if src.iter().any(|(k, _, d)| {
matches!(k, Key::Dynamic(_))
|| matches!(d, Some(PropDirective::ApplyAsProperty(_)))
}) {
// don't try to make a static attribute list if there are any properties to
// assign
// assign or any labels are dynamic
return None;
}
let mut kv = Vec::with_capacity(src.len());
Expand All @@ -252,13 +278,24 @@ impl ToTokens for HtmlElement {
Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
}

let attrs = normal_attrs
.chain(boolean_attrs)
.chain(class_attr)
.collect::<Vec<(LitStr, Value, Option<PropDirective>)>>();
try_into_static(&attrs).unwrap_or_else(|| {
let keys = attrs.iter().map(|(k, ..)| quote! { #k });
let values = attrs.iter().map(|(_, v, directive)| {
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Dynamic`
fn try_into_dynamic(
src: &[(Key, Value, Option<PropDirective>)],
) -> Option<TokenStream> {
if src.iter().any(|(k, ..)| {
!matches!(
k,
Key::Dynamic(Expr::Lit(ExprLit {
lit: Lit::Str(_),
..
})) | Key::Static(_)
)
}) {
// use IndexMap if there are any dynamic-expr labels
return None;
}
let keys = src.iter().map(|(k, ..)| quote! { #k });
let values = src.iter().map(|(_, v, directive)| {
let value = match directive {
Some(PropDirective::ApplyAsProperty(token)) => {
quote_spanned!(token.span()=> ::std::option::Option::Some(
Expand All @@ -276,11 +313,50 @@ impl ToTokens for HtmlElement {
};
quote! { #value }
});
quote! {
Some(quote! {
::yew::virtual_dom::Attributes::Dynamic{
keys: &[#(#keys),*],
values: ::std::boxed::Box::new([#(#values),*]),
}
})
}

let attrs = normal_attrs
.chain(boolean_attrs)
.chain(class_attr)
.collect::<Vec<(Key, Value, Option<PropDirective>)>>();
try_into_static(&attrs).or_else(|| try_into_dynamic(&attrs)).unwrap_or_else(|| {
let results = attrs.iter()
.map(|(k, v, directive)| {
let value = match directive {
Some(PropDirective::ApplyAsProperty(token)) => {
quote_spanned!(token.span()=> ::std::option::Option::Some(
::yew::virtual_dom::AttributeOrProperty::Property(
::std::convert::Into::into(#v)
))
)
}
None => {
let value = wrap_attr_value(v);
quote! {
::std::option::Option::map(#value, ::yew::virtual_dom::AttributeOrProperty::Attribute)
}
},
};
quote! { (::std::convert::Into::into(#k), #value) }
});
quote! {
::yew::virtual_dom::Attributes::IndexMap(
::std::rc::Rc::new(
::std::iter::Iterator::collect(
::std::iter::Iterator::filter_map(
::std::iter::IntoIterator::into_iter([#(#results),*]),
// FIXME verify if i understood it correctly
|(k, v)| v.map(|v| (k, v))
)
)
)
)
}
})
};
Expand All @@ -289,7 +365,8 @@ impl ToTokens for HtmlElement {
quote! { ::yew::virtual_dom::listeners::Listeners::None }
} else {
let listeners_it = listeners.iter().map(|Prop { label, value, .. }| {
let name = &label.name;
// TODO: consider making a `ListenerProp` that has dashed name's name and value
let name = &<&HtmlDashedName>::try_from(label).unwrap().name;
quote! {
::yew::html::#name::Wrapper::__macro_new(#value)
}
Expand Down
13 changes: 8 additions & 5 deletions packages/yew-macro/src/html_tree/html_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,14 @@ impl Parse for HtmlListProps {
return Err(input.error("only a single `key` prop is allowed on a fragment"));
}

if prop.label.to_ascii_lowercase_string() != "key" {
return Err(syn::Error::new_spanned(
prop.label,
"fragments only accept the `key` prop",
));
match String::try_from(&prop.label) {
Ok(label) if label.eq_ignore_ascii_case("key") => {}
_ => {
return Err(syn::Error::new_spanned(
prop.label,
"fragments only accept the `key` prop",
))
}
}

Some(prop.value)
Expand Down
10 changes: 6 additions & 4 deletions packages/yew-macro/src/html_tree/lint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ where
///
/// Attribute names are lowercased before being compared (so pass "href" for `name` and not "HREF").
fn get_attribute<'a>(props: &'a ElementProps, name: &str) -> Option<&'a Prop> {
props
.attributes
.iter()
.find(|item| item.label.eq_ignore_ascii_case(name))
props.attributes.iter().find(|item| {
matches!(
String::try_from(&item.label),
Ok(label) if label.eq_ignore_ascii_case(name)
)
})
}

/// Lints to check if anchor (`<a>`) tags have valid `href` attributes defined.
Expand Down
17 changes: 7 additions & 10 deletions packages/yew-macro/src/props/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use syn::spanned::Spanned;
use syn::token::DotDot;
use syn::Expr;

use super::{Prop, Props, SpecialProps, CHILDREN_LABEL};
use super::{Prop, PropLabel, Props, SpecialProps, CHILDREN_LABEL};

struct BaseExpr {
pub dot_dot: DotDot,
Expand Down Expand Up @@ -190,15 +190,12 @@ impl TryFrom<Props> for ComponentProps {

fn validate(props: Props) -> Result<Props, syn::Error> {
props.check_no_duplicates()?;
props.check_all(|prop| {
if !prop.label.extended.is_empty() {
Err(syn::Error::new_spanned(
&prop.label,
"expected a valid Rust identifier",
))
} else {
Ok(())
}
props.check_all(|prop| match &prop.label {
PropLabel::Static(dashed_name) if dashed_name.extended.is_empty() => Ok(()),
_ => Err(syn::Error::new_spanned(
&prop.label,
"components expect valid Rust identifiers for their property names",
)),
})?;

Ok(props)
Expand Down
12 changes: 8 additions & 4 deletions packages/yew-macro/src/props/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ impl Parse for ElementProps {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut props = input.parse::<Props>()?;

let listeners =
props.drain_filter(|prop| LISTENER_SET.contains(prop.label.to_string().as_str()));
let listeners = props.drain_filter(|prop| {
matches!(String::try_from(&prop.label),
Ok(prop) if LISTENER_SET.contains(prop.as_str()))
});

// Multiple listener attributes are allowed, but no others
props.check_no_duplicates()?;

let booleans =
props.drain_filter(|prop| BOOLEAN_SET.contains(prop.label.to_string().as_str()));
let booleans = props.drain_filter(|prop| {
matches!(String::try_from(&prop.label),
Ok(prop) if BOOLEAN_SET.contains(prop.as_str()))
});

let classes = props.pop("class");
let value = props.pop("value");
Expand Down
Loading