Skip to content

Commit

Permalink
Static attribute lists (#1962)
Browse files Browse the repository at this point in the history
* yew-macro: optimize VTag construction in html! macro

* yew/vtag: decrease VTag memory footpting and construction args

* yew,yew-macro: optimize VTag contruction, memory footprint and diffing

* yew/vlist: revert to VTag boxing

* yew-macro: add clippy allow for nightly rust

* yew-macro:  fix allow namespace

* *: bump MSRV to 1.49.0

* yew/attributes: static attribute keys and values

* yew/attributes: use boxed slices and inline dynamic class construction

* Update packages/yew/src/virtual_dom/vtag.rs

Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>

* yew/vnode: revert mismerge

* yew/classes: add safety explanation comment

* Update packages/yew/src/utils/mod.rs

Co-authored-by: mc1098 <m.cripps1@uni.brighton.ac.uk>

Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>
Co-authored-by: mc1098 <m.cripps1@uni.brighton.ac.uk>
  • Loading branch information
3 people authored Jul 31, 2021
1 parent c9deba0 commit 8fbb1a2
Show file tree
Hide file tree
Showing 9 changed files with 524 additions and 367 deletions.
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
9 changes: 8 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,22 @@ 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,
// SAFETY: the collection is checked to be non-empty above
None => unsafe { unreachable_unchecked() },
}
} 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 (https://github.com/rust-lang/rust/issues/66359).
#[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

0 comments on commit 8fbb1a2

Please sign in to comment.