Skip to content

Commit

Permalink
html! macro support hyphens in attributes
Browse files Browse the repository at this point in the history
This commit makes it possible to use hyphenated attribute names when
creating elements using the `html!` macro.

For example, the following is now possible:

```rust
html! {
<svg
  xmlns="http://www.w3.org/2000/svg"
  stroke-width="1.5"
  // ...
>
  // ...
</svg>
}
```

Before this commit trying to add a "stroke-width" attribute would have
led to a compile time error.
  • Loading branch information
chinedufn committed Jun 3, 2024
1 parent 1d26d9c commit de1e289
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 107 deletions.
84 changes: 45 additions & 39 deletions crates/html-macro-test/src/tests/all_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use html_macro::html;
use std::collections::HashMap;
use virtual_node::{IterableNodes, VElement, VText, View, VirtualNode};
use virtual_node::{AttributeValue, IterableNodes, VElement, VText, View, VirtualNode};

#[must_use]
pub(crate) struct HtmlMacroTest {
Expand All @@ -28,7 +28,7 @@ fn empty_div() {
generated: html! { <div></div> },
expected: VirtualNode::element("div"),
}
.test();
.test();
}

#[test]
Expand All @@ -43,7 +43,7 @@ fn one_attr() {
generated: html! { <div id="hello-world"></div> },
expected: expected.into(),
}
.test();
.test();
}

#[test]
Expand All @@ -55,7 +55,7 @@ fn child_node() {
generated: html! { <div><span></span></div> },
expected: expected.into(),
}
.test();
.test();
}

#[test]
Expand All @@ -67,7 +67,7 @@ fn sibling_child_nodes() {
generated: html! { <div><span></span><b></b></div> },
expected: expected.into(),
}
.test();
.test();
}

/// Nested 3 nodes deep
Expand All @@ -83,16 +83,15 @@ fn three_nodes_deep() {
generated: html! { <div><span><b></b></span></div> },
expected: expected.into(),
}
.test()
.test()
}


// TODO: Requires proc macro APIs that are currently unstable - https://github.com/rust-lang/rust/issues/54725
// #[test]
// fn sibling_text_nodes() {
// let mut expected = VElement::new("div");
// expected.children = vec![VirtualNode::text("This is a text node")];
//
//
// HtmlMacroTest {
// generated: html! { <div>This is a text node</div> },
// expected: expected.into(),
Expand All @@ -116,7 +115,7 @@ fn nested_macro() {
},
expected: expected.into(),
}
.test();
.test();
}

/// If the first thing we see is a block then we grab whatever is inside it.
Expand All @@ -132,21 +131,21 @@ fn block_root() {
},
expected,
}
.test();
.test();
}

// TODO: Requires proc macro APIs that are currently unstable - https://github.com/rust-lang/rust/issues/54725
// /// Text followed by a block
// #[test]
// fn text_next_to_block() {
// let child = html! { <ul></ul> };
//
//
// let mut expected = VElement::new("div");
// expected.children = vec![
// VirtualNode::text(" A bit of text "),
// VirtualNode::element("ul"),
// ];
//
//
// HtmlMacroTest {
// generated: html! {
// <div>
Expand All @@ -165,7 +164,7 @@ fn block_root() {
// #[test]
// fn punctuation_token() {
// let text = "Hello, World";
//
//
// HtmlMacroTest {
// generated: html! { Hello, World },
// expected: VirtualNode::text(text),
Expand All @@ -184,22 +183,29 @@ fn vec_of_nodes() {
generated: html! { <div> { children } </div> },
expected: expected.into(),
}
.test();
.test();
}

/// Just make sure that this compiles since as, async, for, loop, and type are keywords
#[test]
fn keyword_attribute() {
html! { <link rel="prefetch" href="/style.css" as="style" /> }
;
html! { <script src="/app.js" async="async" /> }
;
html! { <label for="username">Username:</label> }
;
html! { <audio loop="loop"><source src="/beep.mp3" type="audio/mpeg" /></audio> }
;
html! { <link rel="stylesheet" type="text/css" href="/app.css" /> }
;
html! { <link rel="prefetch" href="/style.css" as="style" /> };
html! { <script src="/app.js" async="async" /> };
html! { <label for="username">Username:</label> };
html! { <audio loop="loop"><source src="/beep.mp3" type="audio/mpeg" /></audio> };
html! { <link rel="stylesheet" type="text/css" href="/app.css" /> };
}

