#![expect(
clippy::unwrap_used,
clippy::panic,
clippy::unreachable,
reason = "proc-macro crate (vendored from `vy-macros`): panics are the historical compile-error mechanism"
)]
mod ast;
mod fmt;
#[macro_use]
mod known;
mod root;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{LitStr, Token, parse::Parse, parse_macro_input};
use self::{
ast::{Element, ElementBody, ElementHead},
fmt::{Part, Serializer},
root::resolve_root,
};
mod kw {
syn::custom_keyword!(__rama_html_import_marker);
}
enum Inner {
Marker,
Body(ElementBody),
}
impl Parse for Inner {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.parse::<kw::__rama_html_import_marker>().is_ok() {
return Ok(Self::Marker);
}
Ok(Self::Body(input.parse()?))
}
}
fn render(head: ElementHead, body: ElementBody) -> TokenStream2 {
render_with_prefix(head, body, "")
}
fn render_with_prefix(head: ElementHead, body: ElementBody, prefix: &str) -> TokenStream2 {
let el = match Element::new(head, body) {
Ok(el) => el,
Err(err) => return err.to_compile_error(),
};
let root = resolve_root();
let mut text = String::from(prefix);
let mut ser = Serializer::new(&mut text, root.clone());
ser.write_element(el);
let imports = ser.as_imports();
let parts = ser.into_parts().into_iter().map(|part| match part {
Part::Str(s) => quote!(#root::protocols::html::PreEscaped(#s)),
Part::Expr(e) => quote!(#e),
});
quote!({
#imports;
#root::protocols::html::HtmlBuf(( #(#parts),* ))
})
}
fn known_inner(name: &str, input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as Inner);
let body = match parsed {
Inner::Marker => return quote!(()).into(),
Inner::Body(element_body) => element_body,
};
let head = match ElementHead::known(format_ident!("{}", name)) {
Ok(h) => h,
Err(err) => return err.to_compile_error().into(),
};
let prefix = if name == "html" {
"<!DOCTYPE html>"
} else {
""
};
render_with_prefix(head, body, prefix).into()
}
#[proc_macro]
pub fn custom(input: TokenStream) -> TokenStream {
struct CustomInput {
tag: LitStr,
body: Option<ElementBody>,
}
impl Parse for CustomInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let tag: LitStr = input.parse()?;
let body = if input.is_empty() {
None
} else {
let _: Token![,] = input.parse()?;
Some(input.parse()?)
};
Ok(Self { tag, body })
}
}
let CustomInput { tag, body } = parse_macro_input!(input as CustomInput);
let head = ElementHead::custom(tag.value());
let body = body.unwrap_or(ElementBody {
attrs: Vec::new(),
nodes: Vec::new(),
});
render(head, body).into()
}
macro_rules! define_proc_macro {
($($(#[doc=$doc:literal])* $el:ident)+) => {
$(
$(#[doc = $doc])*
#[proc_macro]
pub fn $el(input: TokenStream) -> TokenStream {
known_inner(stringify!($el), input)
}
)+
};
}
for_all_elements!(define_proc_macro);