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}