Skip to main content

html_macro/
lib.rs

1extern crate proc_macro;
2
3use crate::config::{HtmlAndConfig, HtmlConfig};
4use crate::create::CreateHtmlMacro;
5use crate::parser::HtmlParser;
6use crate::tag::Tag;
7use syn::parse::{Parse, ParseStream};
8use syn::{parse_macro_input, parse_quote};
9
10mod config;
11mod create;
12
13mod maybe_dollar;
14mod parser;
15mod tag;
16
17/// Creates a custom `html!` macro with the given settings.
18/// The generated `html!` macro calls [`html_with_config`] under the hood.
19/// ```no_run
20/// # use html_macro::define_html_macro;
21/// # use percy_dom::{VirtualNode, VirtualElement, VirtualText, IterableNodes};
22///
23/// // This creates a macro called `my_html!`
24/// define_html_macro! {
25///     /// This documentation comment will get added to the generated `my_html!` macro.
26///     my_html!
27///     real_dom = (),
28///
29///     // Optionally configure the path to the underlying macro to call.
30///     calls = html_macro::html_with_config,
31/// };
32///
33/// // `my_html!` can now be used to create `VirtualNode`s.
34/// let node: VirtualNode<()> = my_html! { <div>hello world</div> };
35/// ```
36#[proc_macro]
37pub fn define_html_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
38    let parsed = parse_macro_input!(input as CreateHtmlMacro);
39    parsed.into_tokens().into()
40}
41
42/// Build a `VirtualNode` from a token stream.
43/// Calls [`html_with_config`] with the following configuration:
44/// - Uses `web_sys::Window` as the `RealDom`.
45///
46/// ## Examples
47// Ignored to avoid having to add `web_sys` as a dev-dependency.
48/// ```ignore
49/// # use html_macro::html;
50/// let div = html! { <div> Welcome to the html! procedural macro! </div> };
51/// ```
52#[proc_macro]
53pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
54    let html = parse_macro_input!(input as Html);
55
56    HtmlAndConfig {
57        html,
58        config: HtmlConfig {
59            real_dom_ty: parse_quote!(web_sys::Window),
60        },
61    }
62    .into_tokens()
63    .into()
64}
65
66/// Build a `VirtualNode` from a token stream.
67///
68/// Takes one or more comma-separated settings to configure, followed by a `;`, followed by by
69/// HTML.
70///
71/// ## Examples
72// Ignored to avoid having to add `web_sys` as a dev-dependency.
73/// ```ignore
74/// # use html_macro::html_with_config;
75///
76/// // Generates a `VirtualNode::<web_sys::Window>`
77/// let node = html_with_config! {
78///     real_dom = web_sys::Window;
79///     <div> hello world </div>
80/// };
81///
82/// ```
83///
84/// ```
85/// # use html_macro::html_with_config;
86/// # use percy_dom::{VirtualNode, VirtualElement, VirtualText, IterableNodes};
87///
88/// // Generates a `VirtualNode::<()>`
89/// let node = html_with_config! {
90///     real_dom = ();
91///     <div> hello world </div>
92/// };
93/// ```
94#[proc_macro]
95pub fn html_with_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
96    let parsed = parse_macro_input!(input as HtmlAndConfig);
97    parsed.into_tokens().into()
98}
99
100/// ...
101#[proc_macro]
102pub fn reflect_tokens(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
103    let parsed = parse_macro_input!(input as proc_macro2::TokenStream);
104    parsed.into()
105}
106
107#[derive(Debug)]
108struct Html {
109    tags: Vec<Tag>,
110}
111
112impl Parse for Html {
113    fn parse(input: ParseStream) -> syn::Result<Self> {
114        let mut tags = Vec::new();
115
116        while !input.is_empty() {
117            let tag: Tag = input.parse()?;
118            tags.push(tag);
119        }
120
121        Ok(Html { tags })
122    }
123}
124
125struct HtmlToTokensConfig {
126    real_dom_ty: syn::Type,
127}
128
129impl Html {
130    /// Start with parsed tags such as `<div hello="world"></div>`.
131    /// End with Rust code such as `let node = VirtualNode::new_element("div"); ...`
132    fn into_tokens(self, config: HtmlToTokensConfig) -> proc_macro2::TokenStream {
133        let html = self;
134
135        let mut html_parser = HtmlParser::new();
136
137        let parsed_tags_len = html.tags.len();
138
139        for (idx, tag) in html.tags.iter().enumerate() {
140            let mut next_tag = None;
141
142            if parsed_tags_len - 1 > idx {
143                next_tag = Some(&html.tags[idx + 1])
144            }
145
146            html_parser.push_tag(tag, next_tag, &config.real_dom_ty);
147        }
148
149        html_parser.finish()
150    }
151}
152
153#[cfg(test)]
154pub(self) mod tests {
155    //! This crate's tests assert that the generated tokens match what we expect.
156    //!
157    //! `crates/html-macro-test` contains tests that invoke the `html!` and confirm that the
158    //! returned node tree has the expected properties.
159
160    use super::*;
161    use proc_macro2::TokenStream;
162    use quote::quote;
163
164    /// Verify that after invoking the `html!` macro, the compiler output matches what we expect.
165    /// Primarily used to test compile-time error messages.
166    #[test]
167    fn ui() {
168        let t = trybuild::TestCases::new();
169
170        let ui_tests = concat!(env!("CARGO_MANIFEST_DIR"), "/ui_tests/*.rs");
171        t.compile_fail(ui_tests);
172    }
173
174    /// Verify that we specify a virtual element's `RealDom` generic parameter.
175    #[test]
176    fn specifies_generic_param() {
177        let tests = [
178            // Root element
179            (
180                quote! { <div> </div> },
181                quote! {
182                    {
183                        let mut node_0 = VirtualNode::<web_sys::Window>::new_element("div");
184                        node_0
185                    }
186                },
187            ),
188            // Root text
189            (
190                quote! { hello },
191                quote! {
192                    {
193                      let mut node_0 = VirtualNode::<web_sys::Window>::new_text("hello");
194                      node_0
195                    }
196                },
197            ),
198            // Root block
199            (
200                quote! {
201                    { some_variable }
202                },
203                quote! {
204                    {
205                        let node_0: VirtualNode::<web_sys::Window> = some_variable.into();
206                        node_0
207                    }
208                },
209            ),
210            // Block inside element
211            (
212                quote! {
213                    <div> { some_variable } </div>
214                },
215                quote! {
216                    {
217                        let mut node_0 = VirtualNode::<web_sys::Window>::new_element("div");
218                        let mut node_1: IterableNodes<web_sys::Window> = (some_variable).into();
219                        if let Some(ref mut element_node) = node_0.as_elem_mut() {
220                            element_node.children.extend(node_1.into_iter());
221                        } else {
222                            // TODO: Change our codegen to create a `VirtualElement`, push the
223                            //  children, then at the end create the `VirtualNode`.
224                            //  This way we can remove this `unreachable!()` branch entirely.
225                            { unreachable!("Non-elements cannot have children"); } ;
226                        }
227                        node_0
228                    }
229                },
230            ),
231        ];
232
233        for (tokens, expected) in tests {
234            assert_expected_html_tokens(tokens, expected);
235        }
236    }
237
238    #[track_caller]
239    pub(super) fn assert_expected_html_tokens(start: TokenStream, expected: TokenStream) {
240        let html: Html = syn::parse2(start).unwrap();
241        let dom_ty: syn::Type = syn::parse2(quote! { web_sys::Window}).unwrap();
242        assert_eq!(
243            html.into_tokens(HtmlToTokensConfig {
244                real_dom_ty: dom_ty,
245            })
246            .to_string(),
247            expected.to_string()
248        );
249    }
250}