diff --git a/crates/html-macro-test/src/tests/all_tests.rs b/crates/html-macro-test/src/tests/all_tests.rs
index dcbac239..9b6ff367 100644
--- a/crates/html-macro-test/src/tests/all_tests.rs
+++ b/crates/html-macro-test/src/tests/all_tests.rs
@@ -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 {
@@ -28,7 +28,7 @@ fn empty_div() {
generated: html! {
@@ -165,7 +164,7 @@ fn block_root() {
// #[test]
// fn punctuation_token() {
// let text = "Hello, World";
-//
+//
// HtmlMacroTest {
// generated: html! { Hello, World },
// expected: VirtualNode::text(text),
@@ -184,22 +183,29 @@ fn vec_of_nodes() {
generated: html! {
{ children }
},
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! {
}
- ;
- html! { }
- ;
- html! {
Username: }
- ;
- html! {
}
- ;
- html! {
}
- ;
+ html! {
};
+ html! { };
+ html! {
Username: };
+ html! {
};
+ html! {
};
+}
+
+/// Verify that we can use an attribute name that contains a hyphen.
+#[test]
+fn hyphenated_attribute() {
+ let element: VirtualNode = html! {
+
+ };
+ 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
@@ -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 {
@@ -231,7 +237,7 @@ fn self_closing_tag_without_backslash() {
},
expected: expected.into(),
}
- .test();
+ .test();
}
/// Verify that our self closing tags work with backslashes
@@ -243,7 +249,7 @@ fn self_closing_tag_with_backslace() {
},
expected: VirtualNode::element("br"),
}
- .test();
+ .test();
}
#[test]
@@ -262,7 +268,7 @@ fn if_true_block() {
},
expected: expected.into(),
}
- .test();
+ .test();
}
#[test]
@@ -285,7 +291,7 @@ fn if_false_block() {
},
expected: expected.into(),
}
- .test();
+ .test();
}
#[test]
@@ -301,7 +307,7 @@ fn single_branch_if_true_block() {
},
expected: expected.into(),
}
- .test();
+ .test();
}
#[test]
@@ -317,7 +323,7 @@ fn single_branch_if_false_block() {
},
expected: expected.into(),
}
- .test();
+ .test();
}
#[test]
@@ -345,7 +351,7 @@ fn custom_component_props() {
},
expected: expected.into(),
}
- .test();
+ .test();
}
#[test]
@@ -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.
@@ -387,7 +393,7 @@ fn space_before_and_after_empty_list() {
generated: html! {
{elements}
},
expected: html! {
},
}
- .test()
+ .test()
}
/// Verify that an Option::None virtual node gets ignored.
@@ -399,7 +405,7 @@ fn option_none() {
generated: html! {
{element}
},
expected: html! {
},
}
- .test()
+ .test()
}
/// Verify that an Some(VirtualNode) gets rendered.
@@ -411,7 +417,7 @@ fn option_some() {
generated: html! {
{element}
},
expected: html! {
},
}
- .test()
+ .test()
}
/// Verify that our macro to generate IterableNodes implementations for numbers works.
@@ -424,5 +430,5 @@ fn numbers() {
generated: html! {
{num} {ref_num}
},
expected: html! {
{"3"} {"4"}
},
}
- .test()
+ .test()
}
diff --git a/crates/html-macro/src/parser/open_tag.rs b/crates/html-macro/src/parser/open_tag.rs
index 71f0e238..3ccafe4e 100644
--- a/crates/html-macro/src/parser/open_tag.rs
+++ b/crates/html-macro/src/parser/open_tag.rs
@@ -90,12 +90,12 @@ fn create_valid_node(
// html! {
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) => {
@@ -151,8 +151,8 @@ fn component_node(
let component_props: Vec
= 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,
diff --git a/crates/html-macro/src/parser/open_tag/event.rs b/crates/html-macro/src/parser/open_tag/event.rs
index 20e2c760..02e1a7b5 100644
--- a/crates/html-macro/src/parser/open_tag/event.rs
+++ b/crates/html-macro/src/parser/open_tag/event.rs
@@ -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" {
diff --git a/crates/html-macro/src/tag.rs b/crates/html-macro/src/tag.rs
index 7a79fcca..825ddad9 100644
--- a/crates/html-macro/src/tag.rs
+++ b/crates/html-macro/src/tag.rs
@@ -1,4 +1,5 @@
use proc_macro2::{Span, TokenStream, TokenTree};
+use quote::{quote_spanned, ToTokens};
use syn::parse::{Parse, ParseStream, Result};
use syn::spanned::Spanned;
use syn::token::Brace;
@@ -69,8 +70,28 @@ pub enum TagKind {
/// etc...
#[derive(Debug)]
pub struct Attr {
- pub key: Ident,
- pub value: Expr,
+ key: TokenStream,
+ key_span: Span,
+ value: Expr,
+}
+impl Attr {
+ /// Get the attribute's key as a String.
+ ///
+ /// If the attribute's tokens were `quote!{ http - equiv }`
+ /// the string will be "http-equiv".
+ pub fn key_string(&self) -> String {
+ self.key.to_string().replace(" ", "")
+ }
+
+ /// Get the span for the attribute's key.
+ pub fn key_span(&self) -> Span {
+ self.key_span
+ }
+
+ /// Get the attribute's value, such as the "refresh" in `
.
+ pub fn value(&self) -> &Expr {
+ &self.value
+ }
}
impl Parse for Tag {
@@ -141,27 +162,7 @@ fn parse_attributes(input: &mut ParseStream) -> Result> {
|| input.peek(Token![loop])
|| input.peek(Token![type])
{
- // = input.parse()?;
- let maybe_async_key: Option = input.parse()?;
- let maybe_for_key: Option = input.parse()?;
- let maybe_loop_key: Option = input.parse()?;
- let maybe_type_key: Option = input.parse()?;
-
- let key = if maybe_as_key.is_some() {
- Ident::new("as", maybe_as_key.unwrap().span())
- } else if maybe_async_key.is_some() {
- Ident::new("async", maybe_async_key.unwrap().span())
- } else if maybe_for_key.is_some() {
- Ident::new("for", maybe_for_key.unwrap().span())
- } else if maybe_loop_key.is_some() {
- Ident::new("loop", maybe_loop_key.unwrap().span())
- } else if maybe_type_key.is_some() {
- Ident::new("type", maybe_type_key.unwrap().span())
- } else {
- input.parse()?
- };
+ let (key, key_span) = parse_attribute_key(input)?;
// =
input.parse::()?;
@@ -192,12 +193,75 @@ fn parse_attributes(input: &mut ParseStream) -> Result> {
let value: Expr = syn::parse2(value_tokens)?;
- attrs.push(Attr { key, value });
+ attrs.push(Attr {
+ key,
+ key_span,
+ value,
+ });
}
Ok(attrs)
}
+/// Parse an attribute key such as the "http-equiv" in
+/// ` `
+fn parse_attribute_key(input: &mut ParseStream) -> Result<(TokenStream, Span)> {
+ let first_key_segment = parse_attribute_key_segment(input)?;
+
+ let maybe_hyphen: Option = input.parse()?;
+
+ let attribute_key;
+ if let Some(hyphen) = maybe_hyphen {
+ let next_segment = parse_attribute_key_segment(input)?;
+
+ let combined_span = first_key_segment
+ .span()
+ .join(hyphen.span())
+ .unwrap()
+ .join(next_segment.span())
+ .unwrap();
+
+ attribute_key = (
+ quote_spanned! {combined_span=> #first_key_segment - #next_segment },
+ combined_span,
+ );
+ } else {
+ attribute_key = (
+ first_key_segment.to_token_stream(),
+ first_key_segment.span(),
+ );
+ }
+
+ Ok(attribute_key)
+}
+
+/// Parse a segment within an attribute key, such as the "http" or "equiv" in
+/// ` `
+fn parse_attribute_key_segment(input: &mut ParseStream) -> Result {
+ // = input.parse()?;
+ let maybe_async_key: Option = input.parse()?;
+ let maybe_for_key: Option = input.parse()?;
+ let maybe_loop_key: Option = input.parse()?;
+ let maybe_type_key: Option = input.parse()?;
+
+ let key = if let Some(as_key) = maybe_as_key {
+ Ident::new("as", as_key.span())
+ } else if let Some(async_key) = maybe_async_key {
+ Ident::new("async", async_key.span())
+ } else if let Some(for_key) = maybe_for_key {
+ Ident::new("for", for_key.span())
+ } else if let Some(loop_key) = maybe_loop_key {
+ Ident::new("loop", loop_key.span())
+ } else if let Some(type_key) = maybe_type_key {
+ Ident::new("type", type_key.span())
+ } else {
+ input.parse()?
+ };
+ Ok(key)
+}
+
///
fn parse_close_tag(input: &mut ParseStream, first_angle_bracket_span: Span) -> Result