tide-jsx v0.4.0

#rust#proc-macro#jsx

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

Tide + JSX


cc58dd1Gal Schlezinger 2019-09-21T08:30:36+03:00
initial implementation
.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ target
2
+ **/*.rs.bk
Cargo.lock ADDED
@@ -0,0 +1,61 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ [[package]]
4
+ name = "proc-macro2"
5
+ version = "1.0.3"
6
+ source = "registry+https://github.com/rust-lang/crates.io-index"
7
+ dependencies = [
8
+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
9
+ ]
10
+
11
+ [[package]]
12
+ name = "quote"
13
+ version = "1.0.2"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ dependencies = [
16
+ "proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
17
+ ]
18
+
19
+ [[package]]
20
+ name = "render"
21
+ version = "0.1.0"
22
+ dependencies = [
23
+ "render_macros 0.1.0",
24
+ ]
25
+
26
+ [[package]]
27
+ name = "render_macros"
28
+ version = "0.1.0"
29
+ dependencies = [
30
+ "proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
31
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
32
+ "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
33
+ ]
34
+
35
+ [[package]]
36
+ name = "render_tests"
37
+ version = "0.1.0"
38
+ dependencies = [
39
+ "render 0.1.0",
40
+ ]
41
+
42
+ [[package]]
43
+ name = "syn"
44
+ version = "1.0.5"
45
+ source = "registry+https://github.com/rust-lang/crates.io-index"
46
+ dependencies = [
47
+ "proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
48
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
49
+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
50
+ ]
51
+
52
+ [[package]]
53
+ name = "unicode-xid"
54
+ version = "0.2.0"
55
+ source = "registry+https://github.com/rust-lang/crates.io-index"
56
+
57
+ [metadata]
58
+ "checksum proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e98a83a9f9b331f54b924e68a66acb1bb35cb01fb0a23645139967abefb697e8"
59
+ "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
60
+ "checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf"
61
+ "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
Cargo.toml ADDED
@@ -0,0 +1,7 @@
1
+ [workspace]
2
+
3
+ members = [
4
+ "render",
5
+ "render_macros",
6
+ "render_tests",
7
+ ]
render/Cargo.toml ADDED
@@ -0,0 +1,10 @@
1
+ [package]
2
+ name = "render"
3
+ version = "0.1.0"
4
+ authors = ["Gal Schlezinger <[email protected]>"]
5
+ edition = "2018"
6
+
7
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8
+
9
+ [dependencies]
10
+ render_macros = { path = "../render_macros" }
render/src/fragment.rs ADDED
@@ -0,0 +1,28 @@
1
+ use crate::Renderable;
2
+
3
+ /// A top-level root component to combine a same-level components
4
+ /// in a RSX fashion
5
+ ///
6
+ /// ```rust
7
+ /// # #![feature(proc_macro_hygiene)]
8
+ /// # use render::html::HTML5Doctype;
9
+ /// # use render_macros::html;
10
+ /// # use render::fragment::Fragment;
11
+ /// let result = html! {
12
+ /// <Fragment>
13
+ /// <a />
14
+ /// <b />
15
+ /// </Fragment>
16
+ /// };
17
+ /// assert_eq!(result, "<a /><b />");
18
+ /// ```
19
+ #[derive(Debug)]
20
+ pub struct Fragment<T: Renderable> {
21
+ pub children: T,
22
+ }
23
+
24
+ impl<T: Renderable> Renderable for Fragment<T> {
25
+ fn render(self) -> String {
26
+ self.children.render()
27
+ }
28
+ }
render/src/html.rs ADDED
@@ -0,0 +1,28 @@
1
+ use crate::Renderable;
2
+
3
+ /// HTML 5 doctype declaration
4
+ ///
5
+ /// ```rust
6
+ /// # #![feature(proc_macro_hygiene)]
7
+ /// # use render::html::HTML5Doctype;
8
+ /// # use render::html;
9
+ /// # use render::fragment::Fragment;
10
+ /// # let result =
11
+ /// html! {
12
+ /// <Fragment>
13
+ /// <HTML5Doctype />
14
+ /// <html>
15
+ /// <body />
16
+ /// </html>
17
+ /// </Fragment>
18
+ /// };
19
+ /// # assert_eq!(result, "<!DOCTYPE html><html><body /></html>");
20
+ /// ```
21
+ #[derive(Debug)]
22
+ pub struct HTML5Doctype;
23
+
24
+ impl Renderable for HTML5Doctype {
25
+ fn render(self) -> String {
26
+ "<!DOCTYPE html>".to_string()
27
+ }
28
+ }
render/src/lib.rs ADDED
@@ -0,0 +1,10 @@
1
+ pub mod fragment;
2
+ pub mod html;
3
+ mod renderable;
4
+ mod simple_element;
5
+ mod text_element;
6
+
7
+ pub use fragment::Fragment;
8
+ pub use renderable::Renderable;
9
+ pub use render_macros::{html, rsx};
10
+ pub use simple_element::SimpleElement;
render/src/renderable.rs ADDED
@@ -0,0 +1,41 @@
1
+ /// A renderable component
2
+ pub trait Renderable: core::fmt::Debug + Sized {
3
+ /// Render the component to the HTML representation.
4
+ ///
5
+ /// Mostly done using the `html!` macro to generate strings
6
+ /// by composing tags.
7
+ fn render(self) -> String;
8
+ }
9
+
10
+ impl Renderable for () {
11
+ fn render(self) -> String {
12
+ "".to_string()
13
+ }
14
+ }
15
+
16
+ impl<A: Renderable, B: Renderable> Renderable for (A, B) {
17
+ fn render(self) -> String {
18
+ format!("{}{}", self.0.render(), self.1.render())
19
+ }
20
+ }
21
+
22
+ impl<A: Renderable, B: Renderable, C: Renderable> Renderable for (A, B, C) {
23
+ fn render(self) -> String {
24
+ ((self.0, self.1), self.2).render()
25
+ }
26
+ }
27
+
28
+ impl<A: Renderable, B: Renderable, C: Renderable, D: Renderable> Renderable for (A, B, C, D) {
29
+ fn render(self) -> String {
30
+ ((self.0, self.1), (self.2, self.3)).render()
31
+ }
32
+ }
33
+
34
+ impl<T: Renderable> Renderable for Option<T> {
35
+ fn render(self) -> String {
36
+ match self {
37
+ None => "".to_string(),
38
+ Some(x) => x.render(),
39
+ }
40
+ }
41
+ }
render/src/simple_element.rs ADDED
@@ -0,0 +1,41 @@
1
+ use crate::Renderable;
2
+ use std::collections::HashMap;
3
+
4
+ /// Simple HTML element tag
5
+ #[derive(Debug)]
6
+ pub struct SimpleElement<'a, T: Renderable> {
7
+ /// the HTML tag name, like `html`, `head`, `body`, `link`...
8
+ pub tag_name: &'a str,
9
+ pub attributes: Option<HashMap<&'a str, &'a str>>,
10
+ pub contents: Option<T>,
11
+ }
12
+
13
+ fn attributes_to_string<Key: std::fmt::Display + std::hash::Hash, Value: std::fmt::Debug>(
14
+ opt: &Option<HashMap<Key, Value>>,
15
+ ) -> String {
16
+ match opt {
17
+ None => "".to_string(),
18
+ Some(map) => {
19
+ let s: String = map
20
+ .iter()
21
+ .map(|(key, value)| format!(" {}={:?}", key, value))
22
+ .collect();
23
+ s
24
+ }
25
+ }
26
+ }
27
+
28
+ impl<'a, T: Renderable> Renderable for SimpleElement<'a, T> {
29
+ fn render(self) -> String {
30
+ let attrs = attributes_to_string(&self.attributes);
31
+ match self.contents {
32
+ None => format!("<{}{} />", self.tag_name, attrs),
33
+ Some(renderable) => format!(
34
+ "<{tag_name}{attrs}>{contents}</{tag_name}>",
35
+ tag_name = self.tag_name,
36
+ attrs = attrs,
37
+ contents = renderable.render()
38
+ ),
39
+ }
40
+ }
41
+ }
render/src/text_element.rs ADDED
@@ -0,0 +1,13 @@
1
+ use crate::Renderable;
2
+
3
+ impl Renderable for String {
4
+ fn render(self) -> String {
5
+ self
6
+ }
7
+ }
8
+
9
+ impl Renderable for &str {
10
+ fn render(self) -> String {
11
+ self.to_string()
12
+ }
13
+ }
render_macros/Cargo.toml ADDED
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "render_macros"
3
+ version = "0.1.0"
4
+ authors = ["Gal Schlezinger <[email protected]>"]
5
+ edition = "2018"
6
+
7
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8
+
9
+ [lib]
10
+ proc-macro = true
11
+
12
+ [dependencies]
13
+ syn = { version = "1.0", features = ["full"] }
14
+ quote = "1.0"
15
+ proc-macro2 = "1.0"
render_macros/src/element_attribute.rs ADDED
@@ -0,0 +1,52 @@
1
+ use quote::quote;
2
+ use syn::parse::{Parse, ParseStream, Result};
3
+
4
+ pub enum ElementAttribute {
5
+ Punned(syn::Ident),
6
+ WithValue(syn::Ident, syn::Block),
7
+ }
8
+
9
+ impl ElementAttribute {
10
+ pub fn ident(&self) -> &syn::Ident {
11
+ match self {
12
+ Self::Punned(ident) | Self::WithValue(ident, _) => ident,
13
+ }
14
+ }
15
+
16
+ pub fn value_tokens(&self) -> proc_macro2::TokenStream {
17
+ match self {
18
+ Self::WithValue(_, value) => quote!(#value),
19
+ Self::Punned(ident) => quote!(#ident),
20
+ }
21
+ }
22
+ }
23
+
24
+ impl PartialEq for ElementAttribute {
25
+ fn eq(&self, other: &Self) -> bool {
26
+ self.ident() == other.ident()
27
+ }
28
+ }
29
+
30
+ impl Eq for ElementAttribute {}
31
+
32
+ impl std::hash::Hash for ElementAttribute {
33
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
34
+ std::hash::Hash::hash(self.ident(), state)
35
+ }
36
+ }
37
+
38
+ impl Parse for ElementAttribute {
39
+ fn parse(input: ParseStream) -> Result<Self> {
40
+ let name = input.parse::<syn::Ident>()?;
41
+ let not_punned = input.peek(syn::Token![=]);
42
+
43
+ if !not_punned {
44
+ return Ok(Self::Punned(name));
45
+ }
46
+
47
+ input.parse::<syn::Token![=]>()?;
48
+ let value = input.parse::<syn::Block>()?;
49
+
50
+ Ok(Self::WithValue(name, value))
51
+ }
52
+ }
render_macros/src/lib.rs ADDED
@@ -0,0 +1,220 @@
1
+ #![feature(proc_macro_diagnostic, proc_macro_hygiene)]
2
+
3
+ // TODO: Extract a `Children` struct that can implement `Parse` and `ToTokens`:
4
+ // - `Parse` will do the nasty things inside `Element`
5
+ // - `ToTokens` will do the trick with the tuples
6
+
7
+ extern crate proc_macro;
8
+
9
+ mod element_attribute;
10
+
11
+ use element_attribute::ElementAttribute;
12
+ use proc_macro::TokenStream;
13
+ use quote::{quote, ToTokens};
14
+ use std::collections::HashSet;
15
+ use syn::parse::{Parse, ParseStream, Result};
16
+ use syn::parse_macro_input;
17
+
18
+ struct Element {
19
+ name: syn::Ident,
20
+ attributes: HashSet<ElementAttribute>,
21
+ children: Vec<RenderableItem>,
22
+ }
23
+
24
+ enum RenderableItem {
25
+ Element(Element),
26
+ RawBlock(syn::Block),
27
+ }
28
+
29
+ impl ToTokens for RenderableItem {
30
+ fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
31
+ match self {
32
+ Self::Element(element) => element.to_tokens(tokens),
33
+ Self::RawBlock(block) => {
34
+ let ts = quote! { #block };
35
+ ts.to_tokens(tokens);
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ impl Parse for RenderableItem {
42
+ fn parse(input: ParseStream) -> Result<Self> {
43
+ match input.parse::<Element>() {
44
+ Ok(element) => Ok(Self::Element(element)),
45
+ Err(_) => {
46
+ let block = input.parse::<syn::Block>()?;
47
+ Ok(Self::RawBlock(block))
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ impl Parse for Element {
54
+ fn parse(input: ParseStream) -> Result<Self> {
55
+ let mut attributes: HashSet<ElementAttribute> = HashSet::new();
56
+ let _starts_a_tag = input.parse::<syn::Token![<]>().is_ok();
57
+
58
+ let name = input.parse()?;
59
+
60
+ while input.peek(syn::Ident) {
61
+ if let Ok(attribute) = input.parse::<ElementAttribute>() {
62
+ if attributes.contains(&attribute) {
63
+ let error_message = format!(
64
+ "There is a previous definition of the {} attribute",
65
+ attribute.ident()
66
+ );
67
+ attribute
68
+ .ident()
69
+ .span()
70
+ .unwrap()
71
+ .warning(error_message)
72
+ .emit();
73
+ }
74
+ attributes.insert(attribute);
75
+ }
76
+ }
77
+
78
+ let can_have_contents = input.parse::<syn::Token![/]>().is_err();
79
+ input.parse::<syn::Token![>]>()?;
80
+
81
+ let mut children = vec![];
82
+
83
+ if can_have_contents {
84
+ while !input.peek(syn::Token![<]) || !input.peek2(syn::Token![/]) {
85
+ if let Ok(child) = input.parse::<RenderableItem>() {
86
+ children.push(child);
87
+ }
88
+ }
89
+
90
+ // parse closing
91
+ input.parse::<syn::Token![<]>()?;
92
+ input.parse::<syn::Token![/]>()?;
93
+ let closing_name: syn::Ident = input.parse()?;
94
+ if closing_name != name {
95
+ let error_message = format!("Expected closing tag for: <{}>", &name);
96
+ closing_name.span().unwrap().error(error_message).emit();
97
+ }
98
+ input.parse::<syn::Token![>]>()?;
99
+ }
100
+
101
+ Ok(Element {
102
+ name,
103
+ attributes,
104
+ children,
105
+ })
106
+ }
107
+ }
108
+
109
+ impl Element {
110
+ pub fn is_custom_element(&self) -> bool {
111
+ let name = self.name.to_string();
112
+ let first_letter = name.get(0..1).unwrap();
113
+ first_letter.to_uppercase() == first_letter
114
+ }
115
+ }
116
+
117
+ impl ToTokens for Element {
118
+ fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
119
+ let Element { name, children, .. } = self;
120
+
121
+ let children_quotes: Vec<_> = children
122
+ .iter()
123
+ .map(|child| {
124
+ quote! { #child }
125
+ })
126
+ .collect();
127
+ let children_tuple = match children_quotes.len() {
128
+ 0 => quote! { Option::<()>::None },
129
+ 1 => quote! { Some(#(#children_quotes)*) },
130
+ _ => {
131
+ let mut iter = children_quotes.iter();
132
+ let first = iter.next().unwrap();
133
+ let second = iter.next().unwrap();
134
+ let tuple_of_tuples = iter.fold(
135
+ quote!((#first, #second)),
136
+ |renderable, current| quote!((#current, #renderable)),
137
+ );
138
+
139
+ quote! { Some(#tuple_of_tuples) }
140
+ }
141
+ };
142
+
143
+ let declaration = if self.is_custom_element() {
144
+ let mut attrs: Vec<_> = self
145
+ .attributes
146
+ .iter()
147
+ .map(|attribute| {
148
+ let ident = attribute.ident();
149
+ let value = attribute.value_tokens();
150
+
151
+ quote! {
152
+ #ident: #value
153
+ }
154
+ })
155
+ .collect();
156
+
157
+ if children_quotes.len() > 0 {
158
+ attrs.push(quote! {
159
+ children: #children_tuple
160
+ });
161
+ }
162
+
163
+ if attrs.len() == 0 {
164
+ quote! { #name }
165
+ } else {
166
+ quote! {
167
+ #name {
168
+ #(#attrs),*
169
+ }
170
+ }
171
+ }
172
+ } else {
173
+ let attrs: Vec<_> = self
174
+ .attributes
175
+ .iter()
176
+ .map(|attribute| {
177
+ let ident = attribute.ident();
178
+ let value = attribute.value_tokens();
179
+
180
+ quote! {
181
+ hm.insert(stringify!(#ident), #value);
182
+ }
183
+ })
184
+ .collect();
185
+ let attributes_value = if self.attributes.len() == 0 {
186
+ quote!(None)
187
+ } else {
188
+ quote! {{
189
+ let mut hm = std::collections::HashMap::<&str, &str>::new();
190
+ #(#attrs)*
191
+ Some(hm)
192
+ }}
193
+ };
194
+ quote! {
195
+ ::render::SimpleElement {
196
+ tag_name: stringify!(#name),
197
+ attributes: #attributes_value,
198
+ contents: #children_tuple,
199
+ }
200
+ }
201
+ };
202
+ declaration.to_tokens(tokens);
203
+ }
204
+ }
205
+
206
+ /// Render a component tree to an HTML string
207
+ #[proc_macro]
208
+ pub fn html(input: TokenStream) -> TokenStream {
209
+ let el = proc_macro2::TokenStream::from(rsx(input));
210
+ let result = quote! { ::render::Renderable::render(#el) };
211
+ TokenStream::from(result)
212
+ }
213
+
214
+ /// Generate a renderable component tree
215
+ #[proc_macro]
216
+ pub fn rsx(input: TokenStream) -> TokenStream {
217
+ let el = parse_macro_input!(input as Element);
218
+ let result = quote! { #el };
219
+ TokenStream::from(result)
220
+ }
render_tests/Cargo.toml ADDED
@@ -0,0 +1,11 @@
1
+ [package]
2
+ name = "render_tests"
3
+ version = "0.1.0"
4
+ authors = ["Gal Schlezinger <[email protected]>"]
5
+ edition = "2018"
6
+ publish = false
7
+
8
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9
+
10
+ [dependencies]
11
+ render = { path = "../render" }
render_tests/src/lib.rs ADDED
@@ -0,0 +1,46 @@
1
+ #![feature(proc_macro_hygiene)]
2
+
3
+ use render::html::HTML5Doctype;
4
+ use render::{html, rsx, Renderable, Fragment};
5
+
6
+ #[derive(Debug)]
7
+ struct Hello<'a, T: Renderable> {
8
+ world: &'a str,
9
+ yes: i32,
10
+ children: T,
11
+ }
12
+
13
+ impl<'a, T: Renderable> Renderable for Hello<'a, T> {
14
+ fn render(self) -> String {
15
+ html! {
16
+ <b class={"some_bem_class"}>
17
+ {format!("{}", self.world)}
18
+ <br />
19
+ {format!("A number: {}", self.yes)}
20
+ {self.children}
21
+ </b>
22
+ }
23
+ }
24
+ }
25
+
26
+ pub fn it_works() -> String {
27
+ let world = "hello";
28
+ let other_value = rsx! {
29
+ <em>{format!("hello world?")}</em>
30
+ };
31
+ let value = html! {
32
+ <Fragment>
33
+ <HTML5Doctype />
34
+ <Hello world yes={1 + 1}>
35
+ <div>{format!("HEY!")}</div>
36
+ {other_value}
37
+ </Hello>
38
+ </Fragment>
39
+ };
40
+ value
41
+ }
42
+
43
+ #[test]
44
+ pub fn verify_works() {
45
+ println!("{}", it_works());
46
+ }