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

Static attribute lists #1962

Merged
merged 18 commits into from
Jul 31, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
130 changes: 94 additions & 36 deletions packages/yew-macro/src/html_tree/html_element.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
use crate::props::{ClassesForm, ElementProps, Prop};
use crate::stringify::Stringify;
use crate::stringify::{Stringify, Value};
use crate::{non_capitalized_ascii, Peek, PeekValue};
use boolinator::Boolinator;
use proc_macro2::{Delimiter, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{Block, Ident, Token};
use syn::{Block, Expr, Ident, Lit, LitStr, Token};

pub struct HtmlElement {
name: TagName,
Expand Down Expand Up @@ -147,17 +147,35 @@ impl ToTokens for HtmlElement {

let attributes = {
let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| {
let key = label.to_lit_str();
let value = value.optimize_literals();
quote! {
::yew::virtual_dom::PositionalAttr::new(#key, #value)
}
(label.to_lit_str(), value.optimize_literals_tagged())
});
let boolean_attrs = booleans.iter().map(|Prop { label, value, .. }| {
let boolean_attrs = booleans.iter().filter_map(|Prop { label, value, .. }| {
let key = label.to_lit_str();
quote! {
::yew::virtual_dom::PositionalAttr::new_boolean(#key, #value)
}
Some((
key.clone(),
match value {
Expr::Lit(e) => match &e.lit {
Lit::Bool(b) => Value::Static(if b.value {
quote! { #key }
} else {
return None;
}),
_ => Value::Dynamic(quote_spanned! {value.span()=> {
::yew::utils::__ensure_type::<bool>(#value);
#key
}}),
},
expr => Value::Dynamic(quote_spanned! {expr.span()=>
if #expr {
::std::option::Option::Some(
::std::borrow::Cow::<'static, str>::Borrowed(#key)
)
} else {
None
}
}),
},
))
});
let class_attr = classes.as_ref().and_then(|classes| match classes {
ClassesForm::Tuple(classes) => {
Expand All @@ -176,36 +194,78 @@ impl ToTokens for HtmlElement {
};
};

Some(quote! {
{
let mut __yew_classes = ::yew::html::Classes::with_capacity(#n);
#(__yew_classes.push(#classes);)*
Some((
LitStr::new("class", span),
Value::Dynamic(quote! {
{
#deprecation_warning

#deprecation_warning

::yew::virtual_dom::PositionalAttr::new("class", __yew_classes)
}
})
let mut __yew_classes = ::yew::html::Classes::with_capacity(#n);
#(__yew_classes.push(#classes);)*
__yew_classes
}
}),
))
}
ClassesForm::Single(classes) => match classes.try_into_lit() {
Some(lit) => {
if lit.value().is_empty() {
None
} else {
let sr = lit.stringify();
Some(quote! { ::yew::virtual_dom::PositionalAttr::new("class", #sr) })
ClassesForm::Single(classes) => {
match classes.try_into_lit() {
Some(lit) => {
if lit.value().is_empty() {
None
} else {
Some((
LitStr::new("class", lit.span()),
Value::Static(quote! { #lit }),
))
}
}
None => {
Some((
LitStr::new("class", classes.span()),
Value::Dynamic(quote! {
::std::convert::Into::<::yew::html::Classes>::into(#classes)
}),
))
}
}
None => {
Some(quote! { ::yew::virtual_dom::PositionalAttr::new("class", ::std::convert::Into::<::yew::html::Classes>::into(#classes)) })
}
}
});

let attrs = normal_attrs.chain(boolean_attrs).chain(class_attr);
quote! {
::yew::virtual_dom::Attributes::Vec(::std::vec![#(#attrs),*])
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static`
fn try_into_static(src: &[(LitStr, Value)]) -> Option<TokenStream> {
let mut kv = Vec::with_capacity(src.len());
for (k, v) in src.iter() {
let v = match v {
Value::Static(v) => quote! { #v },
Value::Dynamic(_) => return None,
};
kv.push(quote! { [ #k, #v ] });
}

Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
}

let attrs = normal_attrs
.chain(boolean_attrs)
.chain(class_attr)
.collect::<Vec<(LitStr, Value)>>();
try_into_static(&attrs).unwrap_or_else(|| {
let keys = attrs.iter().map(|(k, _)| quote! { #k });
let values = attrs.iter().map(|(_, v)| {
quote_spanned! {v.span()=>
::yew::html::IntoPropValue::<
::std::option::Option::<::yew::virtual_dom::AttrValue>
>
::into_prop_value(#v)
}
});
quote! {
::yew::virtual_dom::Attributes::Dynamic{
keys: &[#(#keys),*],
values: ::std::boxed::Box::new([#(#values),*]),
}
}
})
};

let listeners = if listeners.is_empty() {
Expand Down Expand Up @@ -291,9 +351,7 @@ impl ToTokens for HtmlElement {
let handle_value_attr = props.value.as_ref().map(|prop| {
let v = prop.value.optimize_literals();
quote_spanned! {v.span()=> {
__yew_vtag.__macro_push_attr(
::yew::virtual_dom::PositionalAttr::new("value", #v),
);
__yew_vtag.__macro_push_attr("value", #v);
}}
});

Expand Down
27 changes: 25 additions & 2 deletions packages/yew-macro/src/stringify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@ pub trait Stringify {

/// Optimize literals to `&'static str`, otherwise keep the value as is.
fn optimize_literals(&self) -> TokenStream
where
Self: ToTokens,
{
self.optimize_literals_tagged().to_token_stream()
}

/// Like `optimize_literals` but tags static or dynamic strings with [Value]
fn optimize_literals_tagged(&self) -> Value
where
Self: ToTokens,
{
if let Some(lit) = self.try_into_lit() {
lit.to_token_stream()
Value::Static(lit.to_token_stream())
} else {
self.to_token_stream()
Value::Dynamic(self.to_token_stream())
}
}
}
Expand All @@ -41,6 +49,21 @@ impl<T: Stringify + ?Sized> Stringify for &T {
}
}

/// A stringified value that can be either static (known at compile time) or dynamic (known only at
/// runtime)
pub enum Value {
Static(TokenStream),
Dynamic(TokenStream),
}

impl ToTokens for Value {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(match self {
Value::Static(tt) | Value::Dynamic(tt) => tt.clone(),
});
}
}

impl Stringify for LitStr {
fn try_into_lit(&self) -> Option<LitStr> {
Some(self.clone())
Expand Down
4 changes: 2 additions & 2 deletions packages/yew-macro/tests/classes_macro/classes-fail.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied
<Classes as From<&[T]>>
and 4 others
= note: required because of the requirements on the impl of `Into<Classes>` for `{integer}`
= note: required because of the requirements on the impl of `From<std::vec::Vec<{integer}>>` for `Classes`
= note: required because of the requirements on the impl of `From<Vec<{integer}>>` for `Classes`
= note: 1 redundant requirements hidden
= note: required because of the requirements on the impl of `Into<Classes>` for `std::vec::Vec<{integer}>`
= note: required because of the requirements on the impl of `Into<Classes>` for `Vec<{integer}>`

error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied
--> $DIR/classes-fail.rs:13:14
Expand Down
27 changes: 5 additions & 22 deletions packages/yew-macro/tests/html_macro/element-fail.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,7 @@ error[E0277]: the trait bound `(): IntoPropValue<Option<Cow<'static, str>>>` is
43 | html! { <input type={()} /> };
| ^^ the trait `IntoPropValue<Option<Cow<'static, str>>>` is not implemented for `()`
|
::: $WORKSPACE/packages/yew/src/virtual_dom/mod.rs
|
| pub fn new(key: &'static str, value: impl IntoPropValue<Option<AttrValue>>) -> Self {
| -------------------------------- required by this bound in `PositionalAttr::new`
= note: required by `into_prop_value`

error[E0277]: the trait bound `(): IntoPropValue<Option<Cow<'static, str>>>` is not satisfied
--> $DIR/element-fail.rs:44:27
Expand All @@ -196,53 +193,39 @@ error[E0277]: the trait bound `(): IntoPropValue<Option<Cow<'static, str>>>` is
45 | html! { <a href={()} /> };
| ^^ the trait `IntoPropValue<Option<Cow<'static, str>>>` is not implemented for `()`
|
::: $WORKSPACE/packages/yew/src/virtual_dom/mod.rs
|
| pub fn new(key: &'static str, value: impl IntoPropValue<Option<AttrValue>>) -> Self {
| -------------------------------- required by this bound in `PositionalAttr::new`
= note: required by `into_prop_value`

error[E0277]: the trait bound `NotToString: IntoPropValue<Option<Cow<'static, str>>>` is not satisfied
--> $DIR/element-fail.rs:46:28
|
46 | html! { <input string={NotToString} /> };
| ^^^^^^^^^^^ the trait `IntoPropValue<Option<Cow<'static, str>>>` is not implemented for `NotToString`
|
::: $WORKSPACE/packages/yew/src/virtual_dom/mod.rs
|
| pub fn new(key: &'static str, value: impl IntoPropValue<Option<AttrValue>>) -> Self {
| -------------------------------- required by this bound in `PositionalAttr::new`
= note: required by `into_prop_value`

error[E0277]: the trait bound `Option<NotToString>: IntoPropValue<Option<Cow<'static, str>>>` is not satisfied
--> $DIR/element-fail.rs:47:23
|
47 | html! { <a media={Some(NotToString)} /> };
| ^^^^^^^^^^^^^^^^^ the trait `IntoPropValue<Option<Cow<'static, str>>>` is not implemented for `Option<NotToString>`
|
::: $WORKSPACE/packages/yew/src/virtual_dom/mod.rs
|
| pub fn new(key: &'static str, value: impl IntoPropValue<Option<AttrValue>>) -> Self {
| -------------------------------- required by this bound in `PositionalAttr::new`
|
= help: the following implementations were found:
<Option<&'static str> as IntoPropValue<Option<Cow<'static, str>>>>
<Option<&'static str> as IntoPropValue<Option<String>>>
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
= note: required by `into_prop_value`

error[E0277]: the trait bound `Option<{integer}>: IntoPropValue<Option<Cow<'static, str>>>` is not satisfied
--> $DIR/element-fail.rs:48:22
|
48 | html! { <a href={Some(5)} /> };
| ^^^^^^^ the trait `IntoPropValue<Option<Cow<'static, str>>>` is not implemented for `Option<{integer}>`
|
::: $WORKSPACE/packages/yew/src/virtual_dom/mod.rs
|
| pub fn new(key: &'static str, value: impl IntoPropValue<Option<AttrValue>>) -> Self {
| -------------------------------- required by this bound in `PositionalAttr::new`
|
= help: the following implementations were found:
<Option<&'static str> as IntoPropValue<Option<Cow<'static, str>>>>
<Option<&'static str> as IntoPropValue<Option<String>>>
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
= note: required by `into_prop_value`

error[E0277]: the trait bound `{integer}: IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not satisfied
--> $DIR/element-fail.rs:51:28
Expand Down
4 changes: 2 additions & 2 deletions packages/yew-macro/tests/props_macro/resolve-prop-fail.stderr
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
error[E0277]: the trait bound `std::vec::Vec<_>: yew::Properties` is not satisfied
error[E0277]: the trait bound `Vec<_>: yew::Properties` is not satisfied
--> $DIR/resolve-prop-fail.rs:38:17
|
38 | yew::props!(Vec<_> {});
| ^^^ the trait `yew::Properties` is not implemented for `std::vec::Vec<_>`
| ^^^ the trait `yew::Properties` is not implemented for `Vec<_>`
|
= note: required by `builder`

Expand Down
8 changes: 7 additions & 1 deletion packages/yew/src/html/classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::virtual_dom::AttrValue;
use indexmap::IndexSet;
use std::{
borrow::{Borrow, Cow},
hint::unreachable_unchecked,
iter::FromIterator,
};

Expand Down Expand Up @@ -65,16 +66,21 @@ impl Classes {
}

impl IntoPropValue<AttrValue> for Classes {
#[inline]
fn into_prop_value(mut self) -> AttrValue {
if self.set.len() == 1 {
self.set.pop().unwrap()
match self.set.pop() {
Some(attr) => attr,
None => unsafe { unreachable_unchecked() },
bakape marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
Cow::Owned(self.to_string())
}
}
}

impl IntoPropValue<Option<AttrValue>> for Classes {
#[inline]
fn into_prop_value(self) -> Option<AttrValue> {
if self.is_empty() {
None
Expand Down
6 changes: 6 additions & 0 deletions packages/yew/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ impl<IN, OUT> IntoIterator for NodeSeq<IN, OUT> {
}
}

/// Hack to force type mismatch compile errors in yew-macro.
//
// TODO: replace with `compile_error!`, when `type_name_of_val` is stabilised.
bakape marked this conversation as resolved.
Show resolved Hide resolved
#[doc(hidden)]
pub fn __ensure_type<T>(_: T) {}

/// Print the [web_sys::Node]'s contents as a string for debugging purposes
pub fn print_node(n: &web_sys::Node) -> String {
use wasm_bindgen::JsCast;
Expand Down
Loading