diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-07-20 08:42:16 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-07-20 08:42:16 +0800 |
| commit | 7b3a1d7fd9f60b91a7f9602374c081882d46facc (patch) | |
| tree | dc0c8a9bf095308e6d29091db4b5037c29ed5742 | |
| parent | 9dc737acec1eab40ef65bef8e6c729a4e5e06401 (diff) | |
feat(macros): add `#[buffer]` attribute and re-export `r_print!(ln)`
macros
Reintroduce `r_print!` and `r_println!` macros as public exports, now
supporting both explicit buffer argument and implicit `#[buffer]` attr.
Add `#[buffer]` attribute macro that wraps unit-returning functions to
produce a `RenderResult` with an automatically injected buffer variable.
Relax `RenderResult::print()` and `println()` to accept `impl
AsRef<str>`.
| -rw-r--r-- | CHANGELOG.md | 21 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 8 | ||||
| -rw-r--r-- | mingling_core/src/renderer/render_result.rs | 8 | ||||
| -rw-r--r-- | mingling_macros/src/extensions.rs | 3 | ||||
| -rw-r--r-- | mingling_macros/src/extensions/buffer.rs | 95 | ||||
| -rw-r--r-- | mingling_macros/src/func.rs | 1 | ||||
| -rw-r--r-- | mingling_macros/src/func/r_print.rs | 62 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 98 |
8 files changed, 288 insertions, 8 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b2fea6c..b90536e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -274,22 +274,35 @@ None #### **BREAKING CHANGES** (API CHANGES): -1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros. The `#[renderer]` and `#[help]` macros no longer implicitly inject an internal `RenderResult` variable or provide `r_println!` / `r_print!` macros. +1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros from being implicitly injected by `#[renderer]` and `#[help]` macros. These macros still exist, but must now be used **explicitly** — either with an explicit buffer argument, or via the `#[buffer]` extension attribute that re-enables implicit buffer injection. - Renderers and help functions must now explicitly create and return a `RenderResult`: + **Option A — Explicit buffer:** Pass a `RenderResult` variable as the first argument: ```rust + use mingling::macros::r_println; use mingling::prelude::*; #[renderer] fn render_greeting(greeting: ResultGreeting) -> RenderResult { let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *greeting).ok(); + r_println!(result, "Hello, {}!", *greeting); result } ``` - All examples, docs, and test cases across the repository have been updated to use the new pattern: creating a `RenderResult` with `RenderResult::new()` or `RenderResult::default()`, writing with `write!`/`writeln!` from `std::io::Write`, and returning the result. + **Option B — Implicit buffer via `#[buffer]` extension:** Use `#[renderer(buffer)]` to re-enable the old implicit injection behavior, where a `RenderResult` buffer is automatically created and `r_println!`/`r_print!` write to it without an explicit argument. This requires the function to return `()` (unit); the expanded function will return `RenderResult`: + + ```rust + use mingling::macros::r_println; + + #[renderer(buffer)] + fn render_greeting(greeting: ResultGreeting) { + r_println!("Hello, {}!", *greeting); + // Returns RenderResult implicitly + } + ``` + + The `#[buffer]` extension is also available standalone as `#[buffer]` for use outside of `#[renderer]`/`#[help]` functions. 2. **[`macros:chain`]** The `#[chain]` macro's return type requirement has been relaxed. Previously, chain functions were required to return `Next` or `()` (with `()` auto-converting to `ResultEmpty`). Now, chain functions can also return `ChainProcess<ThisProgram>` directly, or omit the return type entirely (which defaults to `()` → `ResultEmpty`). diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index a34452d..d1691fb 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -42,6 +42,8 @@ pub mod macros { /// New Parser provided by the `picker` feature #[cfg(feature = "picker")] pub use arg_picker::macros::*; + /// `#[buffer]` - Wraps a unit-returning function to produce a `RenderResult`. + pub use mingling_macros::buffer; /// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method pub use mingling_macros::chain; /// `#[completion(EntryType)]` - Used to generate completion entry @@ -91,6 +93,12 @@ pub mod macros { /// `#[program_setup]` - Used to generate program setup #[cfg(feature = "extra_macros")] pub use mingling_macros::program_setup; + /// `r_print!` - Prints text to a `RenderResult` buffer (without newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_print; + /// `r_println!` - Prints text to a `RenderResult` buffer (with newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_println; #[doc(hidden)] pub use mingling_macros::register_chain; #[doc(hidden)] diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs index e57a5b9..763f6fc 100644 --- a/mingling_core/src/renderer/render_result.rs +++ b/mingling_core/src/renderer/render_result.rs @@ -132,8 +132,8 @@ impl RenderResult { /// result.print(", world!"); /// assert_eq!(result.deref(), "Hello, world!"); /// ``` - pub fn print(&mut self, text: &str) { - self.render_text.push_str(text); + pub fn print(&mut self, text: impl AsRef<str>) { + self.render_text.push_str(text.as_ref()); } /// Appends the given text followed by a newline to the rendered content. @@ -149,8 +149,8 @@ impl RenderResult { /// result.println("Second line"); /// assert_eq!(result.deref(), "First line\nSecond line\n"); /// ``` - pub fn println(&mut self, text: &str) { - self.render_text.push_str(text); + pub fn println(&mut self, text: impl AsRef<str>) { + self.render_text.push_str(text.as_ref()); self.render_text.push('\n'); } diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs index aff20e2..022761b 100644 --- a/mingling_macros/src/extensions.rs +++ b/mingling_macros/src/extensions.rs @@ -13,6 +13,9 @@ use syn::{Ident, Token}; #[cfg(feature = "extra_macros")] pub(crate) mod routeify; +/// Extension: `#[buffer]` — wraps a unit-returning function to return `RenderResult`. +pub(crate) mod buffer; + /// Parsed extensions from an attribute macro like `#[chain(routeify, other_ext)]`. pub(crate) struct Extensions { pub(crate) exts: Vec<Ident>, diff --git a/mingling_macros/src/extensions/buffer.rs b/mingling_macros/src/extensions/buffer.rs new file mode 100644 index 0000000..27eb612 --- /dev/null +++ b/mingling_macros/src/extensions/buffer.rs @@ -0,0 +1,95 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{ItemFn, ReturnType, Type, parse_macro_input}; + +/// Checks whether the return type is unit `()`. +fn is_unit_return_type(sig: &syn::Signature) -> bool { + match &sig.output { + ReturnType::Type(_, ty) => match &**ty { + Type::Tuple(tuple) => tuple.elems.is_empty(), + _ => false, + }, + ReturnType::Default => true, + } +} + +/// The `#[buffer]` attribute macro. +/// +/// Wraps a unit-returning function to produce a `::mingling::RenderResult` by +/// injecting a local `__render_result_buffer` variable. Inside the function +/// body, the `r_print!` / `r_println!` macros can write into the buffer. +/// +/// # Example +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_println}; +/// +/// #[buffer] +/// fn render_greeting(prev: Greeting) { +/// r_println!("Hello, {}!", *prev); +/// } +/// ``` +/// +/// Expands to: +/// +/// ```rust,ignore +/// fn render_greeting(prev: Greeting) -> mingling::RenderResult { +/// let mut __render_result_buffer = mingling::RenderResult::new(); +/// { +/// r_println!("Hello, {}!", *prev); +/// } +/// __render_result_buffer +/// } +/// ``` +pub(crate) fn buffer_impl(attr: TokenStream, item: TokenStream) -> TokenStream { + // Reject non-empty attribute arguments; #[buffer] must be bare + if !attr.is_empty() { + return syn::Error::new( + attr.into_iter().next().unwrap().span().into(), + "#[buffer] does not accept arguments", + ) + .to_compile_error() + .into(); + } + + // Parse the function item + let input_fn = parse_macro_input!(item as ItemFn); + + // Validate the function is not async + if input_fn.sig.asyncness.is_some() { + return syn::Error::new(input_fn.sig.span(), "Buffer function cannot be async") + .to_compile_error() + .into(); + } + + // Validate return type is unit + if !is_unit_return_type(&input_fn.sig) { + return syn::Error::new( + input_fn.sig.span(), + "#[buffer] function must not have a return value (must return `()`)", + ) + .to_compile_error() + .into(); + } + + // Get function attributes (excluding the buffer attribute) + let mut fn_attrs = input_fn.attrs.clone(); + fn_attrs.retain(|attr| !attr.path().is_ident("buffer")); + + let vis = &input_fn.vis; + let fn_name = &input_fn.sig.ident; + let inputs = &input_fn.sig.inputs; + let fn_body = &input_fn.block; + + let expanded = quote! { + #(#fn_attrs)* + #vis fn #fn_name(#inputs) -> ::mingling::RenderResult { + let mut __render_result_buffer = ::mingling::RenderResult::new(); + #fn_body + __render_result_buffer + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs index 720b20a..33bb094 100644 --- a/mingling_macros/src/func.rs +++ b/mingling_macros/src/func.rs @@ -8,5 +8,6 @@ pub(crate) mod node; pub(crate) mod pack; #[cfg(feature = "extra_macros")] pub(crate) mod pack_err; +pub(crate) mod r_print; #[cfg(feature = "comp")] pub(crate) mod suggest; diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs new file mode 100644 index 0000000..e81b544 --- /dev/null +++ b/mingling_macros/src/func/r_print.rs @@ -0,0 +1,62 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Token}; + +/// Parsed input for `r_println!` and `r_print!`. +/// +/// Two forms: +/// - `(ident, format_args...)` — explicit buffer +/// - `(format_args...)` — implicit `__render_result_buffer` +enum PrintInput { + Explicit { dst: Ident, args: TokenStream2 }, + Implicit { args: TokenStream2 }, +} + +impl Parse for PrintInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + // Peek: if the next token is an ident followed by a comma, it's the explicit form + if input.peek(Ident) && input.peek2(Token![,]) { + let dst: Ident = input.parse()?; + let _comma: Token![,] = input.parse()?; + let args: TokenStream2 = input.parse()?; + Ok(PrintInput::Explicit { dst, args }) + } else { + let args: TokenStream2 = input.parse()?; + Ok(PrintInput::Implicit { args }) + } + } +} + +fn expand_print(input: TokenStream, method: &str) -> TokenStream { + let parsed: PrintInput = match syn::parse(input) { + Ok(p) => p, + Err(e) => return e.to_compile_error().into(), + }; + + let method_ident = Ident::new(method, proc_macro2::Span::call_site()); + + let expanded = match parsed { + PrintInput::Explicit { dst, args } => { + quote! { + #dst.#method_ident(format!(#args)) + } + } + PrintInput::Implicit { args } => { + quote! { + __render_result_buffer.#method_ident(format!(#args)) + } + } + }; + + expanded.into() +} + +pub(crate) fn r_println(input: TokenStream) -> TokenStream { + expand_print(input, "println") +} + +pub(crate) fn r_print(input: TokenStream) -> TokenStream { + expand_print(input, "print") +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 462c6dd..cc18958 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -1282,6 +1282,104 @@ pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream { extensions::routeify::routeify_impl(attr, item) } +/// Wraps a unit-returning function to produce a `RenderResult`. +/// +/// The `#[buffer]` attribute macro injects a local `__render_result_buffer` +/// variable of type `::mingling::RenderResult` and changes the function's +/// return type to `::mingling::RenderResult`. Inside the body, use the +/// `r_print!` and `r_println!` macros to write into the buffer. +/// +/// # Example +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_println}; +/// +/// #[buffer] +/// fn render_my_type(prev: MyType) { +/// r_println!("Value: {:?}", *prev); +/// } +/// ``` +/// +/// This expands to: +/// +/// ```rust,ignore +/// fn render_my_type(prev: MyType) -> mingling::RenderResult { +/// let mut __render_result_buffer = mingling::RenderResult::new(); +/// { +/// r_println!("Value: {:?}", *prev); +/// } +/// __render_result_buffer +/// } +/// ``` +/// +/// # Requirements +/// +/// - The function must return `()` (unit). +/// - The function cannot be async. +#[proc_macro_attribute] +pub fn buffer(attr: TokenStream, item: TokenStream) -> TokenStream { + extensions::buffer::buffer_impl(attr, item) +} + +/// Prints text to a `RenderResult` buffer, with a trailing newline. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_println}; +/// +/// #[buffer] +/// fn render() { +/// r_println!("Hello, {}!", name); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// Pass a `RenderResult` variable as the first argument: +/// +/// ```rust,ignore +/// use mingling::macros::r_println; +/// use mingling::RenderResult; +/// +/// let mut r = RenderResult::new(); +/// r_println!(r, "value: {}", 42); +/// assert_eq!(&*r, "value: 42\n"); +/// ``` +#[proc_macro] +pub fn r_println(input: TokenStream) -> TokenStream { + func::r_print::r_println(input) +} + +/// Prints text to a `RenderResult` buffer, without a trailing newline. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_print}; +/// +/// #[buffer] +/// fn render() { +/// r_print!("Hello, "); +/// r_println!("world!"); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// ```rust,ignore +/// use mingling::macros::r_print; +/// use mingling::RenderResult; +/// +/// let mut r = RenderResult::new(); +/// r_print!(r, "value: {}", 42); +/// assert_eq!(&*r, "value: 42"); +/// ``` +#[proc_macro] +pub fn r_print(input: TokenStream) -> TokenStream { + func::r_print::r_print(input) +} + /// Derive macro for automatically implementing the `Grouped` trait on a struct. /// /// The `#[derive(Grouped)]` macro: |
