plait_macros/lib.rs
1//! Procedural macros for the Plait HTML templating library.
2//!
3//! This crate provides the [`html!`] and [`component!`] macros that enable type-safe, compile-time HTML generation
4//! with a macro-based syntax.
5//!
6//! # Overview
7//!
8//! - [`html!`] - Generate HTML fragments with embedded Rust expressions
9//! - [`component!`] - Define reusable HTML components with props and children
10//!
11//! # Quick start
12//!
13//! ```rust,ignore
14//! use plait::{html, component, render};
15//!
16//! // Simple HTML generation
17//! let page = render(html! {
18//! div(class: "container") {
19//! h1 { "Hello, World!" }
20//! p { "Welcome to Plait." }
21//! }
22//! });
23//!
24//! // Define a reusable component
25//! component! {
26//! pub fn Card(title: &str) {
27//! div(class: "card", #attrs) {
28//! h2 { (title) }
29//! #children
30//! }
31//! }
32//! }
33//!
34//! // Use the component
35//! let card = render(html! {
36//! @Card(title: "My Card"; id: "card-1") {
37//! p { "Card content goes here." }
38//! }
39//! });
40//! ```
41//!
42//! # Features
43//!
44//! - **Type-safe**: Compile-time validation of HTML structure and Rust expressions
45//! - **XSS protection**: Automatic HTML escaping with opt-out for trusted content
46//! - **URL validation**: Dangerous protocols in URL attributes are automatically stripped
47//! - **Ergonomic syntax**: `snake_case` to `kebab-case` conversion for element and attribute names
48//! - **Full Rust integration**: Conditionals, loops, and pattern matching within templates
49//! - **Component system**: Reusable components with props, children, and attribute spreading
50//!
51//! # Crate organization
52//!
53//! This is a proc-macro crate and should typically be used through the main `plait` crate, which re-exports these
54//! macros along with the runtime types (`HtmlFormatter`, `render`, etc.).
55//!
56//! See the individual macro documentation for complete syntax references and examples.
57
58mod ast;
59mod codegen;
60mod desugar;
61
62use proc_macro::TokenStream;
63
64/// A procedural macro for generating type-safe HTML with embedded Rust expressions.
65///
66/// The `html!` macro provides a macro-based syntax for creating HTML content at compile time. It returns an
67/// `HtmlFragment` that implements `IntoHtml` and `IntoHtmlRaw`.
68///
69/// # Basic Usage
70///
71/// ```rust,ignore
72/// use plait::{html, render};
73///
74/// let html = render(html! {
75/// div {
76/// "Hello World"
77/// }
78/// });
79/// // Output: <div>Hello World</div>
80/// ```
81///
82/// # Syntax reference
83///
84/// ## Text content
85///
86/// String literals are automatically HTML-escaped:
87///
88/// ```rust,ignore
89/// html! { "<script>alert('XSS')</script>" }
90/// // Output: <script>alert('XSS')</script>
91/// ```
92///
93/// ## Expressions
94///
95/// Rust expressions can be embedded using parentheses. Values are HTML-escaped:
96///
97/// ```rust,ignore
98/// let name = "World";
99/// html! { "Hello " (name) }
100/// // Output: Hello World
101/// ```
102///
103/// ## Raw (unescaped) output
104///
105/// Use `#(expr)` to output content without HTML escaping:
106///
107/// ```rust,ignore
108/// html! { #("<strong>Bold</strong>") }
109/// // Output: <strong>Bold</strong>
110/// ```
111///
112/// **Warning:** Only use raw output with trusted content to prevent XSS vulnerabilities.
113///
114/// ## Elements
115///
116/// HTML elements use a block syntax with the tag name followed by braces:
117///
118/// ```rust,ignore
119/// html! {
120/// div {
121/// span { "Nested content" }
122/// }
123/// }
124/// // Output: <div><span>Nested content</span></div>
125/// ```
126///
127/// ### Element name conversion
128///
129/// Element names are automatically converted from `snake_case` to `kebab-case`:
130///
131/// ```rust,ignore
132/// html! { custom_element { "Content" } }
133/// // Output: <custom-element>Content</custom-element>
134/// ```
135///
136/// ### Void elements
137///
138/// Void elements (`br`, `hr`, `img`, `input`, `meta`, `link`, etc.) use a semicolon
139/// instead of braces:
140///
141/// ```rust,ignore
142/// html! {
143/// div {
144/// br;
145/// input(type: "text");
146/// }
147/// }
148/// // Output: <div><br><input type="text"></div>
149/// ```
150///
151/// ## Attributes
152///
153/// Attributes are specified in parentheses after the element name:
154///
155/// ```rust,ignore
156/// html! {
157/// div(class: "container", id: "main") {
158/// "Content"
159/// }
160/// }
161/// // Output: <div class="container" id="main">Content</div>
162/// ```
163///
164/// ### Attribute name conversion
165///
166/// Attribute names with underscores are converted to hyphens:
167///
168/// ```rust,ignore
169/// html! { div(hx_target: "body") {} }
170/// // Output: <div hx-target="body"></div>
171/// ```
172///
173/// ### String literal attribute names
174///
175/// Use string literals for attribute names with special characters:
176///
177/// ```rust,ignore
178/// html! { div("@click": "handleClick()") {} }
179/// // Output: <div @click="handleClick()"></div>
180/// ```
181///
182/// ### Boolean attributes
183///
184/// Attributes without values render as boolean attributes:
185///
186/// ```rust,ignore
187/// html! { button(disabled) { "Submit" } }
188/// // Output: <button disabled>Submit</button>
189/// ```
190///
191/// ### Optional attributes
192///
193/// Use `?:` syntax for conditional attributes. Works with `Option<T>` and `bool`:
194///
195/// ```rust,ignore
196/// let class = Some("active");
197/// let disabled = false;
198///
199/// html! {
200/// button(class?: class, disabled?: disabled) {
201/// "Click"
202/// }
203/// }
204/// // Output: <button class="active">Click</button>
205/// ```
206///
207/// ### Raw attribute values
208///
209/// Use `#(expr)` for unescaped attribute values:
210///
211/// ```rust,ignore
212/// html! { div(class: #("<script>")) {} }
213/// // Output: <div class="<script>"></div>
214/// ```
215///
216/// ### URL attribute protection
217///
218/// URL attributes (`href`, `src`, `action`, etc.) are automatically validated.
219/// Dangerous protocols like `javascript:` are stripped:
220///
221/// ```rust,ignore
222/// html! { a(href: "javascript:alert('XSS')") { "Link" } }
223/// // Output: <a>Link</a>
224///
225/// // Safe URLs work normally:
226/// html! { a(href: "https://example.com") { "Link" } }
227/// // Output: <a href="https://example.com">Link</a>
228/// ```
229///
230/// Use `#(expr)` to bypass URL validation when needed (trusted content only).
231///
232/// ## Control flow
233///
234/// ### If expressions
235///
236/// ```rust,ignore
237/// let show = true;
238///
239/// html! {
240/// if show {
241/// div { "Visible" }
242/// }
243/// }
244/// ```
245///
246/// With `else` and `else if`:
247///
248/// ```rust,ignore
249/// let count = 5;
250///
251/// html! {
252/// if count == 0 {
253/// "None"
254/// } else if count == 1 {
255/// "One"
256/// } else {
257/// "Many"
258/// }
259/// }
260/// ```
261///
262/// ### If let expressions
263///
264/// ```rust,ignore
265/// let user = Some("Alice");
266///
267/// html! {
268/// if let Some(name) = user {
269/// span { "Hello, " (name) }
270/// } else {
271/// span { "Guest" }
272/// }
273/// }
274/// ```
275///
276/// ### For loops
277///
278/// ```rust,ignore
279/// let items = vec!["Apple", "Banana", "Cherry"];
280///
281/// html! {
282/// ul {
283/// for item in items {
284/// li { (item) }
285/// }
286/// }
287/// }
288/// // Output: <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>
289/// ```
290///
291/// ### Match expressions
292///
293/// ```rust,ignore
294/// let status = "success";
295///
296/// html! {
297/// match status {
298/// "success" => span(class: "green") { "OK" },
299/// "error" => span(class: "red") { "Failed" },
300/// _ => span { "Unknown" }
301/// }
302/// }
303/// ```
304///
305/// ## Component calls
306///
307/// Components are invoked with `@ComponentName` syntax. See the [`component!`] macro for defining components.
308///
309/// ```rust,ignore
310/// html! {
311/// @Button(class: "primary"; id: "submit", disabled?: false) {
312/// "Click me"
313/// }
314/// }
315/// ```
316///
317/// Component calls use a semicolon to separate component props from HTML attributes:
318/// - Before `;`: Component-specific props
319/// - After `;`: HTML attributes passed through `#attrs`
320///
321/// # Generated code
322///
323/// The macro generates an `HtmlFragment` which implements `IntoHtml`. Use with `render()` or `render_with_capacity()`
324/// to produce the final HTML string, or pass fragments directly to component props that accept `T: IntoHtml`.
325#[proc_macro]
326pub fn html(input: TokenStream) -> TokenStream {
327 codegen::html_impl(input.into()).into()
328}
329
330/// A procedural macro for defining reusable HTML components.
331///
332/// The `component!` macro creates a struct and implements the `Component` trait, enabling composable and reusable
333/// HTML templates.
334///
335/// # Basic usage
336///
337/// ```rust,ignore
338/// use plait::{component, html, render};
339///
340/// component! {
341/// pub fn Card {
342/// div(class: "card") {
343/// #children
344/// }
345/// }
346/// }
347///
348/// let html = render(html! {
349/// @Card {
350/// "Card content"
351/// }
352/// });
353/// // Output: <div class="card">Card content</div>
354/// ```
355///
356/// # Syntax
357///
358/// ```rust,ignore
359/// component! {
360/// [visibility] fn ComponentName[<generics>]([props]) [where clause] {
361/// // component body (html! syntax)
362/// }
363/// }
364/// ```
365///
366/// ## Component props
367///
368/// Define typed props in the function signature:
369///
370/// ```rust,ignore
371/// component! {
372/// pub fn Button(class: &str, size: u32) {
373/// button(class: format_args!("btn {} size-{}", class, size)) {
374/// #children
375/// }
376/// }
377/// }
378///
379/// // Usage:
380/// html! {
381/// @Button(class: "primary", size: 2) {
382/// "Click me"
383/// }
384/// }
385/// // Output: <button class="btn primary size-2">Click me</button>
386/// ```
387///
388/// ## Special placeholders
389///
390/// ### `#children` - Child content
391///
392/// The `#children` placeholder renders content passed between the component's opening and closing tags:
393///
394/// ```rust,ignore
395/// component! {
396/// pub fn Wrapper {
397/// div(class: "wrapper") {
398/// #children
399/// }
400/// }
401/// }
402///
403/// html! {
404/// @Wrapper {
405/// span { "Child 1" }
406/// span { "Child 2" }
407/// }
408/// }
409/// // Output: <div class="wrapper"><span>Child 1</span><span>Child 2</span></div>
410/// ```
411///
412/// ### `#attrs` - Attribute spreading
413///
414/// The `#attrs` placeholder spreads HTML attributes passed to the component. This enables attribute forwarding for
415/// flexible component APIs:
416///
417/// ```rust,ignore
418/// component! {
419/// pub fn Button(class: &str) {
420/// button(class: format_args!("btn {}", class), #attrs) {
421/// #children
422/// }
423/// }
424/// }
425///
426/// // Attributes after `;` are spread via #attrs:
427/// html! {
428/// @Button(class: "primary"; id: "submit", disabled?: true) {
429/// "Submit"
430/// }
431/// }
432/// // Output: <button class="btn primary" id="submit" disabled>Submit</button>
433/// ```
434///
435/// ## Accepting HTML fragments as props
436///
437/// Use the `IntoHtml` trait bound to accept `html!` fragments as props:
438///
439/// ```rust,ignore
440/// use plait::IntoHtml;
441///
442/// component! {
443/// pub fn Card(header: impl IntoHtml) {
444/// div(class: "card") {
445/// div(class: "card-header") { (header) }
446/// div(class: "card-body") { #children }
447/// }
448/// }
449/// }
450///
451/// // Pass an html! fragment as a prop:
452/// html! {
453/// @Card(header: html! { strong { "Title" } }) {
454/// "Card content"
455/// }
456/// }
457/// // Output: <div class="card"><div class="card-header"><strong>Title</strong></div><div class="card-body">Card content</div></div>
458/// ```
459///
460/// This works because `HtmlFragment` (returned by `html!`) implements `IntoHtml`.
461///
462/// ## Component composition
463///
464/// Components can be nested and composed:
465///
466/// ```rust,ignore
467/// component! {
468/// pub fn Button(class: impl ClassPart) {
469/// button(class: classes!("btn", class), #attrs) {
470/// #children
471/// }
472/// }
473/// }
474///
475/// component! {
476/// pub fn Card {
477/// div(class: "card") {
478/// @Button(class: "card-btn"; #attrs) {
479/// #children
480/// }
481/// }
482/// }
483/// }
484/// ```
485///
486/// # Calling components
487///
488/// Components are called using `@ComponentName` syntax within `html!`:
489///
490/// ```rust,ignore
491/// html! {
492/// @ComponentName(prop1: value1, prop2: value2; attr1: val1, attr2?: optional) {
493/// // children
494/// }
495/// }
496/// ```
497///
498/// The syntax is:
499/// - `@ComponentName` - Component invocation prefix
500/// - `(...)` - Optional arguments section
501/// - Before `;` - Component props (matched to component parameters)
502/// - After `;` - HTML attributes (spread via `#attrs`)
503/// - `{ ... }` - Children content (rendered via `#children`)
504///
505/// If a component has no props, you can omit everything before the semicolon:
506///
507/// ```rust,ignore
508/// html! {
509/// @Card(; class: "highlighted") {
510/// "Content"
511/// }
512/// }
513/// ```
514///
515/// # Generated code
516///
517/// The macro generates:
518///
519/// 1. A struct with the component's props as public fields:
520/// ```rust,ignore
521/// pub struct Button<'a> {
522/// pub class: &'a str,
523/// }
524/// ```
525///
526/// 2. An implementation of the `Component` trait:
527/// ```rust,ignore
528/// impl<'a> Component for Button<'a> {
529/// fn render(
530/// self,
531/// f: &mut HtmlFormatter<'_>,
532/// attrs: impl FnOnce(&mut HtmlFormatter<'_>),
533/// children: impl FnOnce(&mut HtmlFormatter<'_>),
534/// ) {
535/// // component body
536/// }
537/// }
538/// ```
539#[proc_macro]
540pub fn component(input: TokenStream) -> TokenStream {
541 codegen::component_impl(input.into()).into()
542}