1#![expect(
13 clippy::unwrap_used,
14 clippy::panic,
15 clippy::unreachable,
16 reason = "proc-macro crate (vendored from `vy-macros`): panics are the historical compile-error mechanism"
17)]
18
19mod ast;
20mod fmt;
21#[macro_use]
22mod known;
23mod root;
24
25use proc_macro::TokenStream;
26use proc_macro2::TokenStream as TokenStream2;
27use quote::{format_ident, quote};
28use syn::{LitStr, Token, parse::Parse, parse_macro_input};
29
30use self::{
31 ast::{Element, ElementBody, ElementHead},
32 fmt::{Part, Serializer},
33 root::resolve_root,
34};
35
36mod kw {
37 syn::custom_keyword!(__rama_html_import_marker);
38}
39
40enum Inner {
41 Marker,
42 Body(ElementBody),
43}
44
45impl Parse for Inner {
46 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
47 if input.parse::<kw::__rama_html_import_marker>().is_ok() {
48 return Ok(Self::Marker);
49 }
50
51 Ok(Self::Body(input.parse()?))
52 }
53}
54
55fn render(head: ElementHead, body: ElementBody) -> TokenStream2 {
56 render_with_prefix(head, body, "")
57}
58
59fn render_with_prefix(head: ElementHead, body: ElementBody, prefix: &str) -> TokenStream2 {
60 let el = match Element::new(head, body) {
61 Ok(el) => el,
62 Err(err) => return err.to_compile_error(),
63 };
64
65 let root = resolve_root();
66
67 let mut text = String::from(prefix);
68 let mut ser = Serializer::new(&mut text, root.clone());
69 ser.write_element(el);
70
71 let imports = ser.as_imports();
72 let parts = ser.into_parts().into_iter().map(|part| match part {
73 Part::Str(s) => quote!(#root::protocols::html::PreEscaped(#s)),
74 Part::Expr(e) => quote!(#e),
75 });
76
77 quote!({
78 #imports;
79 #root::protocols::html::HtmlBuf(( #(#parts),* ))
80 })
81}
82
83fn known_inner(name: &str, input: TokenStream) -> TokenStream {
84 let parsed = parse_macro_input!(input as Inner);
85
86 let body = match parsed {
87 Inner::Marker => return quote!(()).into(),
88 Inner::Body(element_body) => element_body,
89 };
90
91 let head = match ElementHead::known(format_ident!("{}", name)) {
92 Ok(h) => h,
93 Err(err) => return err.to_compile_error().into(),
94 };
95
96 let prefix = if name == "html" {
102 "<!DOCTYPE html>"
103 } else {
104 ""
105 };
106
107 render_with_prefix(head, body, prefix).into()
108}
109
110#[proc_macro]
129pub fn custom(input: TokenStream) -> TokenStream {
130 struct CustomInput {
131 tag: LitStr,
132 body: Option<ElementBody>,
133 }
134
135 impl Parse for CustomInput {
136 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
137 let tag: LitStr = input.parse()?;
138 let body = if input.is_empty() {
139 None
140 } else {
141 let _: Token![,] = input.parse()?;
142 Some(input.parse()?)
143 };
144 Ok(Self { tag, body })
145 }
146 }
147
148 let CustomInput { tag, body } = parse_macro_input!(input as CustomInput);
149 let head = ElementHead::custom(tag.value());
150 let body = body.unwrap_or(ElementBody {
151 attrs: Vec::new(),
152 nodes: Vec::new(),
153 });
154 render(head, body).into()
155}
156
157macro_rules! define_proc_macro {
158 ($($(#[doc=$doc:literal])* $el:ident)+) => {
159 $(
160 $(#[doc = $doc])*
161 #[proc_macro]
162 pub fn $el(input: TokenStream) -> TokenStream {
163 known_inner(stringify!($el), input)
164 }
165 )+
166 };
167}
168
169for_all_elements!(define_proc_macro);