Skip to content

Commit

Permalink
add runtime checks for dynamic tags
Browse files Browse the repository at this point in the history
  • Loading branch information
siku2 committed May 27, 2020
1 parent cc14fae commit f37ebd3
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 3 deletions.
49 changes: 46 additions & 3 deletions yew-macro/src/html_tree/html_tag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ impl Parse for HtmlTag {
if let TagName::Lit(name) = &open.tag_name {
// Void elements should not have children.
// See https://html.spec.whatwg.org/multipage/syntax.html#void-elements
//
// For dynamic tags this is done at runtime!
match name.to_string().as_str() {
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
| "meta" | "param" | "source" | "track" | "wbr" => {
Expand Down Expand Up @@ -108,9 +110,15 @@ impl ToTokens for HtmlTag {
TagName::Expr(name) => {
let expr = &name.expr;
// 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()=>
::std::borrow::Cow::<'static, str>::from(#expr)
}
quote_spanned! {expr.span()=> {
let mut name = ::std::borrow::Cow::<'static, str>::from(#expr);
if !name.bytes().all(|b| b.is_ascii_alphanumeric()) {
::std::panic!("a dynamic tag returned a tag name containing non ASCII alphanumerics: `{}`", name);
}
// convert to lowercase because the runtime checks rely on it.
name.to_mut().make_ascii_lowercase();
name
}}
}
};

Expand Down Expand Up @@ -192,6 +200,37 @@ impl ToTokens for HtmlTag {
}}
});

// These are the runtime-checks exclusive to dynamic tags.
// For literal tags this is already done at compile-time.
let dyn_tag_runtime_checks = if matches!(&tag_name, TagName::Expr(_)) {
// when Span::source_file Span::start get stabilised or yew-macro introduces a nightly feature flag
// we should expand the panic message to contain the exact location of the dynamic tag.
Some(quote! {
// check void element
if !#vtag.children.is_empty() {
match #vtag.tag() {
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
| "meta" | "param" | "source" | "track" | "wbr" => {
::std::panic!("a dynamic tag tried to create a `<{0}>` tag with children. `<{0}>` is a void element which can't have any children.", #vtag.tag());
}
_ => {}
}
}

// handle special attribute value
match #vtag.tag() {
"input" | "textarea" => {}
_ => {
if let ::std::option::Option::Some(value) = #vtag.value.take() {
#vtag.attributes.insert("value".to_string(), value);
}
}
}
})
} else {
None
};

tokens.extend(quote! {{
let mut #vtag = ::yew::virtual_dom::VTag::new(#name);
#(#set_kind)*
Expand All @@ -205,6 +244,7 @@ impl ToTokens for HtmlTag {
#vtag.add_attributes(vec![#(#attr_pairs),*]);
#vtag.add_listeners(vec![#(::std::rc::Rc::new(#listeners)),*]);
#vtag.add_children(vec![#(#children),*]);
#dyn_tag_runtime_checks
::yew::virtual_dom::VNode::from(#vtag)
}});
}
Expand Down Expand Up @@ -315,8 +355,10 @@ impl PeekValue<TagKey> for HtmlTagOpen {

let (tag_key, cursor) = TagName::peek(cursor)?;
if let TagKey::Lit(name) = &tag_key {
// Avoid parsing `<key=[...]>` as an HtmlTag. It needs to be parsed as an HtmlList.
if name.to_string() == "key" {
let (punct, _) = cursor.punct()?;
// ... unless it isn't followed by a '='. `<key></key>` is a valid HtmlTag!
(punct.as_char() != '=').as_option()?;
} else {
non_capitalized_ascii(&name.to_string()).as_option()?;
Expand All @@ -337,6 +379,7 @@ impl Parse for HtmlTagOpen {
match &tag_name {
TagName::Lit(name) => {
// Don't treat value as special for non input / textarea fields
// For dynamic tags this is done at runtime!
match name.to_string().as_str() {
"input" | "textarea" => {}
_ => {
Expand Down
24 changes: 24 additions & 0 deletions yew-macro/tests/macro_test.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use yew::html;

#[allow(dead_code)]
#[rustversion::attr(stable(1.43), test)]
fn tests() {
Expand All @@ -22,3 +24,25 @@ fn tests() {
t.pass("tests/macro/html-tag-pass.rs");
t.compile_fail("tests/macro/html-tag-fail.rs");
}

#[test]
#[should_panic(
expected = "a dynamic tag tried to create a `<br>` tag with children. `<br>` is a void element which can't have any children."
)]
fn dynamic_tags_catch_void_elements() {
html! {
<@{"br"}>
<span>{ "No children allowed" }</span>
</@>
};
}

#[test]
#[should_panic(
expected = "a dynamic tag returned a tag name containing non ASCII alphanumerics: `❤`"
)]
fn dynamic_tags_catch_non_ascii() {
html! {
<@{"❤"}/>
};
}
47 changes: 47 additions & 0 deletions yew/src/virtual_dom/vtag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1195,4 +1195,51 @@ mod tests {
expected
);
}

#[test]
fn dynamic_tags_work() {
let scope = test_scope();
let parent = document().create_element("div").unwrap();

#[cfg(feature = "std_web")]
document().body().unwrap().append_child(&parent);
#[cfg(feature = "web_sys")]
document().body().unwrap().append_child(&parent).unwrap();

let mut elem = html! { <@{"a"}/> };
elem.apply(&scope, &parent, None, None);
let vtag = assert_vtag(&mut elem);
// make sure the new tag name is used.
// Element.tagName is always in the canonical upper-case form.
assert_eq!(vtag.reference.as_ref().unwrap().tag_name(), "A");
}

#[test]
fn dynamic_tags_handle_value_attribute() {
let mut div_el = html! {
<@{"div"} value="Hello"/>
};
let div_vtag = assert_vtag(&mut div_el);
assert!(div_vtag.value.is_none());
assert_eq!(
div_vtag.attributes.get("value").map(String::as_str),
Some("Hello")
);

let mut input_el = html! {
<@{"input"} value="World"/>
};
let input_vtag = assert_vtag(&mut input_el);
assert_eq!(input_vtag.value, Some("World".to_string()));
assert!(!input_vtag.attributes.contains_key("value"));
}

#[test]
fn dynamic_tags_handle_weird_capitalisation() {
let mut el = html! {
<@{"tExTAREa"}/>
};
let vtag = assert_vtag(&mut el);
assert_eq!(vtag.tag(), "textarea");
}
}

0 comments on commit f37ebd3

Please sign in to comment.