005fdc1f
—
Gal Schlezinger 6 years ago
Support dashes in html tags attributes (#7)
- render/src/lib.rs +1 -1
- render_macros/src/element_attribute.rs +54 -6
- render_macros/src/element_attributes.rs +29 -9
- render_macros/src/lib.rs +42 -5
- render_macros/src/tags.rs +17 -6
- render_tests/src/lib.rs +9 -1
render/src/lib.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//! > 🔏 A safe and simple template engine with the ergonomics of JSX
|
|
2
|
-
//!
|
|
2
|
+
//!
|
|
3
3
|
//! `render` itself is a combination of traits, structs and macros that together unify and
|
|
4
4
|
//! boost the experience of composing tree-shaped data structures. This works best with HTML and
|
|
5
5
|
//! XML rendering, but can work with other usages as well, like ReasonML's [`Pastel`](https://reason-native.com/docs/pastel/) library for terminal colors.
|
render_macros/src/element_attribute.rs
CHANGED
|
@@ -1,30 +1,77 @@
|
|
|
1
1
|
use quote::quote;
|
|
2
2
|
use std::hash::{Hash, Hasher};
|
|
3
3
|
use syn::parse::{Parse, ParseStream, Result};
|
|
4
|
+
use syn::spanned::Spanned;
|
|
5
|
+
|
|
6
|
+
pub type AttributeKey = syn::punctuated::Punctuated<syn::Ident, syn::Token![-]>;
|
|
4
7
|
|
|
5
8
|
pub enum ElementAttribute {
|
|
6
|
-
Punned(
|
|
9
|
+
Punned(AttributeKey),
|
|
7
|
-
WithValue(
|
|
10
|
+
WithValue(AttributeKey, syn::Block),
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
impl ElementAttribute {
|
|
11
|
-
pub fn ident(&self) -> &
|
|
14
|
+
pub fn ident(&self) -> &AttributeKey {
|
|
12
15
|
match self {
|
|
13
16
|
Self::Punned(ident) | Self::WithValue(ident, _) => ident,
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
pub fn idents(&self) -> Vec<&syn::Ident> {
|
|
21
|
+
self.ident().iter().collect::<Vec<_>>()
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
pub fn value_tokens(&self) -> proc_macro2::TokenStream {
|
|
18
25
|
match self {
|
|
19
26
|
Self::WithValue(_, value) => quote!(#value),
|
|
20
27
|
Self::Punned(ident) => quote!(#ident),
|
|
21
28
|
}
|
|
22
29
|
}
|
|
30
|
+
|
|
31
|
+
pub fn validate(self, is_custom_element: bool) -> Result<Self> {
|
|
32
|
+
if is_custom_element {
|
|
33
|
+
self.validate_for_custom_element()
|
|
34
|
+
} else {
|
|
35
|
+
self.validate_for_simple_element()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn validate_for_custom_element(self) -> Result<Self> {
|
|
40
|
+
if self.idents().len() < 2 {
|
|
41
|
+
Ok(self)
|
|
42
|
+
} else {
|
|
43
|
+
let alternative_name = self
|
|
44
|
+
.idents()
|
|
45
|
+
.iter()
|
|
46
|
+
.map(|x| x.to_string())
|
|
47
|
+
.collect::<Vec<_>>()
|
|
48
|
+
.join("_");
|
|
49
|
+
|
|
50
|
+
let error_message = format!(
|
|
51
|
+
"Can't use dash-delimited values on custom components. Did you mean `{}`?",
|
|
52
|
+
alternative_name
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
Err(syn::Error::new(self.ident().span(), error_message))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub fn validate_for_simple_element(self) -> Result<Self> {
|
|
60
|
+
match (&self, self.idents().len()) {
|
|
61
|
+
(Self::Punned(ref key), len) if len > 1 => {
|
|
62
|
+
let error_message = "Can't use punning with dash-delimited values";
|
|
63
|
+
Err(syn::Error::new(key.span(), error_message))
|
|
64
|
+
}
|
|
65
|
+
_ => Ok(self),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
23
68
|
}
|
|
24
69
|
|
|
25
70
|
impl PartialEq for ElementAttribute {
|
|
26
71
|
fn eq(&self, other: &Self) -> bool {
|
|
72
|
+
let self_idents: Vec<_> = self.ident().iter().collect();
|
|
73
|
+
let other_idents: Vec<_> = other.ident().iter().collect();
|
|
27
|
-
|
|
74
|
+
self_idents == other_idents
|
|
28
75
|
}
|
|
29
76
|
}
|
|
30
77
|
|
|
@@ -32,13 +79,14 @@ impl Eq for ElementAttribute {}
|
|
|
32
79
|
|
|
33
80
|
impl Hash for ElementAttribute {
|
|
34
81
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
82
|
+
let ident = self.idents();
|
|
35
|
-
Hash::hash(
|
|
83
|
+
Hash::hash(&ident, state)
|
|
36
84
|
}
|
|
37
85
|
}
|
|
38
86
|
|
|
39
87
|
impl Parse for ElementAttribute {
|
|
40
88
|
fn parse(input: ParseStream) -> Result<Self> {
|
|
41
|
-
let name =
|
|
89
|
+
let name = AttributeKey::parse_separated_nonempty(input)?;
|
|
42
90
|
let not_punned = input.peek(syn::Token![=]);
|
|
43
91
|
|
|
44
92
|
if !not_punned {
|
render_macros/src/element_attributes.rs
CHANGED
|
@@ -3,9 +3,11 @@ use crate::element_attribute::ElementAttribute;
|
|
|
3
3
|
use quote::{quote, ToTokens};
|
|
4
4
|
use std::collections::HashSet;
|
|
5
5
|
use syn::parse::{Parse, ParseStream, Result};
|
|
6
|
+
use syn::spanned::Spanned;
|
|
6
7
|
|
|
7
8
|
pub type Attributes = HashSet<ElementAttribute>;
|
|
8
9
|
|
|
10
|
+
#[derive(Default)]
|
|
9
11
|
pub struct ElementAttributes {
|
|
10
12
|
pub attributes: Attributes,
|
|
11
13
|
}
|
|
@@ -30,6 +32,24 @@ impl ElementAttributes {
|
|
|
30
32
|
attributes: &self.attributes,
|
|
31
33
|
}
|
|
32
34
|
}
|
|
35
|
+
|
|
36
|
+
pub fn parse(input: ParseStream, is_custom_element: bool) -> Result<Self> {
|
|
37
|
+
let mut parsed_self = input.parse::<Self>()?;
|
|
38
|
+
|
|
39
|
+
let new_attributes: Attributes = parsed_self
|
|
40
|
+
.attributes
|
|
41
|
+
.drain()
|
|
42
|
+
.filter_map(|attribute| match attribute.validate(is_custom_element) {
|
|
43
|
+
Ok(x) => Some(x),
|
|
44
|
+
Err(err) => {
|
|
45
|
+
err.span().unwrap().error(err.to_string()).emit();
|
|
46
|
+
None
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.collect();
|
|
50
|
+
|
|
51
|
+
Ok(ElementAttributes::new(new_attributes))
|
|
52
|
+
}
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
impl Parse for ElementAttributes {
|
|
@@ -37,17 +57,13 @@ impl Parse for ElementAttributes {
|
|
|
37
57
|
let mut attributes: HashSet<ElementAttribute> = HashSet::new();
|
|
38
58
|
while input.peek(syn::Ident) {
|
|
39
59
|
if let Ok(attribute) = input.parse::<ElementAttribute>() {
|
|
60
|
+
let ident = attribute.ident();
|
|
40
61
|
if attributes.contains(&attribute) {
|
|
41
62
|
let error_message = format!(
|
|
42
63
|
"There is a previous definition of the {} attribute",
|
|
43
|
-
|
|
64
|
+
quote!(#ident)
|
|
44
65
|
);
|
|
45
|
-
attribute
|
|
46
|
-
.ident()
|
|
47
|
-
.span()
|
|
48
|
-
.unwrap()
|
|
49
|
-
|
|
66
|
+
ident.span().unwrap().warning(error_message).emit();
|
|
50
|
-
.emit();
|
|
51
67
|
}
|
|
52
68
|
attributes.insert(attribute);
|
|
53
69
|
}
|
|
@@ -106,11 +122,15 @@ impl<'a> ToTokens for SimpleElementAttributes<'a> {
|
|
|
106
122
|
.attributes
|
|
107
123
|
.iter()
|
|
108
124
|
.map(|attribute| {
|
|
109
|
-
let
|
|
125
|
+
let mut iter = attribute.ident().iter();
|
|
126
|
+
let first_word = iter.next().unwrap();
|
|
127
|
+
let ident = iter.fold(first_word.to_string(), |acc, curr| {
|
|
128
|
+
format!("{}-{}", acc, curr)
|
|
129
|
+
});
|
|
110
130
|
let value = attribute.value_tokens();
|
|
111
131
|
|
|
112
132
|
quote! {
|
|
113
|
-
hm.insert(
|
|
133
|
+
hm.insert(#ident, #value);
|
|
114
134
|
}
|
|
115
135
|
})
|
|
116
136
|
.collect();
|
render_macros/src/lib.rs
CHANGED
|
@@ -51,6 +51,43 @@ use syn::parse_macro_input;
|
|
|
51
51
|
/// assert_eq!(rendered, r#"<h1>Hello world!</h1>"#);
|
|
52
52
|
/// ```
|
|
53
53
|
///
|
|
54
|
+
/// ### Values are always surrounded by curly braces
|
|
55
|
+
///
|
|
56
|
+
/// ```rust
|
|
57
|
+
/// # #![feature(proc_macro_hygiene)]
|
|
58
|
+
/// # use render_macros::html;
|
|
59
|
+
/// # use pretty_assertions::assert_eq;
|
|
60
|
+
/// let rendered = html! {
|
|
61
|
+
/// <div id={"main"} />
|
|
62
|
+
/// };
|
|
63
|
+
///
|
|
64
|
+
/// assert_eq!(rendered, r#"<div id="main" />"#);
|
|
65
|
+
/// ```
|
|
66
|
+
///
|
|
67
|
+
/// ### HTML entities can accept dashed-separated value
|
|
68
|
+
///
|
|
69
|
+
/// ```rust
|
|
70
|
+
/// # #![feature(proc_macro_hygiene)]
|
|
71
|
+
/// # use render_macros::html;
|
|
72
|
+
/// # use pretty_assertions::assert_eq;
|
|
73
|
+
/// let rendered = html! {
|
|
74
|
+
/// <div data-testid={"some test id"} />
|
|
75
|
+
/// };
|
|
76
|
+
///
|
|
77
|
+
/// assert_eq!(rendered, r#"<div data-testid="some test id" />"#);
|
|
78
|
+
/// ```
|
|
79
|
+
///
|
|
80
|
+
/// ### Custom components can't accept dashed-separated values
|
|
81
|
+
///
|
|
82
|
+
/// ```compile_fail
|
|
83
|
+
/// # #![feature(proc_macro_hygiene)]
|
|
84
|
+
/// # use render_macros::html;
|
|
85
|
+
/// // This will fail the compilation:
|
|
86
|
+
/// let rendered = html! {
|
|
87
|
+
/// <MyElement data-testid={"some test id"} />
|
|
88
|
+
/// };
|
|
89
|
+
/// ```
|
|
90
|
+
///
|
|
54
91
|
/// ### Punning is supported
|
|
55
92
|
/// but instead of expanding to `value={true}`, it expands to
|
|
56
93
|
/// `value={value}` like Rust's punning
|
|
@@ -68,17 +105,17 @@ use syn::parse_macro_input;
|
|
|
68
105
|
/// assert_eq!(rendered, r#"<div class="some_class" />"#);
|
|
69
106
|
/// ```
|
|
70
107
|
///
|
|
71
|
-
/// ###
|
|
108
|
+
/// ### Punning is not supported for dashed-delimited attributes
|
|
72
109
|
///
|
|
73
|
-
/// ```
|
|
110
|
+
/// ```compile_fail
|
|
74
111
|
/// # #![feature(proc_macro_hygiene)]
|
|
75
112
|
/// # use render_macros::html;
|
|
76
|
-
///
|
|
113
|
+
///
|
|
77
114
|
/// let rendered = html! {
|
|
78
|
-
/// <div
|
|
115
|
+
/// <div this-wont-work />
|
|
79
116
|
/// };
|
|
80
117
|
///
|
|
81
|
-
/// assert_eq!(rendered, r#"<div
|
|
118
|
+
/// assert_eq!(rendered, r#"<div class="some_class" />"#);
|
|
82
119
|
/// ```
|
|
83
120
|
#[proc_macro]
|
|
84
121
|
pub fn html(input: TokenStream) -> TokenStream {
|
render_macros/src/tags.rs
CHANGED
|
@@ -7,28 +7,39 @@ pub struct OpenTag {
|
|
|
7
7
|
pub name: syn::Path,
|
|
8
8
|
pub attributes: ElementAttributes,
|
|
9
9
|
pub self_closing: bool,
|
|
10
|
+
pub is_custom_element: bool,
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
fn name_or_fragment(maybe_name: Result<syn::Path>) -> syn::Path {
|
|
13
|
-
maybe_name.unwrap_or_else(|_| {
|
|
14
|
-
|
|
14
|
+
maybe_name.unwrap_or_else(|_| syn::parse_str::<syn::Path>("::render::Fragment").unwrap())
|
|
15
|
-
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn is_custom_element_name(path: &syn::Path) -> bool {
|
|
18
|
+
match path.get_ident() {
|
|
19
|
+
None => true,
|
|
20
|
+
Some(ident) => {
|
|
21
|
+
let name = ident.to_string();
|
|
22
|
+
let first_letter = name.get(0..1).unwrap();
|
|
23
|
+
first_letter.to_uppercase() == first_letter
|
|
24
|
+
}
|
|
25
|
+
}
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
impl Parse for OpenTag {
|
|
19
29
|
fn parse(input: ParseStream) -> Result<Self> {
|
|
20
30
|
input.parse::<syn::Token![<]>()?;
|
|
21
31
|
let maybe_name = syn::Path::parse_mod_style(input);
|
|
32
|
+
let name = name_or_fragment(maybe_name);
|
|
33
|
+
let is_custom_element = is_custom_element_name(&name);
|
|
22
|
-
let attributes =
|
|
34
|
+
let attributes = ElementAttributes::parse(input, is_custom_element)?;
|
|
23
35
|
let self_closing = input.parse::<syn::Token![/]>().is_ok();
|
|
24
36
|
input.parse::<syn::Token![>]>()?;
|
|
25
37
|
|
|
26
|
-
let name = name_or_fragment(maybe_name);
|
|
27
|
-
|
|
28
38
|
Ok(Self {
|
|
29
39
|
name,
|
|
30
40
|
attributes,
|
|
31
41
|
self_closing,
|
|
42
|
+
is_custom_element,
|
|
32
43
|
})
|
|
33
44
|
}
|
|
34
45
|
}
|
render_tests/src/lib.rs
CHANGED
|
@@ -32,7 +32,7 @@ pub fn it_works() -> String {
|
|
|
32
32
|
<>
|
|
33
33
|
<HTML5Doctype />
|
|
34
34
|
<Hello world yes={1 + 1}>
|
|
35
|
-
<div>{format!("HEY!")}</div>
|
|
35
|
+
<div data-testid={"hey"} hello={"hello"}>{format!("HEY!")}</div>
|
|
36
36
|
{other_value}
|
|
37
37
|
</Hello>
|
|
38
38
|
</>
|
|
@@ -64,6 +64,14 @@ pub fn verify_works() {
|
|
|
64
64
|
println!("{}", it_works());
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
#[test]
|
|
68
|
+
pub fn works_with_dashes() {
|
|
69
|
+
use pretty_assertions::assert_eq;
|
|
70
|
+
|
|
71
|
+
let value = html! { <div data-id={"some id"} /> };
|
|
72
|
+
assert_eq!(value, r#"<div data-id="some id" />"#);
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
#[test]
|
|
68
76
|
pub fn works_with_raw() {
|
|
69
77
|
use pretty_assertions::assert_eq;
|