aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md21
-rw-r--r--mingling/src/lib.rs8
-rw-r--r--mingling_core/src/renderer/render_result.rs8
-rw-r--r--mingling_macros/src/extensions.rs3
-rw-r--r--mingling_macros/src/extensions/buffer.rs95
-rw-r--r--mingling_macros/src/func.rs1
-rw-r--r--mingling_macros/src/func/r_print.rs62
-rw-r--r--mingling_macros/src/lib.rs98
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: