Skip to main content

rama_http_macros/
lib.rs

1//! Procedural macros powering `rama_http::protocols::html`.
2//!
3//! End users should not depend on this crate directly — it is re-exported
4//! from `rama-http` (and thus `rama`) under the `html` feature gate.
5//!
6//! The macros and parser logic are a permanent fork of
7//! [`vy-macros`](https://github.com/JonahLund/vy), adapted to integrate
8//! naturally with the rama ecosystem (in particular re-using
9//! `rama_core::combinators::Either` instead of vendoring its own
10//! `Either` type).
11
12#![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    // The `<html>` element is always the document root, so the macro
97    // emits `<!DOCTYPE html>` as a prefix. The result is therefore a
98    // complete HTML page — directly returnable from a handler as
99    // `IntoResponse`. If a caller really wants a bare `<html>` element
100    // without the doctype (rare), they can use `custom!("html", ...)`.
101    let prefix = if name == "html" {
102        "<!DOCTYPE html>"
103    } else {
104        ""
105    };
106
107    render_with_prefix(head, body, prefix).into()
108}
109
110/// Render an HTML element with a runtime-supplied tag name —
111/// useful for [web components] and any other custom element.
112///
113/// ```ignore
114/// // <my-icon size="lg"><span>x</span></my-icon>
115/// custom!("my-icon", size = "lg", span!("x"))
116/// ```
117///
118/// The first argument must be a string literal naming the tag. Subsequent
119/// arguments behave exactly like the body of a known-element macro:
120/// attributes (`name = value` / `name? = optional`) followed by children.
121///
122/// Custom elements are always rendered as containers — there is no
123/// "void" form, since web components are by definition non-void. Also
124/// note that the macro performs no validation of the tag name; the caller
125/// is responsible for picking a name that is legal HTML.
126///
127/// [web components]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components
128#[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);