Optimise vtag (#1447)

* yew/vtag: remove needless indirection

* yew/vtag: reduce VTag memory footprint

* Revert "yew/vtag: remove needless indirection"

This reverts commit 7c59c61e0e.

* yew/vtag,yew-macro: optimise attribute setting and memory usage

* yew/vtag,yew-macro: reduce string memory footprint and use static strings more

* yew,yew-macro: opportunistically use static memory for VText

* yew/vtag: use String instead of Box<str>

* yew-macro: remove one extra iteration

* yew/vtag: remove API extension for textarea

* yew/vtag: remove extra calls

* yew-macro: preconstruct StringRef for class literals

* yew-macro: construct non-dynamic VTags in-place

* yew-macro: Insert class and href into attrs in-place

* *: run stable checks

* yew/vtag: use key-pair vector for attributes

* yew/macro,yew: use trait associated methods with paths, where possible

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* yew/vtag: comment clarification

as suggested by @teymour-aldridge

* yew/vtag: fix added test failing

* yew/vlist: revert removing key

* yew/vtag,yew-macro: stash VTagInner changes for later

* yew/vtag: restore diff_attributes() generating a patch list + add bechmarks

* yew: fix becnhmarks running with std_web

* yew: remove Href

* yew/vtag: fix comment changes

* examples: fix trait impl

* yew: swap Stringref with Cow

* examples: remove redundant clone

* ci: fix stable check

* yew/VText: remove needless constructor

* *: remove needless trait full paths

* yew/benchmarks: add IndexMap attribute diff becnhmark

* yew-macro: fix stderr regressions

* yew: convert Attributes to enum

with variants optimised for both the html! macro and the VTag API

* yew/benchmarks: move feature guard

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* examples: remove unused import

* Apply suggestions from code review

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* yew-macro: rebuild stderr

* yew-macro: accept Into<Cow> for dynamic tags

* yew-macro: remove unneeded {} wrapping

* yew: revert doc comment

* yew/vtag: clean up attribute type conversion

* yew-macro: remove now supported literal failures

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>
Co-authored-by: Justin Starry <justin.starry@icloud.com>
Co-authored-by: Justin Starry <justin.m.starry@gmail.com>
This commit is contained in:
bakape 2020-08-22 13:45:02 +03:00 committed by GitHub
parent 1e3f4e54d3
commit 2b584ca37b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 750 additions and 257 deletions

18
ci/run_benchmarks.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
# SETUP
test_flags=("--headless" "--firefox")
test_features="wasm_test"
echo "running benchmarks with flags: ${test_flags[*]} and features: ${test_features}"
# TESTS
set -x
(cd yew &&
wasm-pack test "${test_flags[@]}" --release \
-- --features "${test_features}" bench
)

View File

@ -13,12 +13,13 @@ use yew::{html, Html};
fn add_class(vtag: &mut VTag, class: &str) { fn add_class(vtag: &mut VTag, class: &str) {
let mut classes: Classes = vtag let mut classes: Classes = vtag
.attributes .attributes
.get("class") .iter()
.map(AsRef::as_ref) .find(|(k, _)| k == &"class")
.map(|(_, v)| AsRef::as_ref(v))
.unwrap_or("") .unwrap_or("")
.into(); .into();
classes.push(class); classes.push(class);
vtag.add_attribute("class", &classes); vtag.add_attribute("class", classes.to_string());
} }
/// Renders a string of Markdown to HTML with the default options (footnotes /// Renders a string of Markdown to HTML with the default options (footnotes
@ -72,7 +73,7 @@ pub fn render_markdown(src: &str) -> Html {
if let VNode::VTag(ref mut vtag) = c { if let VNode::VTag(ref mut vtag) = c {
// TODO // TODO
// vtag.tag = "th".into(); // vtag.tag = "th".into();
vtag.add_attribute("scope", &"col"); vtag.add_attribute("scope", "col");
} }
} }
} }
@ -84,7 +85,7 @@ pub fn render_markdown(src: &str) -> Html {
} }
Event::Text(text) => add_child!(VText::new(text.to_string()).into()), Event::Text(text) => add_child!(VText::new(text.to_string()).into()),
Event::Rule => add_child!(VTag::new("hr").into()), Event::Rule => add_child!(VTag::new("hr").into()),
Event::SoftBreak => add_child!(VText::new("\n".to_string()).into()), Event::SoftBreak => add_child!(VText::new("\n").into()),
Event::HardBreak => add_child!(VTag::new("br").into()), Event::HardBreak => add_child!(VTag::new("br").into()),
_ => println!("Unknown event: {:#?}", ev), _ => println!("Unknown event: {:#?}", ev),
} }
@ -109,7 +110,7 @@ fn make_tag(t: Tag) -> VTag {
} }
Tag::BlockQuote => { Tag::BlockQuote => {
let mut el = VTag::new("blockquote"); let mut el = VTag::new("blockquote");
el.add_attribute("class", &"blockquote"); el.add_attribute("class", "blockquote");
el el
} }
Tag::CodeBlock(code_block_kind) => { Tag::CodeBlock(code_block_kind) => {
@ -121,10 +122,10 @@ fn make_tag(t: Tag) -> VTag {
// highlighting support by locating the language classes and applying dom transforms // highlighting support by locating the language classes and applying dom transforms
// on their contents. // on their contents.
match lang.as_ref() { match lang.as_ref() {
"html" => el.add_attribute("class", &"html-language"), "html" => el.add_attribute("class", "html-language"),
"rust" => el.add_attribute("class", &"rust-language"), "rust" => el.add_attribute("class", "rust-language"),
"java" => el.add_attribute("class", &"java-language"), "java" => el.add_attribute("class", "java-language"),
"c" => el.add_attribute("class", &"c-language"), "c" => el.add_attribute("class", "c-language"),
_ => {} // Add your own language highlighting support _ => {} // Add your own language highlighting support
}; };
} }
@ -135,13 +136,13 @@ fn make_tag(t: Tag) -> VTag {
Tag::List(Some(1)) => VTag::new("ol"), Tag::List(Some(1)) => VTag::new("ol"),
Tag::List(Some(ref start)) => { Tag::List(Some(ref start)) => {
let mut el = VTag::new("ol"); let mut el = VTag::new("ol");
el.add_attribute("start", start); el.add_attribute("start", start.to_string());
el el
} }
Tag::Item => VTag::new("li"), Tag::Item => VTag::new("li"),
Tag::Table(_) => { Tag::Table(_) => {
let mut el = VTag::new("table"); let mut el = VTag::new("table");
el.add_attribute("class", &"table"); el.add_attribute("class", "table");
el el
} }
Tag::TableHead => VTag::new("th"), Tag::TableHead => VTag::new("th"),
@ -149,36 +150,36 @@ fn make_tag(t: Tag) -> VTag {
Tag::TableCell => VTag::new("td"), Tag::TableCell => VTag::new("td"),
Tag::Emphasis => { Tag::Emphasis => {
let mut el = VTag::new("span"); let mut el = VTag::new("span");
el.add_attribute("class", &"font-italic"); el.add_attribute("class", "font-italic");
el el
} }
Tag::Strong => { Tag::Strong => {
let mut el = VTag::new("span"); let mut el = VTag::new("span");
el.add_attribute("class", &"font-weight-bold"); el.add_attribute("class", "font-weight-bold");
el el
} }
Tag::Link(_link_type, ref href, ref title) => { Tag::Link(_link_type, ref href, ref title) => {
let mut el = VTag::new("a"); let mut el = VTag::new("a");
el.add_attribute("href", href); el.add_attribute("href", href.to_string());
let title = title.clone().into_string(); let title = title.clone().into_string();
if title != "" { if title != "" {
el.add_attribute("title", &title); el.add_attribute("title", title);
} }
el el
} }
Tag::Image(_link_type, ref src, ref title) => { Tag::Image(_link_type, ref src, ref title) => {
let mut el = VTag::new("img"); let mut el = VTag::new("img");
el.add_attribute("src", src); el.add_attribute("src", src.to_string());
let title = title.clone().into_string(); let title = title.clone().into_string();
if title != "" { if title != "" {
el.add_attribute("title", &title); el.add_attribute("title", title);
} }
el el
} }
Tag::FootnoteDefinition(ref _footnote_id) => VTag::new("span"), // Footnotes are not rendered as anything special Tag::FootnoteDefinition(ref _footnote_id) => VTag::new("span"), // Footnotes are not rendered as anything special
Tag::Strikethrough => { Tag::Strikethrough => {
let mut el = VTag::new("span"); let mut el = VTag::new("span");
el.add_attribute("class", &"text-decoration-strikethrough"); el.add_attribute("class", "text-decoration-strikethrough");
el el
} }
} }

View File

@ -1,6 +1,7 @@
#![recursion_limit = "512"] #![recursion_limit = "512"]
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use strum_macros::{EnumIter, ToString}; use strum_macros::{EnumIter, ToString};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -8,7 +9,7 @@ use yew::events::KeyboardEvent;
use yew::format::Json; use yew::format::Json;
use yew::services::storage::{Area, StorageService}; use yew::services::storage::{Area, StorageService};
use yew::web_sys::HtmlInputElement as InputElement; use yew::web_sys::HtmlInputElement as InputElement;
use yew::{html, Component, ComponentLink, Href, Html, InputData, NodeRef, ShouldRender}; use yew::{html, Component, ComponentLink, Html, InputData, NodeRef, ShouldRender};
const KEY: &str = "yew.todomvc.self"; const KEY: &str = "yew.todomvc.self";
@ -190,11 +191,16 @@ impl Component for Model {
impl Model { impl Model {
fn view_filter(&self, filter: Filter) -> Html { fn view_filter(&self, filter: Filter) -> Html {
let cls = if self.state.filter == filter {
"selected"
} else {
"not-selected"
};
let flt = filter.clone(); let flt = filter.clone();
html! { html! {
<li> <li>
<a class=if self.state.filter == flt { "selected" } else { "not-selected" } <a class=cls
href=&flt href=filter
onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))> onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))>
{ filter } { filter }
</a> </a>
@ -272,8 +278,8 @@ pub enum Filter {
Completed, Completed,
} }
impl<'a> Into<Href> for &'a Filter { impl<'a> Into<Cow<'static, str>> for &'a Filter {
fn into(self) -> Href { fn into(self) -> Cow<'static, str> {
match *self { match *self {
Filter::All => "#/".into(), Filter::All => "#/".into(),
Filter::Active => "#/active".into(), Filter::Active => "#/active".into(),

View File

@ -137,7 +137,7 @@ where
let view_option = |value: &T| { let view_option = |value: &T| {
let flag = selected == Some(value); let flag = selected == Some(value);
html! { html! {
<option value=value selected=flag>{ value.to_string() }</option> <option value=value.to_string() selected=flag>{ value.to_string() }</option>
} }
}; };

View File

@ -36,9 +36,9 @@ impl<COMP: Component> VTagProducer<COMP> {
self self
} }
pub fn attribute(mut self, name: String, value: String) -> Self { pub fn attribute(mut self, name: &'static str, value: String) -> Self {
let effect = Effect::new(move |mut vtag: VTag, _scope: &ScopeHolder<COMP>| { let effect = Effect::new(move |mut vtag: VTag, _scope: &ScopeHolder<COMP>| {
vtag.add_attribute(&name, &value); vtag.add_attribute(name, value);
vtag vtag
}); });
self.effects.push(effect); self.effects.push(effect);
@ -56,7 +56,7 @@ impl<COMP: Component> VTagProducer<COMP> {
pub fn classes(mut self, classes: Classes) -> Self { pub fn classes(mut self, classes: Classes) -> Self {
let effect = Effect::new(move |mut vtag: VTag, _scope: &ScopeHolder<COMP>| { let effect = Effect::new(move |mut vtag: VTag, _scope: &ScopeHolder<COMP>| {
vtag.add_attribute("class", &classes.to_string()); vtag.add_attribute("class", classes.to_string());
vtag vtag
}); });
self.effects.push(effect); self.effects.push(effect);

View File

@ -44,7 +44,10 @@ impl PeekValue<()> for HtmlNode {
impl ToTokens for HtmlNode { impl ToTokens for HtmlNode {
fn to_tokens(&self, tokens: &mut TokenStream) { fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(match &self { tokens.extend(match &self {
HtmlNode::Literal(lit) => quote! {#lit}, HtmlNode::Literal(lit) => {
let sr = crate::stringify::Constructor::from(lit.as_ref());
quote! { ::yew::virtual_dom::VText::new(#sr) }
}
HtmlNode::Expression(expr) => quote_spanned! {expr.span()=> #expr}, HtmlNode::Expression(expr) => quote_spanned! {expr.span()=> #expr},
}); });
} }

View File

@ -4,6 +4,7 @@ use super::HtmlChildrenTree;
use super::HtmlDashedName; use super::HtmlDashedName;
use super::HtmlProp as TagAttribute; use super::HtmlProp as TagAttribute;
use super::HtmlPropSuffix as TagSuffix; use super::HtmlPropSuffix as TagSuffix;
use crate::stringify;
use crate::{non_capitalized_ascii, Peek, PeekValue}; use crate::{non_capitalized_ascii, Peek, PeekValue};
use boolinator::Boolinator; use boolinator::Boolinator;
use proc_macro2::{Delimiter, Span}; use proc_macro2::{Delimiter, Span};
@ -104,7 +105,7 @@ impl ToTokens for HtmlTag {
let name = match &tag_name { let name = match &tag_name {
TagName::Lit(name) => { TagName::Lit(name) => {
let name_str = name.to_string(); let name_str = name.to_string();
quote! {#name_str} quote! { ::std::borrow::Cow::<'static, str>::Borrowed(#name_str) }
} }
TagName::Expr(name) => { TagName::Expr(name) => {
let expr = &name.expr; let expr = &name.expr;
@ -112,7 +113,7 @@ impl ToTokens for HtmlTag {
// this way we get a nice error message (with the correct span) when the expression doesn't return a valid value // 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()=> { quote_spanned! {expr.span()=> {
#[allow(unused_braces)] #[allow(unused_braces)]
let mut #vtag_name = ::std::borrow::Cow::<'static, str>::from(#expr); let mut #vtag_name = ::std::convert::Into::<::std::borrow::Cow::<'static, str>>::into(#expr);
if !#vtag_name.is_ascii() { if !#vtag_name.is_ascii() {
::std::panic!("a dynamic tag returned a tag name containing non ASCII characters: `{}`", #vtag_name); ::std::panic!("a dynamic tag returned a tag name containing non ASCII characters: `{}`", #vtag_name);
} }
@ -132,50 +133,72 @@ impl ToTokens for HtmlTag {
checked, checked,
node_ref, node_ref,
key, key,
href,
listeners, listeners,
} = &attributes; } = &attributes;
let vtag = Ident::new("__yew_vtag", tag_name.span()); let vtag = Ident::new("__yew_vtag", tag_name.span());
let attr_pairs = attributes.iter().map(|TagAttribute { label, value }| { let mut attr_pairs: Vec<_> = attributes
let label_str = label.to_string(); .iter()
quote_spanned! {value.span()=> (#label_str.to_owned(), (#value).to_string()) } .map(|TagAttribute { label, value }| {
}); let label_str = label.to_string();
let sr = stringify::Constructor::from(value);
quote! { (#label_str, #sr) }
})
.collect();
let set_booleans = booleans.iter().map(|TagAttribute { label, value }| { let set_booleans = booleans.iter().map(|TagAttribute { label, value }| {
let label_str = label.to_string(); let label_str = label.to_string();
// We use `set_boolean_attribute` instead of inlining an if statement to avoid quote_spanned! {value.span()=> {
// the `suspicious_else_formatting` clippy warning. #[allow(clippy::suspicious_else_formatting)]
quote_spanned! {value.span()=> #vtag.set_boolean_attribute(&#label_str, #value); } if #value {
#vtag.push_attribute(
#label_str,
::std::borrow::Cow::<'static, str>::Borrowed(#label_str),
);
}
}}
}); });
let set_kind = kind.iter().map(|kind| { let set_kind = kind.iter().map(|kind| {
quote_spanned! {kind.span()=> #vtag.set_kind(&(#kind)); } let sr = stringify::Constructor::from(kind);
quote_spanned! {kind.span()=> #vtag.set_kind(#sr); }
}); });
let set_value = value.iter().map(|value| { let set_value = value.iter().map(|value| {
quote_spanned! {value.span()=> #vtag.set_value(&(#value)); } quote_spanned! {value.span()=> #vtag.set_value(&(#value)); }
}); });
let add_href = href.iter().map(|href| {
quote_spanned! {href.span()=>
let __yew_href: ::yew::html::Href = (#href).into();
#vtag.add_attribute("href", &__yew_href);
}
});
let set_checked = checked.iter().map(|checked| { let set_checked = checked.iter().map(|checked| {
quote_spanned! {checked.span()=> #vtag.set_checked(#checked); } quote_spanned! {checked.span()=> #vtag.set_checked(#checked); }
}); });
let set_classes = classes.iter().map(|classes_form| match classes_form {
ClassesForm::Tuple(classes) => quote! { let set_classes = match classes {
let __yew_classes = ::yew::virtual_dom::Classes::default()#(.extend(#classes))*; Some(ClassesForm::Tuple(classes)) => Some(quote! {
let __yew_classes
= ::yew::virtual_dom::Classes::default()#(.extend(#classes))*;
if !__yew_classes.is_empty() { if !__yew_classes.is_empty() {
#vtag.add_attribute("class", &__yew_classes); #vtag.push_attribute("class", __yew_classes.to_string());
} }
}, }),
ClassesForm::Single(classes) => quote! { Some(ClassesForm::Single(classes)) => match stringify::try_stringify_expr(classes) {
let __yew_classes = ::std::convert::Into::<::yew::virtual_dom::Classes>::into(#classes); Some(s) => {
if !__yew_classes.is_empty() { if !s.is_empty() {
#vtag.add_attribute("class", &__yew_classes); let sr = stringify::Constructor::from(s);
attr_pairs.push(quote! { ("class", #sr) });
}
None
} }
None => Some(quote! {
let __yew_classes
= ::std::convert::Into::<::yew::virtual_dom::Classes>::into(#classes);
if !__yew_classes.is_empty() {
#vtag.push_attribute(
"class",
::std::string::ToString::to_string(&__yew_classes),
);
}
}),
}, },
}); None => None,
};
let set_classes_it = set_classes.iter();
let set_node_ref = node_ref.iter().map(|node_ref| { let set_node_ref = node_ref.iter().map(|node_ref| {
quote! { quote! {
#vtag.node_ref = #node_ref; #vtag.node_ref = #node_ref;
@ -186,16 +209,18 @@ impl ToTokens for HtmlTag {
#vtag.key = Some(::yew::virtual_dom::Key::from(#key)); #vtag.key = Some(::yew::virtual_dom::Key::from(#key));
} }
}); });
let listeners = listeners.iter().map(|listener| { let listeners: Vec<_> = listeners
let name = &listener.label.name; .iter()
let callback = &listener.value; .map(|listener| {
let name = &listener.label.name;
let callback = &listener.value;
quote_spanned! {name.span()=> ::yew::html::#name::Wrapper::new( quote_spanned! {name.span()=> ::yew::html::#name::Wrapper::new(
<::yew::virtual_dom::VTag as ::yew::virtual_dom::Transformer<_, _>>::transform( <::yew::virtual_dom::VTag as ::yew::virtual_dom::Transformer<_, _>>
#callback ::transform(#callback),
) )}
)} })
}); .collect();
// These are the runtime-checks exclusive to dynamic tags. // These are the runtime-checks exclusive to dynamic tags.
// For literal tags this is already done at compile-time. // For literal tags this is already done at compile-time.
@ -219,7 +244,7 @@ impl ToTokens for HtmlTag {
"input" | "textarea" => {} "input" | "textarea" => {}
_ => { _ => {
if let ::std::option::Option::Some(value) = #vtag.value.take() { if let ::std::option::Option::Some(value) = #vtag.value.take() {
#vtag.attributes.insert("value".to_string(), value); #vtag.push_attribute("value", value);
} }
} }
} }
@ -228,21 +253,37 @@ impl ToTokens for HtmlTag {
None None
}; };
// Constant ifs - easy for the compiler to optimise out
// Attribute setting ordered to reduce reallocation on collection expansion
let has_attrs = !attr_pairs.is_empty();
let has_listeners = !listeners.is_empty();
let has_children = !children.is_empty();
tokens.extend(quote! {{ tokens.extend(quote! {{
#[allow(unused_braces)]
let mut #vtag = ::yew::virtual_dom::VTag::new(#name); let mut #vtag = ::yew::virtual_dom::VTag::new(#name);
#(#set_kind)*
#(#set_value)*
#(#add_href)*
#(#set_checked)*
#(#set_booleans)*
#(#set_classes)*
#(#set_node_ref)* #(#set_node_ref)*
#(#set_key)* #(#set_key)*
#[allow(redundant_clone, unused_braces)] #(#set_kind)*
#vtag.add_attributes(vec![#(#attr_pairs),*]);
#vtag.add_listeners(vec![#(::std::rc::Rc::new(#listeners)),*]); #[allow(clippy::suspicious_else_formatting)]
#vtag.add_children(#children); if #has_attrs {
#vtag.attributes = ::yew::virtual_dom::Attributes::Vec(vec![#(#attr_pairs),*]);
}
#(#set_booleans)*
#(#set_classes_it)*
#(#set_checked)*
#(#set_value)*
if #has_listeners {
#vtag.add_listeners(vec![#(::std::rc::Rc::new(#listeners)),*]);
}
if #has_children {
#[allow(redundant_clone, unused_braces)]
#vtag.add_children(#children);
}
#dyn_tag_runtime_checks #dyn_tag_runtime_checks
#[allow(unused_braces)]
::yew::virtual_dom::VNode::from(#vtag) ::yew::virtual_dom::VNode::from(#vtag)
}}); }});
} }

View File

@ -16,7 +16,6 @@ pub struct TagAttributes {
pub checked: Option<Expr>, pub checked: Option<Expr>,
pub node_ref: Option<Expr>, pub node_ref: Option<Expr>,
pub key: Option<Expr>, pub key: Option<Expr>,
pub href: Option<Expr>,
} }
pub enum ClassesForm { pub enum ClassesForm {
@ -316,8 +315,6 @@ impl Parse for TagAttributes {
let node_ref = TagAttributes::remove_attr(&mut attributes, "ref"); let node_ref = TagAttributes::remove_attr(&mut attributes, "ref");
let key = TagAttributes::remove_attr(&mut attributes, "key"); let key = TagAttributes::remove_attr(&mut attributes, "key");
let href = TagAttributes::remove_attr(&mut attributes, "href");
Ok(TagAttributes { Ok(TagAttributes {
attributes, attributes,
classes, classes,
@ -327,7 +324,6 @@ impl Parse for TagAttributes {
value, value,
kind, kind,
node_ref, node_ref,
href,
key, key,
}) })
} }

View File

@ -131,9 +131,10 @@ impl Parse for HtmlRootVNode {
impl ToTokens for HtmlRootVNode { impl ToTokens for HtmlRootVNode {
fn to_tokens(&self, tokens: &mut TokenStream) { fn to_tokens(&self, tokens: &mut TokenStream) {
let new_tokens = self.0.to_token_stream(); let new_tokens = self.0.to_token_stream();
tokens.extend(quote! { tokens.extend(quote! {{
#[allow(unused_braces)]
::yew::virtual_dom::VNode::from(#new_tokens) ::yew::virtual_dom::VNode::from(#new_tokens)
}); }});
} }
} }

View File

@ -59,6 +59,7 @@
mod derive_props; mod derive_props;
mod html_tree; mod html_tree;
mod stringify;
use derive_props::DerivePropsInput; use derive_props::DerivePropsInput;
use html_tree::{HtmlRoot, HtmlRootVNode}; use html_tree::{HtmlRoot, HtmlRootVNode};

View File

@ -0,0 +1,67 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{Expr, Lit};
/// Attempt converting expression to str, if it's a literal
pub fn try_stringify_expr(src: &Expr) -> Option<String> {
match src {
Expr::Lit(l) => try_stringify_lit(&l.lit),
_ => None,
}
}
/// Attempt converting literal to str literal
fn try_stringify_lit(src: &Lit) -> Option<String> {
match src {
Lit::Str(v) => Some(v.value()),
Lit::Char(v) => Some(v.value().to_string()),
Lit::Int(v) => Some(v.base10_digits().to_string()),
Lit::Float(v) => Some(v.base10_digits().to_string()),
Lit::Bool(v) => Some(v.value.to_string()),
_ => None,
}
}
/// Converts literals and expressions to Cow<'static, str> construction calls
pub struct Constructor(TokenStream);
macro_rules! stringify_at_runtime {
($src:expr) => {{
let src = $src;
Self(quote! {
::std::borrow::Cow::<'static, str>::Owned(
::std::string::ToString::to_string(&#src),
)
})
}};
}
impl From<&Expr> for Constructor {
fn from(src: &Expr) -> Self {
match try_stringify_expr(src) {
Some(s) => Self::from(s),
None => stringify_at_runtime!(src),
}
}
}
impl From<&Lit> for Constructor {
fn from(src: &Lit) -> Self {
match try_stringify_lit(src) {
Some(s) => Self::from(s),
None => stringify_at_runtime!(src),
}
}
}
impl From<String> for Constructor {
fn from(src: String) -> Self {
Self(quote! { ::std::borrow::Cow::<'static, str>::Borrowed(#src) })
}
}
impl ToTokens for Constructor {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(std::iter::once(self.0.clone()));
}
}

View File

@ -285,13 +285,13 @@ error[E0599]: no method named `build` found for struct `ChildContainerProperties
candidate #1: `proc_macro::bridge::server::TokenStreamBuilder` candidate #1: `proc_macro::bridge::server::TokenStreamBuilder`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<&str>` is not satisfied error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vtext::VText>` is not satisfied
--> $DIR/html-component-fail.rs:113:5 --> $DIR/html-component-fail.rs:113:5
| |
113 | html! { <ChildContainer>{ "Not allowed" }</ChildContainer> }; 113 | html! { <ChildContainer>{ "Not allowed" }</ChildContainer> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<&str>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<yew::virtual_dom::vtext::VText>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
| |
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `&str` = note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vtext::VText`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied

View File

@ -9,10 +9,8 @@ fn compile_fail() {
// unsupported literals // unsupported literals
html! { b'a' }; html! { b'a' };
html! { b"str" }; html! { b"str" };
html! { 1111111111111111111111111111111111111111111111111111111111111111111111111111 };
html! { <span>{ b'a' }</span> }; html! { <span>{ b'a' }</span> };
html! { <span>{ b"str" }</span> }; html! { <span>{ b"str" }</span> };
html! { <span>{ 1111111111111111111111111111111111111111111111111111111111111111111111111111 }</span> };
let not_node = || (); let not_node = || ();
html! { html! {

View File

@ -22,30 +22,18 @@ error: unsupported type
11 | html! { b"str" }; 11 | html! { b"str" };
| ^^^^^^ | ^^^^^^
error: integer literal is too large error: unsupported type
--> $DIR/html-node-fail.rs:12:14 --> $DIR/html-node-fail.rs:12:22
| |
12 | html! { 1111111111111111111111111111111111111111111111111111111111111111111111111111 }; 12 | html! { <span>{ b'a' }</span> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^
error: unsupported type error: unsupported type
--> $DIR/html-node-fail.rs:13:22 --> $DIR/html-node-fail.rs:13:22
| |
13 | html! { <span>{ b'a' }</span> }; 13 | html! { <span>{ b"str" }</span> };
| ^^^^
error: unsupported type
--> $DIR/html-node-fail.rs:14:22
|
14 | html! { <span>{ b"str" }</span> };
| ^^^^^^ | ^^^^^^
error: integer literal is too large
--> $DIR/html-node-fail.rs:15:22
|
15 | html! { <span>{ 1111111111111111111111111111111111111111111111111111111111111111111111111111 }</span> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0425]: cannot find value `invalid` in this scope error[E0425]: cannot find value `invalid` in this scope
--> $DIR/html-node-fail.rs:7:13 --> $DIR/html-node-fail.rs:7:13
| |
@ -65,9 +53,9 @@ error[E0277]: `()` doesn't implement `std::fmt::Display`
= note: required by `std::convert::From::from` = note: required by `std::convert::From::from`
error[E0277]: `()` doesn't implement `std::fmt::Display` error[E0277]: `()` doesn't implement `std::fmt::Display`
--> $DIR/html-node-fail.rs:19:9 --> $DIR/html-node-fail.rs:17:9
| |
19 | not_node() 17 | not_node()
| ^^^^^^^^^^ `()` cannot be formatted with the default formatter | ^^^^^^^^^^ `()` cannot be formatted with the default formatter
| |
= help: the trait `std::fmt::Display` is not implemented for `()` = help: the trait `std::fmt::Display` is not implemented for `()`

View File

@ -155,14 +155,16 @@ error[E0308]: mismatched types
| ^ expected `bool`, found integer | ^ expected `bool`, found integer
error[E0277]: `()` doesn't implement `std::fmt::Display` error[E0277]: `()` doesn't implement `std::fmt::Display`
--> $DIR/html-tag-fail.rs:28:25 --> $DIR/html-tag-fail.rs:28:5
| |
28 | html! { <input type=() /> }; 28 | html! { <input type=() /> };
| ^^ `()` cannot be formatted with the default formatter | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `()` cannot be formatted with the default formatter
| |
= help: the trait `std::fmt::Display` is not implemented for `()` = help: the trait `std::fmt::Display` is not implemented for `()`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required because of the requirements on the impl of `std::string::ToString` for `()` = note: required because of the requirements on the impl of `std::string::ToString` for `()`
= note: required by `std::string::ToString::to_string`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: `()` doesn't implement `std::fmt::Display` error[E0277]: `()` doesn't implement `std::fmt::Display`
--> $DIR/html-tag-fail.rs:29:26 --> $DIR/html-tag-fail.rs:29:26
@ -174,16 +176,17 @@ error[E0277]: `()` doesn't implement `std::fmt::Display`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required because of the requirements on the impl of `std::string::ToString` for `()` = note: required because of the requirements on the impl of `std::string::ToString` for `()`
error[E0277]: the trait bound `yew::html::Href: std::convert::From<()>` is not satisfied error[E0277]: `()` doesn't implement `std::fmt::Display`
--> $DIR/html-tag-fail.rs:30:21 --> $DIR/html-tag-fail.rs:30:5
| |
30 | html! { <a href=() /> }; 30 | html! { <a href=() /> };
| ^^ the trait `std::convert::From<()>` is not implemented for `yew::html::Href` | ^^^^^^^^^^^^^^^^^^^^^^^^ `()` cannot be formatted with the default formatter
| |
= help: the following implementations were found: = help: the trait `std::fmt::Display` is not implemented for `()`
<yew::html::Href as std::convert::From<&'a str>> = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
<yew::html::Href as std::convert::From<std::string::String>> = note: required because of the requirements on the impl of `std::string::ToString` for `()`
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::Href>` for `()` = note: required by `std::string::ToString::to_string`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0308]: mismatched types error[E0308]: mismatched types
--> $DIR/html-tag-fail.rs:32:20 --> $DIR/html-tag-fail.rs:32:20
@ -203,22 +206,17 @@ error[E0308]: mismatched types
= note: expected enum `yew::callback::Callback<web_sys::features::gen_MouseEvent::MouseEvent>` = note: expected enum `yew::callback::Callback<web_sys::features::gen_MouseEvent::MouseEvent>`
found enum `yew::callback::Callback<std::string::String>` found enum `yew::callback::Callback<std::string::String>`
error[E0599]: no method named `to_string` found for struct `NotToString` in the current scope error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
--> $DIR/html-tag-fail.rs:35:27 --> $DIR/html-tag-fail.rs:35:5
| |
3 | struct NotToString;
| -------------------
| |
| method `to_string` not found for this
| doesn't satisfy `NotToString: std::fmt::Display`
| doesn't satisfy `NotToString: std::string::ToString`
...
35 | html! { <input string=NotToString /> }; 35 | html! { <input string=NotToString /> };
| ^^^^^^^^^^^ method not found in `NotToString` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `NotToString` cannot be formatted with the default formatter
| |
= note: the method `to_string` exists but the following trait bounds were not satisfied: = help: the trait `std::fmt::Display` is not implemented for `NotToString`
`NotToString: std::fmt::Display` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
which is required by `NotToString: std::string::ToString` = note: required because of the requirements on the impl of `std::string::ToString` for `NotToString`
= note: required by `std::string::ToString::to_string`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0308]: mismatched types error[E0308]: mismatched types
--> $DIR/html-tag-fail.rs:37:24 --> $DIR/html-tag-fail.rs:37:24
@ -238,4 +236,5 @@ error[E0277]: the trait bound `std::borrow::Cow<'static, str>: std::convert::Fro
<std::borrow::Cow<'a, [T]> as std::convert::From<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>> <std::borrow::Cow<'a, std::ffi::CStr> as std::convert::From<&'a std::ffi::CStr>>
and 11 others and 11 others
= note: required by `std::convert::From::from` = note: required because of the requirements on the impl of `std::convert::Into<std::borrow::Cow<'static, str>>` for `{integer}`
= note: required by `std::convert::Into::into`

View File

@ -1,12 +1,13 @@
#![recursion_limit = "512"] #![recursion_limit = "512"]
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use strum_macros::{EnumIter, ToString}; use strum_macros::{EnumIter, ToString};
use yew::events::IKeyboardEvent; use yew::events::IKeyboardEvent;
use yew::format::Json; use yew::format::Json;
use yew::services::storage::{Area, StorageService}; use yew::services::storage::{Area, StorageService};
use yew::{html, Component, ComponentLink, Href, Html, InputData, KeyPressEvent, ShouldRender}; use yew::{html, Component, ComponentLink, Html, InputData, KeyPressEvent, ShouldRender};
const KEY: &str = "yew.todomvc.self"; const KEY: &str = "yew.todomvc.self";
@ -168,11 +169,16 @@ impl Component for Model {
impl Model { impl Model {
fn view_filter(&self, filter: Filter) -> Html { fn view_filter(&self, filter: Filter) -> Html {
let cls = if self.state.filter == filter {
"selected"
} else {
"not-selected"
};
let flt = filter.clone(); let flt = filter.clone();
html! { html! {
<li> <li>
<a class=if self.state.filter == flt { "selected" } else { "not-selected" } <a class=cls
href=&flt href=flt
onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))> onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))>
{ filter } { filter }
</a> </a>
@ -248,8 +254,8 @@ pub enum Filter {
Completed, Completed,
} }
impl<'a> Into<Href> for &'a Filter { impl<'a> Into<Cow<'static, str>> for &'a Filter {
fn into(self) -> Href { fn into(self) -> Cow<'static, str> {
match *self { match *self {
Filter::All => "#/".into(), Filter::All => "#/".into(),
Filter::Active => "#/active".into(), Filter::Active => "#/active".into(),

View File

@ -110,6 +110,7 @@ wasm-bindgen = "0.2.60"
wasm-bindgen-test = "0.3.4" wasm-bindgen-test = "0.3.4"
base64 = "0.12.0" base64 = "0.12.0"
ssri = "6.0.0" ssri = "6.0.0"
easybench-wasm = "0.2.1"
[target.'cfg(target_os = "emscripten")'.dependencies] [target.'cfg(target_os = "emscripten")'.dependencies]
ryu = "1.0.2" # 1.0.1 breaks emscripten ryu = "1.0.2" # 1.0.1 breaks emscripten

View File

@ -500,32 +500,6 @@ impl EmptyBuilder {
/// Link to component's scope for creating callbacks. /// Link to component's scope for creating callbacks.
pub type ComponentLink<COMP> = Scope<COMP>; pub type ComponentLink<COMP> = Scope<COMP>;
/// A bridging type for checking `href` attribute value.
#[derive(Debug)]
pub struct Href {
link: String,
}
impl From<String> for Href {
fn from(link: String) -> Self {
Href { link }
}
}
impl<'a> From<&'a str> for Href {
fn from(link: &'a str) -> Self {
Href {
link: link.to_owned(),
}
}
}
impl ToString for Href {
fn to_string(&self) -> String {
self.link.to_owned()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -206,7 +206,7 @@ pub mod prelude {
pub use crate::callback::Callback; pub use crate::callback::Callback;
pub use crate::events::*; pub use crate::events::*;
pub use crate::html::{ pub use crate::html::{
Children, ChildrenWithProps, Component, ComponentLink, Href, Html, NodeRef, Properties, Children, ChildrenWithProps, Component, ComponentLink, Html, NodeRef, Properties,
Renderable, ShouldRender, Renderable, ShouldRender,
}; };
pub use crate::macros::*; pub use crate::macros::*;

View File

@ -15,8 +15,8 @@ pub mod vtext;
use crate::html::{AnyScope, NodeRef}; use crate::html::{AnyScope, NodeRef};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use indexmap::set::IndexSet; use indexmap::{IndexMap, IndexSet};
use std::collections::HashMap; use std::borrow::Cow;
use std::fmt; use std::fmt;
use std::rc::Rc; use std::rc::Rc;
cfg_if! { cfg_if! {
@ -60,8 +60,77 @@ impl fmt::Debug for dyn Listener {
/// A list of event listeners. /// A list of event listeners.
type Listeners = Vec<Rc<dyn Listener>>; type Listeners = Vec<Rc<dyn Listener>>;
/// A map of attributes. /// A collection of attributes for an element
type Attributes = HashMap<String, String>; #[derive(PartialEq, Eq, Clone, Debug)]
pub enum Attributes {
/// A vector is ideal because most of the time the list will neither change
/// length nor key order.
Vec(Vec<(&'static str, Cow<'static, str>)>),
/// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro
/// was not used to guarantee it.
IndexMap(IndexMap<&'static str, Cow<'static, str>>),
}
impl Attributes {
/// Construct a default Attributes instance
pub fn new() -> Self {
Default::default()
}
/// Construct new IndexMap variant from Vec variant
pub(crate) fn new_indexmap(v: Vec<(&'static str, Cow<'static, str>)>) -> Self {
Self::IndexMap(v.into_iter().collect())
}
/// Return iterator over attribute key-value pairs
pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&'static str, &'a str)> + 'a> {
macro_rules! pack {
($src:expr) => {
Box::new($src.iter().map(|(k, v)| (*k, v.as_ref())))
};
}
match self {
Self::Vec(v) => pack!(v),
Self::IndexMap(m) => pack!(m),
}
}
}
impl AsMut<IndexMap<&'static str, Cow<'static, str>>> for Attributes {
fn as_mut(&mut self) -> &mut IndexMap<&'static str, Cow<'static, str>> {
match self {
Self::IndexMap(m) => m,
Self::Vec(v) => {
*self = Self::new_indexmap(std::mem::take(v));
self.as_mut()
}
}
}
}
macro_rules! impl_attrs_from {
($($from:path => $variant:ident)*) => {
$(
impl From<$from> for Attributes {
fn from(v: $from) -> Self {
Self::$variant(v)
}
}
)*
};
}
impl_attrs_from! {
Vec<(&'static str, Cow<'static, str>)> => Vec
IndexMap<&'static str, Cow<'static, str>> => IndexMap
}
impl Default for Attributes {
fn default() -> Self {
Self::Vec(Default::default())
}
}
/// A set of classes. /// A set of classes.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -343,7 +412,7 @@ mod layout_tests {
let parent_node: Node = parent_element.clone().into(); let parent_node: Node = parent_element.clone().into();
let end_node = document.create_text_node("END"); let end_node = document.create_text_node("END");
parent_node.append_child(&end_node).unwrap(); parent_node.append_child(&end_node).unwrap();
let mut empty_node: VNode = VText::new("".into()).into(); let mut empty_node: VNode = VText::new("").into();
// Tests each layout independently // Tests each layout independently
let next_sibling = NodeRef::new(end_node.into()); let next_sibling = NodeRef::new(end_node.into());

View File

@ -95,7 +95,7 @@ impl VDiff for VList {
// Without a placeholder the next element becomes first // Without a placeholder the next element becomes first
// and corrupts the order of rendering // and corrupts the order of rendering
// We use empty text element to stake out a place // We use empty text element to stake out a place
let placeholder = VText::new("".into()); let placeholder = VText::new("");
self.children.push(placeholder.into()); self.children.push(placeholder.into());
} }

View File

@ -5,6 +5,7 @@ use crate::html::{AnyScope, NodeRef};
use crate::utils::document; use crate::utils::document;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use cfg_match::cfg_match; use cfg_match::cfg_match;
use indexmap::IndexMap;
use log::warn; use log::warn;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::PartialEq; use std::cmp::PartialEq;
@ -76,7 +77,7 @@ pub struct VTag {
/// Contains /// Contains
/// [kind](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types) /// [kind](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types)
/// value of an `InputElement`. /// value of an `InputElement`.
pub kind: Option<String>, pub kind: Option<Cow<'static, str>>,
/// Represents `checked` attribute of /// Represents `checked` attribute of
/// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked). /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked).
/// It exists to override standard behavior of `checked` attribute, because /// It exists to override standard behavior of `checked` attribute, because
@ -112,7 +113,7 @@ impl Clone for VTag {
impl VTag { impl VTag {
/// Creates a new `VTag` instance with `tag` name (cannot be changed later in DOM). /// Creates a new `VTag` instance with `tag` name (cannot be changed later in DOM).
pub fn new<S: Into<Cow<'static, str>>>(tag: S) -> Self { pub fn new(tag: impl Into<Cow<'static, str>>) -> Self {
let tag: Cow<'static, str> = tag.into(); let tag: Cow<'static, str> = tag.into();
let element_type = ElementType::from_tag(&tag); let element_type = ElementType::from_tag(&tag);
VTag { VTag {
@ -157,8 +158,8 @@ impl VTag {
/// Sets `kind` property of an /// Sets `kind` property of an
/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
/// Same as set `type` attribute. /// Same as set `type` attribute.
pub fn set_kind<T: ToString>(&mut self, value: &T) { pub fn set_kind(&mut self, value: impl Into<Cow<'static, str>>) {
self.kind = Some(value.to_string()); self.kind = Some(value.into());
} }
/// Sets `checked` property of an /// Sets `checked` property of an
@ -168,34 +169,41 @@ impl VTag {
self.checked = value; self.checked = value;
} }
/// Adds attribute to a virtual node. Not every attribute works when /// Pushes a key-value pair to the Vec variant of attributes.
/// it set as attribute. We use workarounds for: ///
/// It is the responsibility of the caller to ensure the attribute list does not contain
/// duplicate keys.
///
/// Not every attribute works when it set as an attribute. We use workarounds for:
/// `type/kind`, `value` and `checked`. /// `type/kind`, `value` and `checked`.
/// pub fn push_attribute(&mut self, key: &'static str, value: impl Into<Cow<'static, str>>) {
/// If this virtual node has this attribute present, the value is replaced. if let Attributes::Vec(v) = &mut self.attributes {
pub fn add_attribute<T: ToString>(&mut self, name: &str, value: &T) { v.push((key, value.into()));
self.attributes.insert(name.to_owned(), value.to_string());
}
/// Sets a boolean attribute if `value` is true. Removes if `value` is false. The name
/// of the attribute will be used as the value.
///
/// Example: `<button disabled="disabled">`
pub fn set_boolean_attribute(&mut self, name: &str, value: bool) {
if value {
self.attributes.insert(name.to_owned(), name.to_owned());
} else {
self.attributes.remove(name);
} }
} }
/// Adds attributes to a virtual node. Not every attribute works when /// Adds a key-value pair to attributes
/// it set as attribute. We use workarounds for: ///
/// Not every attribute works when it set as an attribute. We use workarounds for:
/// `type/kind`, `value` and `checked`. /// `type/kind`, `value` and `checked`.
pub fn add_attributes(&mut self, attrs: Vec<(String, String)>) { pub fn add_attribute(&mut self, key: &'static str, value: impl Into<Cow<'static, str>>) {
for (name, value) in attrs { self.attributes_mut().insert(key, value.into());
self.attributes.insert(name, value); }
}
/// Returns a mutable reference to the IndexMap variant of attributes.
///
/// Not every attribute works when it set as an attribute. We use workarounds for:
/// `type/kind`, `value` and `checked`.
pub fn attributes_mut(&mut self) -> &mut IndexMap<&'static str, Cow<'static, str>> {
self.attributes.as_mut()
}
/// Sets attributes to a virtual node.
///
/// Not every attribute works when it set as an attribute. We use workarounds for:
/// `type/kind`, `value` and `checked`.
pub fn set_attributes(&mut self, attrs: impl Into<Attributes>) {
self.attributes = attrs.into();
} }
/// Adds new listener to the node. /// Adds new listener to the node.
@ -209,9 +217,7 @@ impl VTag {
/// They are boxed because we want to keep them in a single list. /// They are boxed because we want to keep them in a single list.
/// Later `Listener::attach` will attach an actual listener to a DOM node. /// Later `Listener::attach` will attach an actual listener to a DOM node.
pub fn add_listeners(&mut self, listeners: Vec<Rc<dyn Listener>>) { pub fn add_listeners(&mut self, listeners: Vec<Rc<dyn Listener>>) {
for listener in listeners { self.listeners.extend(listeners);
self.listeners.push(listener);
}
} }
/// Every render it removes all listeners and attach it back later /// Every render it removes all listeners and attach it back later
@ -261,36 +267,132 @@ impl VTag {
} }
} }
/// This handles patching of attributes when the keys are equal but /// Diffs attributes between the old and new lists.
/// the values are different. ///
fn diff_attributes<'a>( /// This method optimises for the lists being the same length, having the
&'a self, /// same keys in the same order - most common case.
ancestor: &'a Option<Box<Self>>, fn diff_attributes_vectors<'a>(
) -> impl Iterator<Item = Patch<&'a str, &'a str>> + 'a { new: &'a [(&'static str, Cow<'static, str>)],
// Only change what is necessary. old: &'a [(&'static str, Cow<'static, str>)],
let to_add_or_replace = ) -> Vec<Patch<&'static str, &'a str>> {
self.attributes.iter().filter_map(move |(key, value)| { let mut out = Vec::new();
match ancestor
.as_ref()
.and_then(|ancestor| ancestor.attributes.get(&**key))
{
None => Some(Patch::Add(&**key, &**value)),
Some(ancestor_value) if value != ancestor_value => {
Some(Patch::Replace(&**key, &**value))
}
_ => None,
}
});
let to_remove = ancestor
.iter()
.flat_map(|ancestor| ancestor.attributes.keys())
.filter(move |key| !self.attributes.contains_key(&**key))
.map(|key| Patch::Remove(&**key));
to_add_or_replace.chain(to_remove) if new.len() != old.len() {
diff_incompatible(&mut out, new, old);
return out;
}
for (i, (n, o)) in new.iter().zip(old).enumerate() {
if n.0 != o.0 {
diff_incompatible(&mut out, &new[i..], &old[i..]);
break;
}
if n.1 != o.1 {
out.push(Patch::Replace(n.0, &n.1));
}
}
return out;
/// Diffs attribute lists that do not contain the same set of keys in
/// the same order
fn diff_incompatible<'a>(
dst: &mut Vec<Patch<&'static str, &'a str>>,
new: &'a [(&'static str, Cow<'static, str>)],
old: &'a [(&'static str, Cow<'static, str>)],
) {
use std::collections::HashMap;
macro_rules! collect {
($src:expr) => {
$src.iter()
.map(|(k, v)| (*k, v))
.collect::<HashMap<&'static str, &Cow<'static, str>>>()
};
}
let new = collect!(new);
let old = collect!(old);
for (k, n_v) in new.iter() {
match old.get(k) {
Some(o_v) => {
if n_v != o_v {
dst.push(Patch::Replace(k, n_v));
}
}
None => dst.push(Patch::Add(k, n_v)),
};
}
for k in old.keys() {
if !new.contains_key(k) {
dst.push(Patch::Remove(k));
}
}
}
} }
/// Similar to `diff_attributes` except there is only a single `kind`. /// Diffs attributes between the old and new IndexMaps.
///
/// This method must be used, if either the new or old node were created without using the html!
/// macro, that provides compile-time attribute deduplication.
fn diff_attributes_indexmaps<'a>(
new: &'a IndexMap<&'static str, Cow<'static, str>>,
old: &'a IndexMap<&'static str, Cow<'static, str>>,
) -> Vec<Patch<&'static str, &'a str>> {
use indexmap::map::Iter;
use std::iter::Peekable;
let mut out = Vec::new();
let mut new_iter = new.iter().peekable();
let mut old_iter = old.iter().peekable();
loop {
if new_iter.peek().is_none() || old_iter.peek().is_none() {
break;
}
match (new_iter.next(), old_iter.next()) {
(Some(n), Some(o)) => {
if n.0 != o.0 {
break;
}
if n.1 != o.1 {
out.push(Patch::Replace(*n.0, n.1.as_ref()));
}
}
_ => break,
}
}
diff_incompatible(&mut out, new_iter, old_iter, &new, &old);
return out;
fn diff_incompatible<'a>(
dst: &mut Vec<Patch<&'static str, &'a str>>,
new_keys: Peekable<Iter<'a, &'static str, Cow<'static, str>>>,
old_keys: Peekable<Iter<'a, &'static str, Cow<'static, str>>>,
new_map: &'a IndexMap<&'static str, Cow<'static, str>>,
old_map: &'a IndexMap<&'static str, Cow<'static, str>>,
) {
for (k, n_v) in new_keys {
match old_map.get(k) {
Some(o_v) => {
if n_v != o_v {
dst.push(Patch::Replace(k, n_v));
}
}
None => dst.push(Patch::Add(k, n_v)),
};
}
for (k, _) in old_keys {
if !new_map.contains_key(k) {
dst.push(Patch::Remove(k));
}
}
}
}
/// Compares new kind with ancestor and produces a patch to apply, if any
fn diff_kind<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> { fn diff_kind<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> {
match ( match (
self.kind.as_ref(), self.kind.as_ref(),
@ -309,7 +411,7 @@ impl VTag {
} }
} }
/// Almost identical in spirit to `diff_kind` /// Compares new value with ancestor and produces a patch to apply, if any
fn diff_value<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> { fn diff_value<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> {
match ( match (
self.value.as_ref(), self.value.as_ref(),
@ -328,14 +430,45 @@ impl VTag {
} }
} }
fn apply_diffs(&mut self, ancestor: &Option<Box<Self>>) { fn apply_diffs(&mut self, ancestor: &mut Option<Box<Self>>) {
let element = self.reference.as_ref().expect("element expected"); let element = self.reference.as_ref().expect("element expected");
// Update parameters // Apply attribute patches including an optional "class"-attribute patch.
let changes = self.diff_attributes(ancestor); macro_rules! add_all {
($src:expr) => {
// apply attribute patches including an optional "class"-attribute patch $src.iter()
for change in changes { .map(|(k, v)| Patch::Add(*k, v.as_ref()))
.collect()
};
}
for change in match (
&mut self.attributes,
&mut ancestor.as_mut().map(|a| &mut a.attributes),
) {
(Attributes::Vec(new), Some(Attributes::Vec(old))) => {
Self::diff_attributes_vectors(new, old)
}
(Attributes::IndexMap(new), Some(Attributes::IndexMap(old))) => {
Self::diff_attributes_indexmaps(new, old)
}
(Attributes::Vec(new), None) => add_all!(new),
(Attributes::IndexMap(new), None) => add_all!(new),
(Attributes::Vec(new), Some(Attributes::IndexMap(old))) => {
self.attributes = Attributes::new_indexmap(std::mem::take(new));
match &self.attributes {
Attributes::IndexMap(new) => Self::diff_attributes_indexmaps(new, old),
_ => unreachable!(),
}
}
(Attributes::IndexMap(new), Some(Attributes::Vec(old))) => {
ancestor.as_mut().unwrap().attributes =
Attributes::new_indexmap(std::mem::take(old));
match &ancestor.as_ref().map(|a| &a.attributes) {
Some(Attributes::IndexMap(old)) => Self::diff_attributes_indexmaps(new, old),
_ => unreachable!(),
}
}
} {
match change { match change {
Patch::Add(key, value) | Patch::Replace(key, value) => { Patch::Add(key, value) | Patch::Replace(key, value) => {
element element
@ -345,7 +478,8 @@ impl VTag {
Patch::Remove(key) => { Patch::Remove(key) => {
cfg_match! { cfg_match! {
feature = "std_web" => element.remove_attribute(&key), feature = "std_web" => element.remove_attribute(&key),
feature = "web_sys" => element.remove_attribute(&key).expect("could not remove attribute"), feature = "web_sys" => element.remove_attribute(&key)
.expect("could not remove attribute"),
}; };
} }
} }
@ -431,7 +565,8 @@ impl VTag {
} }
fn create_element(&self, parent: &Element) -> Element { fn create_element(&self, parent: &Element) -> Element {
if self.tag == "svg" let tag = self.tag();
if tag == "svg"
|| parent || parent
.namespace_uri() .namespace_uri()
.map_or(false, |ns| ns == SVG_NAMESPACE) .map_or(false, |ns| ns == SVG_NAMESPACE)
@ -441,11 +576,11 @@ impl VTag {
feature = "web_sys" => Some(SVG_NAMESPACE), feature = "web_sys" => Some(SVG_NAMESPACE),
}; };
document() document()
.create_element_ns(namespace, &self.tag) .create_element_ns(namespace, tag)
.expect("can't create namespaced element for vtag") .expect("can't create namespaced element for vtag")
} else { } else {
document() document()
.create_element(&self.tag) .create_element(tag)
.expect("can't create element for vtag") .expect("can't create element for vtag")
} }
} }
@ -480,7 +615,7 @@ impl VDiff for VTag {
match ancestor { match ancestor {
// If the ancestor is a tag of the same type, don't recreate, keep the // If the ancestor is a tag of the same type, don't recreate, keep the
// old tag and update its attributes and children. // old tag and update its attributes and children.
VNode::VTag(vtag) if self.tag == vtag.tag && self.key == vtag.key => Some(vtag), VNode::VTag(vtag) if self.tag() == vtag.tag() && self.key == vtag.key => Some(vtag),
_ => { _ => {
let element = self.create_element(parent); let element = self.create_element(parent);
super::insert_node(&element, parent, Some(ancestor.first_node())); super::insert_node(&element, parent, Some(ancestor.first_node()));
@ -503,7 +638,7 @@ impl VDiff for VTag {
self.reference = Some(element); self.reference = Some(element);
} }
self.apply_diffs(&ancestor_tag); self.apply_diffs(&mut ancestor_tag);
self.recreate_listeners(&mut ancestor_tag); self.recreate_listeners(&mut ancestor_tag);
// Process children // Process children
@ -713,8 +848,9 @@ mod tests {
/// Returns the class attribute as str reference, or "" if the attribute is not set. /// Returns the class attribute as str reference, or "" if the attribute is not set.
fn get_class_str(vtag: &VTag) -> &str { fn get_class_str(vtag: &VTag) -> &str {
vtag.attributes vtag.attributes
.get("class") .iter()
.map(AsRef::as_ref) .find(|(k, _)| k == &"class")
.map(|(_, v)| AsRef::as_ref(v))
.unwrap_or("") .unwrap_or("")
} }
@ -745,7 +881,7 @@ mod tests {
#[test] #[test]
fn supports_multiple_classes_string() { fn supports_multiple_classes_string() {
let a = html! { let a = html! {
<div class="class-1 class-2 class-3"></div> <div class="class-1 class-2 class-3"></div>
}; };
let b = html! { let b = html! {
@ -836,26 +972,32 @@ mod tests {
let d_arr = [""]; let d_arr = [""];
let d = html! { <div class=&d_arr[..]></div> }; let d = html! { <div class=&d_arr[..]></div> };
macro_rules! has_class {
($vtag:expr) => {
$vtag.attributes.iter().any(|(k, _)| k == "class")
};
}
if let VNode::VTag(vtag) = a { if let VNode::VTag(vtag) = a {
assert!(!vtag.attributes.contains_key("class")); assert!(!has_class!(vtag));
} else { } else {
panic!("vtag expected"); panic!("vtag expected");
} }
if let VNode::VTag(vtag) = b { if let VNode::VTag(vtag) = b {
assert!(!vtag.attributes.contains_key("class")); assert!(!has_class!(vtag));
} else { } else {
panic!("vtag expected"); panic!("vtag expected");
} }
if let VNode::VTag(vtag) = c { if let VNode::VTag(vtag) = c {
assert!(!vtag.attributes.contains_key("class")); assert!(!has_class!(vtag));
} else { } else {
panic!("vtag expected"); panic!("vtag expected");
} }
if let VNode::VTag(vtag) = d { if let VNode::VTag(vtag) = d {
assert!(!vtag.attributes.contains_key("class")); assert!(!vtag.attributes.iter().any(|(k, _)| k == "class"));
} else { } else {
panic!("vtag expected"); panic!("vtag expected");
} }
@ -911,7 +1053,7 @@ mod tests {
#[test] #[test]
fn keeps_order_of_classes() { fn keeps_order_of_classes() {
let a = html! { let a = html! {
<div class="class-1 class-2 class-3",></div> <div class=vec!["class-1", "class-2", "class-3"],></div>
}; };
if let VNode::VTag(vtag) = a { if let VNode::VTag(vtag) = a {
@ -997,10 +1139,12 @@ mod tests {
</p> </p>
}; };
if let VNode::VTag(vtag) = a { if let VNode::VTag(vtag) = a {
assert!(vtag.attributes.contains_key("aria-controls"));
assert_eq!( assert_eq!(
vtag.attributes.get("aria-controls"), vtag.attributes
Some(&"it-works".into()) .iter()
.find(|(k, _)| k == &"aria-controls")
.map(|(_, v)| v),
Some("it-works")
); );
} else { } else {
panic!("vtag expected"); panic!("vtag expected");
@ -1364,7 +1508,7 @@ mod tests {
elem.apply(&scope, &parent, NodeRef::default(), None); elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = assert_vtag(&mut elem); let vtag = assert_vtag(&mut elem);
// make sure the new tag name is used internally // make sure the new tag name is used internally
assert_eq!(vtag.tag, "a"); assert_eq!(vtag.tag(), "a");
#[cfg(feature = "web_sys")] #[cfg(feature = "web_sys")]
// Element.tagName is always in the canonical upper-case form. // Element.tagName is always in the canonical upper-case form.
@ -1378,17 +1522,19 @@ mod tests {
}; };
let div_vtag = assert_vtag(&mut div_el); let div_vtag = assert_vtag(&mut div_el);
assert!(div_vtag.value.is_none()); assert!(div_vtag.value.is_none());
assert_eq!( let v: Option<&str> = div_vtag
div_vtag.attributes.get("value").map(String::as_str), .attributes
Some("Hello") .iter()
); .find(|(k, _)| k == &"value")
.map(|(_, v)| AsRef::as_ref(v));
assert_eq!(v, Some("Hello"));
let mut input_el = html! { let mut input_el = html! {
<@{"input"} value="World"/> <@{"input"} value="World"/>
}; };
let input_vtag = assert_vtag(&mut input_el); let input_vtag = assert_vtag(&mut input_el);
assert_eq!(input_vtag.value, Some("World".to_string())); assert_eq!(input_vtag.value, Some("World".to_string()));
assert!(!input_vtag.attributes.contains_key("value")); assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value"));
} }
#[test] #[test]
@ -1519,3 +1665,180 @@ mod layout_tests {
diff_layouts(vec![layout1, layout2, layout3, layout4]); diff_layouts(vec![layout1, layout2, layout3, layout4]);
} }
} }
#[cfg(all(test, feature = "web_sys", feature = "wasm_test"))]
mod benchmarks {
use super::{Patch, VTag};
use easybench_wasm::bench_env_limit;
use std::borrow::Cow;
use std::collections::HashMap;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
wasm_bindgen_test_configure!(run_in_browser);
// In seconds
const BENCHMARK_DURATION: f64 = 1.0;
fn diff_attributes_hashmap<'a>(
new: &'a HashMap<&'static str, Cow<'static, str>>,
old: &'a HashMap<&'static str, Cow<'static, str>>,
) -> Vec<Patch<&'static str, &'a str>> {
// Only change what is necessary.
let to_add_or_replace = new
.iter()
.filter_map(move |(key, value)| match old.get(key) {
None => Some(Patch::Add(&**key, &**value)),
Some(ancestor_value) if value != ancestor_value => {
Some(Patch::Replace(&**key, &**value))
}
_ => None,
});
let to_remove = old
.keys()
.filter(move |key| !new.contains_key(&**key))
.map(|key| Patch::Remove(&**key));
to_add_or_replace.chain(to_remove).collect()
}
macro_rules! bench_attrs {
($name:ident, $args:expr) => {
#[test]
fn $name() {
let env = $args;
macro_rules! build_map {
($type:ident, $src:expr) => {
&$src
.into_iter()
.collect::<$type<&'static str, Cow<'static, str>>>();
};
}
wasm_bindgen_test::console_log!(
"{}: hashmaps: {}",
stringify!($name),
bench_env_limit(BENCHMARK_DURATION, env.clone(), |(a, b)| {
format!(
"{:?}",
diff_attributes_hashmap(build_map!(HashMap, a), build_map!(HashMap, b))
)
})
);
wasm_bindgen_test::console_log!(
"{}: indexmaps: {}",
stringify!($name),
bench_env_limit(BENCHMARK_DURATION, env.clone(), |(a, b)| {
use indexmap::IndexMap;
format!(
"{:?}",
VTag::diff_attributes_indexmaps(
build_map!(IndexMap, a),
build_map!(IndexMap, b)
)
)
})
);
wasm_bindgen_test::console_log!(
"{}: vectors: {}",
stringify!($name),
bench_env_limit(BENCHMARK_DURATION, env.clone(), |(a, b)| format!(
"{:?}",
VTag::diff_attributes_vectors(&a, &b)
))
);
}
};
}
// Fill vector wit more attributes
fn extend_attrs(dst: &mut Vec<(&'static str, Cow<'static, str>)>) {
dst.extend(vec![
("oh", Cow::Borrowed("danny")),
("boy", Cow::Borrowed("the")),
("pipes", Cow::Borrowed("the")),
("are", Cow::Borrowed("calling")),
("from", Cow::Borrowed("glen")),
("to", Cow::Borrowed("glen")),
("and", Cow::Borrowed("down")),
("the", Cow::Borrowed("mountain")),
("side", Cow::Borrowed("")),
]);
}
bench_attrs! {
bench_diff_attributes_same,
{
let mut old: Vec<(&'static str, Cow<'static, str>)> = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
(old.clone(), old)
}
}
bench_attrs! {
bench_diff_attributes_append,
{
let mut old = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
let mut new = old.clone();
new.push(("hidden", Cow::Borrowed("hidden")));
(new, old)
}
}
bench_attrs! {
bench_diff_attributes_change_first,
{
let mut old = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
let mut new = old.clone();
new[0] = ("disable", Cow::Borrowed("enable"));
(new, old)
}
}
bench_attrs! {
bench_diff_attributes_change_middle,
{
let mut old = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
let mut new = old.clone();
let mid = &mut new.get_mut(old.len()/2).unwrap();
mid.1 = Cow::Borrowed("changed");
(new, old)
}
}
bench_attrs! {
bench_diff_attributes_change_last,
{
let mut old = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
let mut new = old.clone();
let last = &mut new.get_mut(old.len()-1).unwrap();
last.1 = Cow::Borrowed("changed");
(new, old)
}
}
}

View File

@ -5,6 +5,7 @@ use crate::html::{AnyScope, NodeRef};
use crate::utils::document; use crate::utils::document;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use log::warn; use log::warn;
use std::borrow::Cow;
use std::cmp::PartialEq; use std::cmp::PartialEq;
cfg_if! { cfg_if! {
if #[cfg(feature = "std_web")] { if #[cfg(feature = "std_web")] {
@ -20,16 +21,16 @@ cfg_if! {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct VText { pub struct VText {
/// Contains a text of the node. /// Contains a text of the node.
pub text: String, pub text: Cow<'static, str>,
/// A reference to the `TextNode`. /// A reference to the `TextNode`.
pub reference: Option<TextNode>, pub reference: Option<TextNode>,
} }
impl VText { impl VText {
/// Creates new virtual text node with a content. /// Creates new virtual text node with a content.
pub fn new(text: String) -> Self { pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
VText { VText {
text, text: text.into(),
reference: None, reference: None,
} }
} }