~repos /tide-jsx

#rust#proc-macro#jsx

git clone https://pyrossh.dev/repos/tide-jsx.git

Tide + JSX


005fdc1f Gal Schlezinger

6 years ago
Support dashes in html tags attributes (#7)
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(syn::Ident),
9
+ Punned(AttributeKey),
7
- WithValue(syn::Ident, syn::Block),
10
+ WithValue(AttributeKey, syn::Block),
8
11
  }
9
12
 
10
13
  impl ElementAttribute {
11
- pub fn ident(&self) -> &syn::Ident {
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
- self.ident() == other.ident()
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(self.ident(), state)
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 = input.parse::<syn::Ident>()?;
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
- attribute.ident()
64
+ quote!(#ident)
44
65
  );
45
- attribute
46
- .ident()
47
- .span()
48
- .unwrap()
49
- .warning(error_message)
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 ident = attribute.ident();
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(stringify!(#ident), #value);
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
- /// ### Values are always surrounded by curly braces
108
+ /// ### Punning is not supported for dashed-delimited attributes
72
109
  ///
73
- /// ```rust
110
+ /// ```compile_fail
74
111
  /// # #![feature(proc_macro_hygiene)]
75
112
  /// # use render_macros::html;
76
- /// # use pretty_assertions::assert_eq;
113
+ ///
77
114
  /// let rendered = html! {
78
- /// <div id={"main"} />
115
+ /// <div this-wont-work />
79
116
  /// };
80
117
  ///
81
- /// assert_eq!(rendered, r#"<div id="main" />"#);
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
- syn::parse_str::<syn::Path>("::render::Fragment").unwrap()
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 = input.parse::<ElementAttributes>()?;
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;