Skip to content

Commit 8fbb1a2

Browse files
bakaperanilemc1098
authored
Static attribute lists (#1962)
* 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 <[email protected]> * yew/vnode: revert mismerge * yew/classes: add safety explanation comment * Update packages/yew/src/utils/mod.rs Co-authored-by: mc1098 <[email protected]> Co-authored-by: Muhammad Hamza <[email protected]> Co-authored-by: mc1098 <[email protected]>
1 parent c9deba0 commit 8fbb1a2

File tree

9 files changed

+524
-367
lines changed

9 files changed

+524
-367
lines changed

packages/yew-macro/src/html_tree/html_element.rs

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
22
use crate::props::{ClassesForm, ElementProps, Prop};
3-
use crate::stringify::Stringify;
3+
use crate::stringify::{Stringify, Value};
44
use crate::{non_capitalized_ascii, Peek, PeekValue};
55
use boolinator::Boolinator;
66
use proc_macro2::{Delimiter, TokenStream};
77
use quote::{quote, quote_spanned, ToTokens};
88
use syn::buffer::Cursor;
99
use syn::parse::{Parse, ParseStream};
1010
use syn::spanned::Spanned;
11-
use syn::{Block, Ident, Token};
11+
use syn::{Block, Expr, Ident, Lit, LitStr, Token};
1212

1313
pub struct HtmlElement {
1414
name: TagName,
@@ -147,17 +147,35 @@ impl ToTokens for HtmlElement {
147147

148148
let attributes = {
149149
let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| {
150-
let key = label.to_lit_str();
151-
let value = value.optimize_literals();
152-
quote! {
153-
::yew::virtual_dom::PositionalAttr::new(#key, #value)
154-
}
150+
(label.to_lit_str(), value.optimize_literals_tagged())
155151
});
156-
let boolean_attrs = booleans.iter().map(|Prop { label, value, .. }| {
152+
let boolean_attrs = booleans.iter().filter_map(|Prop { label, value, .. }| {
157153
let key = label.to_lit_str();
158-
quote! {
159-
::yew::virtual_dom::PositionalAttr::new_boolean(#key, #value)
160-
}
154+
Some((
155+
key.clone(),
156+
match value {
157+
Expr::Lit(e) => match &e.lit {
158+
Lit::Bool(b) => Value::Static(if b.value {
159+
quote! { #key }
160+
} else {
161+
return None;
162+
}),
163+
_ => Value::Dynamic(quote_spanned! {value.span()=> {
164+
::yew::utils::__ensure_type::<bool>(#value);
165+
#key
166+
}}),
167+
},
168+
expr => Value::Dynamic(quote_spanned! {expr.span()=>
169+
if #expr {
170+
::std::option::Option::Some(
171+
::std::borrow::Cow::<'static, str>::Borrowed(#key)
172+
)
173+
} else {
174+
None
175+
}
176+
}),
177+
},
178+
))
161179
});
162180
let class_attr = classes.as_ref().and_then(|classes| match classes {
163181
ClassesForm::Tuple(classes) => {
@@ -176,36 +194,78 @@ impl ToTokens for HtmlElement {
176194
};
177195
};
178196

179-
Some(quote! {
180-
{
181-
let mut __yew_classes = ::yew::html::Classes::with_capacity(#n);
182-
#(__yew_classes.push(#classes);)*
197+
Some((
198+
LitStr::new("class", span),
199+
Value::Dynamic(quote! {
200+
{
201+
#deprecation_warning
183202

184-
#deprecation_warning
185-
186-
::yew::virtual_dom::PositionalAttr::new("class", __yew_classes)
187-
}
188-
})
203+
let mut __yew_classes = ::yew::html::Classes::with_capacity(#n);
204+
#(__yew_classes.push(#classes);)*
205+
__yew_classes
206+
}
207+
}),
208+
))
189209
}
190-
ClassesForm::Single(classes) => match classes.try_into_lit() {
191-
Some(lit) => {
192-
if lit.value().is_empty() {
193-
None
194-
} else {
195-
let sr = lit.stringify();
196-
Some(quote! { ::yew::virtual_dom::PositionalAttr::new("class", #sr) })
210+
ClassesForm::Single(classes) => {
211+
match classes.try_into_lit() {
212+
Some(lit) => {
213+
if lit.value().is_empty() {
214+
None
215+
} else {
216+
Some((
217+
LitStr::new("class", lit.span()),
218+
Value::Static(quote! { #lit }),
219+
))
220+
}
221+
}
222+
None => {
223+
Some((
224+
LitStr::new("class", classes.span()),
225+
Value::Dynamic(quote! {
226+
::std::convert::Into::<::yew::html::Classes>::into(#classes)
227+
}),
228+
))
197229
}
198-
}
199-
None => {
200-
Some(quote! { ::yew::virtual_dom::PositionalAttr::new("class", ::std::convert::Into::<::yew::html::Classes>::into(#classes)) })
201230
}
202231
}
203232
});
204233

205-
let attrs = normal_attrs.chain(boolean_attrs).chain(class_attr);
206-
quote! {
207-
::yew::virtual_dom::Attributes::Vec(::std::vec![#(#attrs),*])
234+
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static`
235+
fn try_into_static(src: &[(LitStr, Value)]) -> Option<TokenStream> {
236+
let mut kv = Vec::with_capacity(src.len());
237+
for (k, v) in src.iter() {
238+
let v = match v {
239+
Value::Static(v) => quote! { #v },
240+
Value::Dynamic(_) => return None,
241+
};
242+
kv.push(quote! { [ #k, #v ] });
243+
}
244+
245+
Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
208246
}
247+
248+
let attrs = normal_attrs
249+
.chain(boolean_attrs)
250+
.chain(class_attr)
251+
.collect::<Vec<(LitStr, Value)>>();
252+
try_into_static(&attrs).unwrap_or_else(|| {
253+
let keys = attrs.iter().map(|(k, _)| quote! { #k });
254+
let values = attrs.iter().map(|(_, v)| {
255+
quote_spanned! {v.span()=>
256+
::yew::html::IntoPropValue::<
257+
::std::option::Option::<::yew::virtual_dom::AttrValue>
258+
>
259+
::into_prop_value(#v)
260+
}
261+
});
262+
quote! {
263+
::yew::virtual_dom::Attributes::Dynamic{
264+
keys: &[#(#keys),*],
265+
values: ::std::boxed::Box::new([#(#values),*]),
266+
}
267+
}
268+
})
209269
};
210270

211271
let listeners = if listeners.is_empty() {
@@ -291,9 +351,7 @@ impl ToTokens for HtmlElement {
291351
let handle_value_attr = props.value.as_ref().map(|prop| {
292352
let v = prop.value.optimize_literals();
293353
quote_spanned! {v.span()=> {
294-
__yew_vtag.__macro_push_attr(
295-
::yew::virtual_dom::PositionalAttr::new("value", #v),
296-
);
354+
__yew_vtag.__macro_push_attr("value", #v);
297355
}}
298356
});
299357

packages/yew-macro/src/stringify.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,21 @@ pub trait Stringify {
2121

2222
/// Optimize literals to `&'static str`, otherwise keep the value as is.
2323
fn optimize_literals(&self) -> TokenStream
24+
where
25+
Self: ToTokens,
26+
{
27+
self.optimize_literals_tagged().to_token_stream()
28+
}
29+
30+
/// Like `optimize_literals` but tags static or dynamic strings with [Value]
31+
fn optimize_literals_tagged(&self) -> Value
2432
where
2533
Self: ToTokens,
2634
{
2735
if let Some(lit) = self.try_into_lit() {
28-
lit.to_token_stream()
36+
Value::Static(lit.to_token_stream())
2937
} else {
30-
self.to_token_stream()
38+
Value::Dynamic(self.to_token_stream())
3139
}
3240
}
3341
}
@@ -41,6 +49,21 @@ impl<T: Stringify + ?Sized> Stringify for &T {
4149
}
4250
}
4351

52+
/// A stringified value that can be either static (known at compile time) or dynamic (known only at
53+
/// runtime)
54+
pub enum Value {
55+
Static(TokenStream),
56+
Dynamic(TokenStream),
57+
}
58+
59+
impl ToTokens for Value {
60+
fn to_tokens(&self, tokens: &mut TokenStream) {
61+
tokens.extend(match self {
62+
Value::Static(tt) | Value::Dynamic(tt) => tt.clone(),
63+
});
64+
}
65+
}
66+
4467
impl Stringify for LitStr {
4568
fn try_into_lit(&self) -> Option<LitStr> {
4669
Some(self.clone())

packages/yew-macro/tests/classes_macro/classes-fail.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied
5151
<Classes as From<&[T]>>
5252
and 4 others
5353
= note: required because of the requirements on the impl of `Into<Classes>` for `{integer}`
54-
= note: required because of the requirements on the impl of `From<std::vec::Vec<{integer}>>` for `Classes`
54+
= note: required because of the requirements on the impl of `From<Vec<{integer}>>` for `Classes`
5555
= note: 1 redundant requirements hidden
56-
= note: required because of the requirements on the impl of `Into<Classes>` for `std::vec::Vec<{integer}>`
56+
= note: required because of the requirements on the impl of `Into<Classes>` for `Vec<{integer}>`
5757

5858
error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied
5959
--> $DIR/classes-fail.rs:13:14

packages/yew-macro/tests/html_macro/element-fail.stderr

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,7 @@ error[E0277]: the trait bound `(): IntoPropValue<Option<Cow<'static, str>>>` is
177177
43 | html! { <input type={()} /> };
178178
| ^^ the trait `IntoPropValue<Option<Cow<'static, str>>>` is not implemented for `()`
179179
|
180-
::: $WORKSPACE/packages/yew/src/virtual_dom/mod.rs
181-
|
182-
| pub fn new(key: &'static str, value: impl IntoPropValue<Option<AttrValue>>) -> Self {
183-
| -------------------------------- required by this bound in `PositionalAttr::new`
180+
= note: required by `into_prop_value`
184181

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

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

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

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

247230
error[E0277]: the trait bound `{integer}: IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not satisfied
248231
--> $DIR/element-fail.rs:51:28

packages/yew-macro/tests/props_macro/resolve-prop-fail.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
error[E0277]: the trait bound `std::vec::Vec<_>: yew::Properties` is not satisfied
1+
error[E0277]: the trait bound `Vec<_>: yew::Properties` is not satisfied
22
--> $DIR/resolve-prop-fail.rs:38:17
33
|
44
38 | yew::props!(Vec<_> {});
5-
| ^^^ the trait `yew::Properties` is not implemented for `std::vec::Vec<_>`
5+
| ^^^ the trait `yew::Properties` is not implemented for `Vec<_>`
66
|
77
= note: required by `builder`
88

packages/yew/src/html/classes.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::virtual_dom::AttrValue;
33
use indexmap::IndexSet;
44
use std::{
55
borrow::{Borrow, Cow},
6+
hint::unreachable_unchecked,
67
iter::FromIterator,
78
};
89

@@ -65,16 +66,22 @@ impl Classes {
6566
}
6667

6768
impl IntoPropValue<AttrValue> for Classes {
69+
#[inline]
6870
fn into_prop_value(mut self) -> AttrValue {
6971
if self.set.len() == 1 {
70-
self.set.pop().unwrap()
72+
match self.set.pop() {
73+
Some(attr) => attr,
74+
// SAFETY: the collection is checked to be non-empty above
75+
None => unsafe { unreachable_unchecked() },
76+
}
7177
} else {
7278
Cow::Owned(self.to_string())
7379
}
7480
}
7581
}
7682

7783
impl IntoPropValue<Option<AttrValue>> for Classes {
84+
#[inline]
7885
fn into_prop_value(self) -> Option<AttrValue> {
7986
if self.is_empty() {
8087
None

packages/yew/src/utils/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ impl<IN, OUT> IntoIterator for NodeSeq<IN, OUT> {
9595
}
9696
}
9797

98+
/// Hack to force type mismatch compile errors in yew-macro.
99+
//
100+
// TODO: replace with `compile_error!`, when `type_name_of_val` is stabilised (https://github.com/rust-lang/rust/issues/66359).
101+
#[doc(hidden)]
102+
pub fn __ensure_type<T>(_: T) {}
103+
98104
/// Print the [web_sys::Node]'s contents as a string for debugging purposes
99105
pub fn print_node(n: &web_sys::Node) -> String {
100106
use wasm_bindgen::JsCast;

0 commit comments

Comments
 (0)