/// Verify that we can use an attribute name that contains a hyphen.
#[test]
fn hyphenated_attribute() {
let element: VirtualNode = html! {
<meta http-equiv="refresh"/>
};
assert_eq!(
element.as_velement_ref().unwrap().attrs.get("http-equiv"),
Some(&AttributeValue::String("refresh".to_string()))
);
}

/// For unquoted text apostrophes should be parsed correctly
Expand All @@ -217,9 +223,9 @@ fn self_closing_tag_without_backslash() {
"area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param", "command",
"keygen", "source",
]
.into_iter()
.map(|tag| VirtualNode::element(tag))
.collect();
.into_iter()
.map(|tag| VirtualNode::element(tag))
.collect();
expected.children = children;

HtmlMacroTest {
Expand All @@ -231,7 +237,7 @@ fn self_closing_tag_without_backslash() {
},
expected: expected.into(),
}
.test();
.test();
}

/// Verify that our self closing tags work with backslashes
Expand All @@ -243,7 +249,7 @@ fn self_closing_tag_with_backslace() {
},
expected: VirtualNode::element("br"),
}
.test();
.test();
}

#[test]
Expand All @@ -262,7 +268,7 @@ fn if_true_block() {
},
expected: expected.into(),
}
.test();
.test();
}

#[test]
Expand All @@ -285,7 +291,7 @@ fn if_false_block() {
},
expected: expected.into(),
}
.test();
.test();
}

#[test]
Expand All @@ -301,7 +307,7 @@ fn single_branch_if_true_block() {
},
expected: expected.into(),
}
.test();
.test();
}

#[test]
Expand All @@ -317,7 +323,7 @@ fn single_branch_if_false_block() {
},
expected: expected.into(),
}
.test();
.test();
}

#[test]
Expand Down Expand Up @@ -345,7 +351,7 @@ fn custom_component_props() {
},
expected: expected.into(),
}
.test();
.test();
}

#[test]
Expand Down Expand Up @@ -373,7 +379,7 @@ fn custom_component_children() {
},
expected: expected.into(),
}
.test();
.test();
}

/// Verify that we can properly render an empty list of virtual nodes that has a space after it.
Expand All @@ -387,7 +393,7 @@ fn space_before_and_after_empty_list() {
generated: html! {<div> {elements} </div>},
expected: html! {<div> </div>},
}
.test()
.test()
}

/// Verify that an Option::None virtual node gets ignored.
Expand All @@ -399,7 +405,7 @@ fn option_none() {
generated: html! {<div> {element} </div>},
expected: html! {<div> </div>},
}
.test()
.test()
}

/// Verify that an Some(VirtualNode) gets rendered.
Expand All @@ -411,7 +417,7 @@ fn option_some() {
generated: html! {<div> {element} </div>},
expected: html! {<div> <em></em> </div>},
}
.test()
.test()
}

/// Verify that our macro to generate IterableNodes implementations for numbers works.
Expand All @@ -424,5 +430,5 @@ fn numbers() {
generated: html! {<div> {num} {ref_num} </div>},
expected: html! {<div> {"3"} {"4"} </div>},
}
.test()
.test()
}
12 changes: 6 additions & 6 deletions crates/html-macro/src/parser/open_tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ fn create_valid_node(
// html! { <div key = "..." ></div>
let key_attr = attrs
.iter()
.find(|attr| attr.key.to_string() == "key")
.map(|attr| &attr.value);
.find(|attr| attr.key_string() == "key")
.map(|attr| attr.value());

for attr in attrs.iter() {
let key = format!("{}", attr.key);
let value = &attr.value;
let key = attr.key_string();
let value = attr.value();

match value {
Expr::Closure(closure) => {
Expand Down Expand Up @@ -151,8 +151,8 @@ fn component_node(
let component_props: Vec<proc_macro2::TokenStream> = attrs
.into_iter()
.map(|attr| {
let key = Ident::new(format!("{}", attr.key).as_str(), name.span());
let value = &attr.value;
let key = Ident::new(attr.key_string().as_str(), name.span());
let value = attr.value();

quote! {
#key: #value,
Expand Down
4 changes: 2 additions & 2 deletions crates/html-macro/src/parser/open_tag/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ pub(super) fn insert_closure_tokens(
key_attr_value: Option<&Expr>,
) -> TokenStream {
let arg_count = closure.inputs.len();
let event_name = event_attribute.key.to_string();
let event_name = event_attribute.key_string();

let attr_key_span = &event_attribute.key.span();
let attr_key_span = &event_attribute.key_span();

// TODO: Refactor duplicate code between these blocks.
if event_name == "on_create_element" {
Expand Down
Loading

0 comments on commit de1e289

Please sign in to comment.