rich_macros/lib.rs
1//! Compile-time checked markup and derive macros for the `rich` Rust port.
2//!
3//! Use these through `rs-rich-ext`'s `macros` feature: it re-exports them and
4//! supplies the runtime they expand to (`rich_ext::__private`, `rich_ext::derive`).
5//!
6//! Markup and styles are checked with the core's own parsers, so a literal the
7//! macro accepts is one the runtime accepts, with one deliberate addition: the
8//! macros also reject unknown style names and unclosed tags, which the runtime
9//! renders silently as no-ops.
10
11// A builder method that returns `Self` is `#[must_use]`: dropping its
12// result drops the value (CONTRIBUTING.md, "API conventions").
13#![warn(clippy::return_self_not_must_use)]
14
15use proc_macro::TokenStream;
16
17mod derive;
18mod inspect;
19mod markup;
20mod template;
21
22/// A [`Text`](https://docs.rs/rs-rich/latest/rich/text/struct.Text.html)
23/// from checked markup with `format!`-style placeholders.
24///
25/// ```ignore
26/// let text = richf!("[bold]{name}[/] has {count:>3} items", count = items.len());
27/// ```
28///
29/// Compile errors: an unbalanced, mismatched or unclosed tag; a tag that is
30/// neither a style (`bold red on blue`) nor a default theme key
31/// (`repr.number`); a malformed placeholder (the name must be an
32/// identifier); an unused argument. Declare custom
33/// theme keys first: `richf!(keys["app.title"], "[app.title]{name}[/]")`.
34///
35/// Values are escaped, so `[` in user data is never read as markup, and a
36/// value cannot reach the template's own tags (a trailing `\` does not escape
37/// the next one). A `[` before a placeholder that a `]` in the value could
38/// complete as a tag is a compile error. A
39/// placeholder inside a tag (`[{color}]`) is inserted as markup instead, and
40/// that tag is checked at run time only.
41#[proc_macro]
42pub fn richf(input: TokenStream) -> TokenStream {
43 template::richf(input.into())
44 .unwrap_or_else(syn::Error::into_compile_error)
45 .into()
46}
47
48/// Check a markup literal at compile time and return it as `&'static str`.
49/// Accepts `keys[...]` before the literal, as [`richf!`] does.
50#[proc_macro]
51pub fn markup(input: TokenStream) -> TokenStream {
52 template::markup(input.into())
53 .unwrap_or_else(syn::Error::into_compile_error)
54 .into()
55}
56
57/// A `Style` parsed at compile time: `style!("bold red on white")`.
58#[proc_macro]
59pub fn style(input: TokenStream) -> TokenStream {
60 template::style(input.into())
61 .unwrap_or_else(syn::Error::into_compile_error)
62 .into()
63}
64
65/// A theme key checked against the default theme: `theme_key!("repr.number")`
66/// expands to the `&'static str`.
67#[proc_macro]
68pub fn theme_key(input: TokenStream) -> TokenStream {
69 template::theme_key(input.into())
70 .unwrap_or_else(syn::Error::into_compile_error)
71 .into()
72}
73
74/// Render a struct or enum through `rich_ext::derive`.
75///
76/// Type attributes: `#[rich(title = "…")]`, `#[rich(panel)]` (bordered, titled),
77/// `#[rich(table)]` (a one-row table). Field attributes: `skip`,
78/// `label = "…"`, `style = "…"` (checked), `display` (use `Display`, not
79/// `Debug`; the default for `String`, `str`, `char` and `Cow` fields),
80/// `format = "{:.2}"`, `justify = "left" | "center" | "right"`, `order = N`.
81#[proc_macro_derive(Rich, attributes(rich))]
82pub fn derive_rich(input: TokenStream) -> TokenStream {
83 derive::rich(syn::parse_macro_input!(input as syn::DeriveInput))
84 .unwrap_or_else(syn::Error::into_compile_error)
85 .into()
86}
87
88/// Record what `rich_ext::inspect` reports on a struct or enum: the type's
89/// name and doc comment, each field's name, type as written, doc comment,
90/// visibility and value, and an enum's variants.
91///
92/// Field values are their `Debug` output, so every field's type must
93/// implement `Debug` unless it is marked. Field attributes:
94/// `#[inspect(skip)]` (left out; the value reads `Name { a: 1, .. }`),
95/// `#[inspect(with = path)]` (formatted by `path`, a function with
96/// `Debug::fmt`'s signature) and `#[inspect(nested)]` (a nested report of
97/// a field that is itself `Inspectable`, or an `Option` of one).
98///
99/// Type attribute: `#[inspect(methods)]` lists the methods recorded by
100/// [`macro@inspect`] on the type's inherent `impl` block.
101///
102/// A generic parameter used in a field's type gets a `Debug` bound on that
103/// type (`InspectField` for a nested field). Unions are refused.
104#[proc_macro_derive(Inspect, attributes(inspect))]
105pub fn derive_inspect(input: TokenStream) -> TokenStream {
106 inspect::derive(syn::parse_macro_input!(input as syn::DeriveInput))
107 .unwrap_or_else(syn::Error::into_compile_error)
108 .into()
109}
110
111/// Record an inherent `impl` block's methods for `rich_ext::inspect`: each
112/// `fn`'s name, signature as written, doc comment, and whether it is `pub`
113/// or `async`. Mark the type `#[inspect(methods)]` to list them; one block
114/// per type. `#[inspect(skip)]` on a method leaves it out.
115#[proc_macro_attribute]
116pub fn inspect(args: TokenStream, item: TokenStream) -> TokenStream {
117 inspect::methods(args.into(), syn::parse_macro_input!(item as syn::ItemImpl))
118 .unwrap_or_else(syn::Error::into_compile_error)
119 .into()
120}