1use std::collections::HashSet;
2
3use proc_macro::TokenStream;
4use proc_macro2_diagnostics::Diagnostic;
5use quote::{quote, quote_spanned, ToTokens};
6use rstml::{
7 node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName},
8 Parser, ParserConfig,
9};
10use syn::punctuated::Punctuated;
11use syn::{parse::Parse, parse_quote, spanned::Spanned, Expr, ExprLit, FnArg, ItemStruct, Token};
12
13#[proc_macro]
14pub fn html(tokens: TokenStream) -> TokenStream {
15 html_inner(tokens, false)
16}
17
18#[proc_macro]
19pub fn html_ide(tokens: TokenStream) -> TokenStream {
20 html_inner(tokens, true)
21}
22
23fn is_empty_element(name: &str) -> bool {
24 match name {
26 "img" | "input" | "meta" | "link" | "hr" | "br" | "source" | "track" | "wbr" | "area"
27 | "base" | "col" | "embed" | "param" => true,
28 _ => false,
29 }
30}
31
32fn empty_elements_set() -> HashSet<&'static str> {
33 [
34 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
35 "source", "track", "wbr",
36 ]
37 .into_iter()
38 .collect()
39}
40
41fn html_inner(tokens: TokenStream, ide_helper: bool) -> TokenStream {
42 let config = ParserConfig::new()
43 .recover_block(true)
44 .element_close_use_default_wildcard_ident(true)
45 .always_self_closed_elements(empty_elements_set());
46
47 let parser = Parser::new(config);
48 let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
49 process_nodes(ide_helper, &nodes, errors).into()
50}
51
52fn process_nodes<'n>(
53 ide_helper: bool,
54 nodes: &'n Vec<Node>,
55 errors: Vec<Diagnostic>,
56) -> proc_macro2::TokenStream {
57 let WalkNodesOutput {
58 static_format: html_string,
59 values,
60 collected_elements: elements,
61 diagnostics,
62 } = walk_nodes(&nodes);
63 let docs = if ide_helper {
64 generate_tags_docs(elements)
65 } else {
66 vec![]
67 };
68 let errors = errors
69 .into_iter()
70 .map(|e| e.emit_as_expr_tokens())
71 .chain(diagnostics);
72 quote! {
73 {
74 #(#errors;)*
76 #(#docs;)*
78 format!(#html_string, #(rscx::FormatWrapper::new(#values)),*)
79 }
80 }
81}
82
83fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec<proc_macro2::TokenStream> {
84 let elements_as_type: HashSet<&'static str> =
88 vec!["html", "head", "meta", "link", "body", "div"]
89 .into_iter()
90 .collect();
91
92 elements
93 .into_iter()
94 .map(|e| {
95 if elements_as_type.contains(&*e.to_string()) {
96 let element = quote_spanned!(e.span() => enum);
97 quote!({#element X{}})
98 } else {
99 let element = quote_spanned!(e.span() => element);
101 quote!(let _ = crate::docs::#element)
102 }
103 })
104 .collect()
105}
106
107#[derive(Default)]
108struct WalkNodesOutput<'a> {
109 static_format: String,
110 values: Vec<proc_macro2::TokenStream>,
113 diagnostics: Vec<proc_macro2::TokenStream>,
115 collected_elements: Vec<&'a NodeName>,
120}
121impl<'a> WalkNodesOutput<'a> {
122 fn extend(&mut self, other: WalkNodesOutput<'a>) {
123 self.static_format.push_str(&other.static_format);
124 self.values.extend(other.values);
125 self.diagnostics.extend(other.diagnostics);
126 self.collected_elements.extend(other.collected_elements);
127 }
128}
129
130fn walk_nodes<'a>(nodes: &'a Vec<Node>) -> WalkNodesOutput<'a> {
131 let mut out = WalkNodesOutput::default();
132
133 for node in nodes {
134 match node {
135 Node::Doctype(doctype) => {
136 let value = &doctype.value.to_token_stream_string();
137 out.static_format.push_str(&format!("<!DOCTYPE {}>", value));
138 }
139 Node::Element(element) => {
140 let name = element.name().to_string();
141
142 if !is_component_tag_name(&name) {
143 match element.name() {
144 NodeName::Block(block) => {
145 out.static_format.push_str("<{}");
146 out.values.push(block.to_token_stream());
147 }
148 _ => {
149 out.static_format.push_str(&format!("<{}", name));
150 out.collected_elements.push(&element.open_tag.name);
151 if let Some(e) = &element.close_tag {
152 out.collected_elements.push(&e.name)
153 }
154 }
155 }
156
157 for attribute in element.attributes() {
159 match attribute {
160 NodeAttribute::Block(block) => {
161 out.static_format.push(' ');
163 out.static_format.push_str("{}");
164 out.values.push(block.to_token_stream());
165 }
166 NodeAttribute::Attribute(attribute) => {
167 let (static_format, value) = walk_attribute(attribute);
168 out.static_format.push_str(&static_format);
169 if let Some(value) = value {
170 out.values.push(value);
171 }
172 }
173 }
174 }
175 if is_empty_element(element.open_tag.name.to_string().as_str()) {
177 out.static_format.push_str(" />");
178 if !element.children.is_empty() {
179 let warning = proc_macro2_diagnostics::Diagnostic::spanned(
180 element.open_tag.name.span(),
181 proc_macro2_diagnostics::Level::Warning,
182 "Element is processed as empty, and cannot have any child",
183 );
184 out.diagnostics.push(warning.emit_as_expr_tokens())
185 }
186
187 continue;
188 }
189 out.static_format.push('>');
190
191 let other_output = walk_nodes(&element.children);
193 out.extend(other_output);
194
195 match element.name() {
196 NodeName::Block(block) => {
197 out.static_format.push_str("</{}>");
198 out.values.push(block.to_token_stream());
199 }
200 _ => {
201 out.static_format.push_str(&format!("</{}>", name));
202 }
203 }
204 } else {
205 out.static_format.push_str("{}");
207 out.values
208 .push(CustomElement::new(element).to_token_stream());
209 }
210 }
211 Node::Text(text) => {
212 out.static_format.push_str(&text.value_string());
213 }
214 Node::RawText(text) => {
215 out.static_format.push_str(&text.to_string_best());
216 }
217 Node::Fragment(fragment) => {
218 let other_output = walk_nodes(&fragment.children);
219 out.extend(other_output)
220 }
221 Node::Comment(comment) => {
222 out.static_format.push_str("<!-- {} -->");
223 out.values.push(comment.value.to_token_stream());
224 }
225 Node::Block(block) => {
226 let block = block.try_block().unwrap();
227 let stmts = &block.stmts;
228 out.static_format.push_str("{}");
229 out.values.push(quote!(#(#stmts)*));
230 }
231 }
232 }
233
234 out
235}
236
237fn walk_attribute(attribute: &KeyedAttribute) -> (String, Option<proc_macro2::TokenStream>) {
238 let mut static_format = String::new();
239 let mut format_value = None;
240 let key = match attribute.key.to_string().as_str() {
241 "as_" => "as".to_string(),
242 _ => attribute.key.to_string(),
243 };
244 static_format.push_str(&format!(" {}", key));
245
246 match attribute.value() {
247 Some(Expr::Lit(ExprLit {
248 lit: syn::Lit::Str(value),
249 ..
250 })) => {
251 static_format.push_str(&format!(
252 r#"="{}""#,
253 html_escape::encode_unquoted_attribute(&value.value())
254 ));
255 }
256 Some(Expr::Lit(ExprLit {
257 lit: syn::Lit::Bool(value),
258 ..
259 })) => {
260 static_format.push_str(&format!(r#"="{}""#, value.value()));
261 }
262 Some(Expr::Lit(ExprLit {
263 lit: syn::Lit::Int(value),
264 ..
265 })) => {
266 static_format.push_str(&format!(r#"="{}""#, value.token()));
267 }
268 Some(Expr::Lit(ExprLit {
269 lit: syn::Lit::Float(value),
270 ..
271 })) => {
272 static_format.push_str(&format!(r#"="{}""#, value.token()));
273 }
274 Some(value) => {
275 static_format.push_str(r#"="{}""#);
276 format_value = Some(
277 quote! {{
278 ::rscx::EscapeAttribute::escape_attribute(&#value)
280 }}
281 .into_token_stream(),
282 );
283 }
284 None => {}
285 }
286
287 (static_format, format_value)
288}
289
290fn is_component_tag_name(name: &str) -> bool {
291 name.starts_with(|c: char| c.is_ascii_uppercase())
292}
293
294struct CustomElement<'e> {
295 e: &'e NodeElement,
296}
297
298impl<'e> CustomElement<'e> {
299 fn new(e: &'e NodeElement) -> Self {
300 CustomElement { e }
301 }
302}
303
304impl<'e> ToTokens for CustomElement<'_> {
305 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
306 let name = self.e.name();
307
308 let mut chain = vec![quote! {
309 ::rscx::props::props_builder(&#name)
310 }];
311
312 let children = &self.e.children;
313 if !children.is_empty() {
314 let c = process_nodes(false, children, vec![]);
315 chain.push(quote! { .children(#c) });
316 }
317
318 chain.push({
319 self.e
320 .attributes()
321 .iter()
322 .map(|a| match a {
323 NodeAttribute::Block(block) => {
324 quote! {
325 .push_attr(
326 #[allow(unused_braces)]
327 #block
328 )
329 }
330 }
331 NodeAttribute::Attribute(attribute) => {
332 let key = &attribute.key;
333 let value = attribute.value().unwrap();
334 quote! { .#key(#value) }
335 }
336 })
337 .collect::<proc_macro2::TokenStream>()
338 });
339
340 chain.push(quote! { .build() });
341
342 tokens.extend(quote! {
343 #name(#(#chain)*).await
344 });
345 }
346}
347
348#[proc_macro_attribute]
349pub fn props(_attr: TokenStream, input: TokenStream) -> TokenStream {
350 let props = syn::parse_macro_input!(input as PropsStruct);
351 quote! { #props }.to_token_stream().into()
352}
353
354struct PropsStruct {
355 name: syn::Ident,
356 item: ItemStruct,
357}
358
359impl Parse for PropsStruct {
360 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
361 let item = input.parse::<ItemStruct>()?;
362 let name = item.ident.clone();
363
364 Ok(PropsStruct { name, item })
365 }
366}
367
368impl ToTokens for PropsStruct {
369 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
370 let name = &self.name;
371 let item = &self.item;
372
373 let builder_name =
374 syn::Ident::new(&format!("{}Builder", name), proc_macro2::Span::call_site());
375
376 tokens.extend(quote! {
377 #[derive(::rscx::typed_builder::TypedBuilder)]
378 #[builder(doc, crate_module_path=::rscx::typed_builder)]
379 #item
380
381 impl ::rscx::props::Props for #name {
382 type Builder = #builder_name;
383 fn builder() -> Self::Builder {
384 #name::builder()
385 }
386 }
387 });
388
389 let has_attributes = item
390 .fields
391 .iter()
392 .any(|field| field.ident.as_ref().unwrap().to_string() == "attributes");
393
394 if has_attributes {
395 tokens.extend(quote! {
396 impl #builder_name {
397 pub fn push_attr<A: std::fmt::Display>(mut self, attr: A) -> Self {
398 self.props.attributes.push_str(&format!("{} ", attr));
399 self
400 }
401 }
402 });
403 }
404 }
405}
406
407#[proc_macro_attribute]
408pub fn component(_attr: TokenStream, input: TokenStream) -> TokenStream {
409 let comp = syn::parse_macro_input!(input as ComponentFn);
410 quote! { #comp }.to_token_stream().into()
411}
412
413struct ComponentFn {
414 item: syn::ItemFn,
415}
416
417impl Parse for ComponentFn {
418 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
419 let item = input.parse::<syn::ItemFn>()?;
420 Ok(ComponentFn { item })
421 }
422}
423
424impl ToTokens for ComponentFn {
425 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
426 let item = &self.item;
427 let name = &item.sig.ident;
428
429 let (defs, args) = match item.sig.inputs.len() {
430 0 => {
431 let props_name =
433 syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
434 (
435 quote! {
436 #[props]
437 pub struct #props_name{}
438 },
439 quote! { _props: #props_name },
440 )
441 }
442 1 if matches!(item.sig.inputs.first().unwrap(), syn::FnArg::Typed(arg) if matches!(arg.ty.as_ref(), syn::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string() == format!("{}Props", name))) =>
444 {
445 let props = item.sig.inputs.first().unwrap();
446 (quote! {}, props.to_token_stream())
447 }
448 _ => {
449 let field_defs = &item
450 .sig
451 .inputs
452 .clone()
453 .into_iter()
454 .map(|i| match i {
455 FnArg::Receiver(_) => {
456 panic!("receiver arguments unsupported");
457 }
458 FnArg::Typed(mut t) => {
459 if t.attrs.is_empty() {
460 t.attrs.push(parse_quote! { #[builder(setter(into))] });
461 }
462
463 t
464 }
465 })
466 .collect::<Punctuated<_, Token![,]>>();
467 let field_names = item
468 .sig
469 .inputs
470 .iter()
471 .map(|i| match i {
472 FnArg::Receiver(_) => {
473 panic!("receiver arguments unsupported");
474 }
475 FnArg::Typed(t) => &t.pat,
476 })
477 .collect::<Punctuated<_, Token![,]>>();
478 let props_name =
479 syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
480
481 (
482 quote! {
483 #[rscx::props]
484 pub struct #props_name {
485 #field_defs
486 }
487 },
488 quote! { #props_name { #field_names }: #props_name },
489 )
490 }
491 };
492
493 let body = &item.block;
494 let output = &item.sig.output;
495 let vis = &item.vis;
496
497 tokens.extend(quote! {
498 #defs
499 #[allow(non_snake_case)]
500 #vis async fn #name(#args) #output {
501 #body
502 }
503 });
504 }
505}