aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md44
-rw-r--r--examples/example-unit-test/src/main.rs8
-rw-r--r--mingling/src/example_docs.rs8
-rw-r--r--mingling/src/lib.rs12
-rw-r--r--mingling/src/setups/repl_basic.rs2
-rw-r--r--mingling_core/src/renderer/render_result.rs444
-rw-r--r--mingling_core/tests/test-all/tests/integration.rs8
-rw-r--r--mingling_core/tests/test-basic/tests/integration.rs2
-rw-r--r--mingling_core/tests/test-dispatch-tree/tests/integration.rs2
-rw-r--r--mingling_core/tests/test-repl/tests/integration.rs2
-rw-r--r--mingling_core/tests/test-structural-renderer/tests/integration.rs5
-rw-r--r--mingling_macros/src/func/r_print.rs8
-rw-r--r--mingling_macros/src/lib.rs66
13 files changed, 525 insertions, 86 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b90536e..b2d2c42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -367,6 +367,50 @@ None
_No behavioral changes — this is a pure API migration from `Grouped` to `Routable` for routing methods._
+6. **[`core`]** **[BREAKING]** Removed the `Deref<Target = str>` implementation from `RenderResult`. Previously, `RenderResult` implemented `Deref<Target = str>`, delegating to the internal `render_text: String` field. This implementation has been removed as part of the internal refactoring of `RenderResult` from a single `String` field to a `Vec<(String, RenderResultMode)>` buffer.
+
+ The `RenderResult` struct has been restructured internally:
+
+ - **Removed:** `render_text: String` — replaced by a buffered storage format
+ - **Added:** `render_buffer: Vec<(String, RenderResultMode)>` — a list of (text, output-mode) pairs
+ - **Added:** `immediate_output: bool` — flag for real-time flushing
+
+ **New `RenderResultMode` enum** (`Stdout` / `Stderr`) has been added to distinguish output streams at the buffer level. Both variants are re-exported as `mingling::RenderResultMode`.
+
+ **New methods added to `RenderResult`:**
+
+ - `append_to_buffer(text, mode)` / `append_line_to_buffer(text, mode)` — Append text with an explicit output mode (`Stdout` or `Stderr`)
+ - `eprint(text)` / `eprintln(text)` — Append text marked for stderr output
+ - `immediate_output()` — Enable real-time output flushing
+ - `std_print()` — Flush all buffered content to stdout/stderr
+ - `len()` / `is_empty()` — Character count and emptiness check (based on the buffer)
+ - `trim_buffer(self) -> RenderResult` — Trim whitespace from the buffer ends, returning a new `RenderResult`
+
+ **`r_eprint!` and `r_eprintln!` macros** have been added to `mingling_macros` and re-exported via `mingling::macros::r_eprint` / `mingling::macros::r_eprintln` and `mingling::prelude::*`. These work analogously to `r_print!` / `r_println!` but target stderr output (calling `RenderResult::eprint` / `RenderResult::eprintln` under the hood). Both support implicit buffer mode (inside `#[buffer]` functions) and explicit buffer mode (passing a `RenderResult` as the first argument).
+
+ The `Display` implementation for `RenderResult` is now: `write!(f, "{}", render_result_to_string(self).trim())` — this trims leading and trailing whitespace from the rendered output, making formatting more predictable and avoiding stray newlines or spaces in display contexts.
+
+ **Migration guide:**
+
+ Code that relied on `Deref<Target = str>` (e.g., `&*result`, `result.as_ref()`, or passing `&RenderResult` where `&str` was expected) must be updated to use one of the following:
+
+ ```rust
+ // Before — relied on Deref
+ fn takes_str(s: &str) { /* ... */ }
+ takes_str(&result);
+
+ // After — convert explicitly
+ let result_string: String = result.to_string();
+ takes_str(&result_string);
+
+ // Or use the Display impl directly
+ println!("{}", result);
+ ```
+
+ The `is_empty()` method now checks the buffer length (in characters) rather than checking `render_text.is_empty()`. The `Display` implementation no longer adds a trailing newline (previously `writeln!` was used; now `write!` is used) — existing code that relied on the trailing newline in `Display` may need adjustment. Additionally, the `to_string()` call on `RenderResult` now trims leading and trailing whitespace from the rendered text via the `Display` implementation, whereas previously the raw content was preserved without trimming.
+
+ All examples and internal usages have been updated across the codebase to reflect these changes (e.g., `repl_basic_setup` now calls `println!("{}", r.result)` instead of `println!("{}", r.result.trim())`, since `Display` no longer adds a trailing newline).
+
---
## Release 0.2.2 (2026-07-10)
diff --git a/examples/example-unit-test/src/main.rs b/examples/example-unit-test/src/main.rs
index b85ff01..e9169df 100644
--- a/examples/example-unit-test/src/main.rs
+++ b/examples/example-unit-test/src/main.rs
@@ -42,25 +42,25 @@ mod tests {
#[test]
fn test_render_result_name() {
let r = render_result_name(ResultName::new("Peter".into()));
- assert_eq!(r.to_string().as_str(), "Hello, Peter!\n")
+ assert_eq!(r.to_string().as_str(), "Hello, Peter!")
}
#[test]
fn test_render_error_no_name_provided() {
let r = render_error_no_name_provided(ErrorNoNameProvided::default());
- assert_eq!(r.to_string().as_str(), "No name provided\n")
+ assert_eq!(r.to_string().as_str(), "No name provided")
}
#[test]
fn test_render_error_name_not_available() {
let r = render_error_name_not_available(ErrorNameNotAvailable::default());
- assert_eq!(r.to_string().as_str(), "Name not available\n")
+ assert_eq!(r.to_string().as_str(), "Name not available")
}
#[test]
fn test_render_error_name_too_long() {
let r = render_error_name_too_long(ErrorNameTooLong::new(17));
- assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10\n")
+ assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10")
}
// --------- IMPORTANT ---------
}
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index bdefb5a..3b36f00 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -2796,25 +2796,25 @@ pub mod example_structural_renderer {}
/// #[test]
/// fn test_render_result_name() {
/// let r = render_result_name(ResultName::new("Peter".into()));
-/// assert_eq!(r.to_string().as_str(), "Hello, Peter!\n")
+/// assert_eq!(r.to_string().as_str(), "Hello, Peter!")
/// }
///
/// #[test]
/// fn test_render_error_no_name_provided() {
/// let r = render_error_no_name_provided(ErrorNoNameProvided::default());
-/// assert_eq!(r.to_string().as_str(), "No name provided\n")
+/// assert_eq!(r.to_string().as_str(), "No name provided")
/// }
///
/// #[test]
/// fn test_render_error_name_not_available() {
/// let r = render_error_name_not_available(ErrorNameNotAvailable::default());
-/// assert_eq!(r.to_string().as_str(), "Name not available\n")
+/// assert_eq!(r.to_string().as_str(), "Name not available")
/// }
///
/// #[test]
/// fn test_render_error_name_too_long() {
/// let r = render_error_name_too_long(ErrorNameTooLong::new(17));
-/// assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10\n")
+/// assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10")
/// }
/// // --------- IMPORTANT ---------
/// }
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index f5362d5..3539647 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -98,6 +98,12 @@ pub mod macros {
/// `#[program_setup]` - Used to generate program setup
#[cfg(feature = "extra_macros")]
pub use mingling_macros::program_setup;
+ /// `r_eprint!` - Prints text to a `RenderResult` error buffer (without newline).
+ /// See the macro documentation for implicit vs. explicit buffer usage.
+ pub use mingling_macros::r_eprint;
+ /// `r_eprintln!` - Prints text to a `RenderResult` error buffer (with newline).
+ /// See the macro documentation for implicit vs. explicit buffer usage.
+ pub use mingling_macros::r_eprintln;
/// `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;
@@ -229,6 +235,12 @@ pub mod prelude {
/// Like `pack!` but also marks the type for structured output
#[cfg(all(feature = "macros", feature = "structural_renderer"))]
pub use mingling_macros::pack_structural;
+ /// `r_eprint!` - Prints text to a `RenderResult` error buffer (without newline).
+ /// See the macro documentation for implicit vs. explicit buffer usage.
+ pub use mingling_macros::r_eprint;
+ /// `r_eprintln!` - Prints text to a `RenderResult` error buffer (with newline).
+ /// See the macro documentation for implicit vs. explicit buffer usage.
+ pub use mingling_macros::r_eprintln;
/// `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;
diff --git a/mingling/src/setups/repl_basic.rs b/mingling/src/setups/repl_basic.rs
index cf04372..150048b 100644
--- a/mingling/src/setups/repl_basic.rs
+++ b/mingling/src/setups/repl_basic.rs
@@ -75,7 +75,7 @@ where
fn setup(self, program: &mut Program<C>) {
program.with_hook(ProgramHook::empty().on_repl_receive_result(|r| {
if !r.result.is_empty() {
- println!("{}", r.result.trim())
+ println!("{}", r.result)
}
}));
}
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index 2c60fa4..af63cdd 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -1,13 +1,24 @@
use std::{
fmt::{Display, Formatter},
io::Write,
- ops::Deref,
};
+use crate::RenderResultMode::{Stderr, Stdout};
+
/// Render result, containing the rendered text content.
#[derive(Default, Debug, PartialEq)]
pub struct RenderResult {
- render_text: String,
+ /// Whether the output should be written immediately.
+ ///
+ /// When set to `true`, rendered content will be flushed to stdout/stderr
+ /// in real time while also being collected in the render buffer.
+ immediate_output: bool,
+
+ /// The buffered render output, stored as a list of (text, mode) pairs.
+ ///
+ /// Each entry contains a rendered string together with a `RenderResultMode`
+ /// indicating whether it should be output to stdout or stderr.
+ render_buffer: Vec<(String, RenderResultMode)>,
/// The exit code to return from the rendering process.
///
@@ -16,12 +27,32 @@ pub struct RenderResult {
pub exit_code: i32,
}
+/// Enum representing the output mode for render results.
+///
+/// This determines whether the rendered content should be directed to standard
+/// output or standard error.
+///
+/// # Variants
+///
+/// * `Stdout` - Output will be written to standard output (stdout).
+/// * `Stderr` - Output will be written to standard error (stderr).
+#[repr(u8)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
+pub enum RenderResultMode {
+ /// Standard output (stdout).
+ #[default]
+ Stdout = 0,
+
+ /// Standard error (stderr).
+ Stderr = 1,
+}
+
impl Write for RenderResult {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let s = std::str::from_utf8(buf).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "not valid UTF-8")
})?;
- self.render_text.push_str(s);
+ self.append_to_buffer(s, Stdout);
Ok(buf.len())
}
@@ -32,15 +63,7 @@ impl Write for RenderResult {
impl Display for RenderResult {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- writeln!(f, "{}", self.render_text.trim())
- }
-}
-
-impl Deref for RenderResult {
- type Target = str;
-
- fn deref(&self) -> &Self::Target {
- &self.render_text
+ write!(f, "{}", render_result_to_string(self).trim())
}
}
@@ -69,40 +92,31 @@ impl_from_int!(i32, i16, i8, u32, u16, u8, usize);
impl From<&String> for RenderResult {
fn from(value: &String) -> Self {
- RenderResult {
- render_text: value.clone(),
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<String> for RenderResult {
fn from(value: String) -> Self {
- RenderResult {
- render_text: value,
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<&str> for RenderResult {
fn from(value: &str) -> Self {
- RenderResult {
- render_text: value.to_string(),
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<RenderResult> for String {
fn from(result: RenderResult) -> Self {
- result.render_text
+ render_result_to_string(&result)
}
}
impl From<&RenderResult> for String {
fn from(result: &RenderResult) -> Self {
- result.render_text.clone()
+ render_result_to_string(result)
}
}
@@ -124,21 +138,95 @@ impl RenderResult {
Self::default()
}
+ /// Marks the render result for immediate output, bypassing any buffering or
+ /// deferred rendering.
+ ///
+ /// When set, the rendered content will be both collected in the result and
+ /// immediately flushed to stdout/stderr in real time, rather than being
+ /// deferred for later display.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.immediate_output();
+ /// ```
+ pub fn immediate_output(&mut self) -> &mut Self {
+ self.immediate_output = true;
+ self
+ }
+
+ /// Appends the given text and mode to the render buffer.
+ ///
+ /// Unlike `print` and `println` which only store plain text in a single string,
+ /// this method stores the text along with a `RenderResultMode` that indicates
+ /// whether the output should be directed to stdout or stderr. This allows for
+ /// more fine-grained control over output routing when the buffer is later flushed.
+ ///
+ /// # Arguments
+ ///
+ /// * `text` - The text content to append to the buffer.
+ /// * `mode` - The output mode (`Stdout` or `Stderr`) indicating where the text
+ /// should be directed.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_to_buffer("Error message", RenderResultMode::Stderr);
+ /// ```
+ pub fn append_to_buffer(&mut self, text: impl Into<String>, mode: RenderResultMode) {
+ self.render_buffer.push((text.into(), mode));
+ }
+
+ /// Appends the given text followed by a newline, along with the mode, to the render buffer.
+ ///
+ /// This is a convenience method that calls `append_to_buffer` for the text and then
+ /// appends a newline with the same mode.
+ ///
+ /// # Arguments
+ ///
+ /// * `text` - The text content to append to the buffer.
+ /// * `mode` - The output mode (`Stdout` or `Stderr`) indicating where the text
+ /// should be directed.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_line_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_line_to_buffer("Warning", RenderResultMode::Stderr);
+ /// ```
+ pub fn append_line_to_buffer(&mut self, text: impl Into<String>, mode: RenderResultMode) {
+ self.append_to_buffer(text, mode);
+ self.append_to_buffer("\n", mode);
+ }
+
/// Appends the given text to the rendered content.
///
/// # Examples
///
/// ```
/// use mingling_core::RenderResult;
- /// use std::ops::Deref;
///
/// let mut result = RenderResult::default();
/// result.print("Hello");
/// result.print(", world!");
- /// assert_eq!(result.deref(), "Hello, world!");
+ /// assert_eq!(result.to_string(), "Hello, world!");
/// ```
- pub fn print(&mut self, text: impl AsRef<str>) {
- self.render_text.push_str(text.as_ref());
+ pub fn print(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ print!("{}", text)
+ }
+ self.append_to_buffer(text, Stdout);
}
/// Appends the given text followed by a newline to the rendered content.
@@ -147,16 +235,58 @@ impl RenderResult {
///
/// ```
/// use mingling_core::RenderResult;
- /// use std::ops::Deref;
///
/// let mut result = RenderResult::default();
/// result.println("First line");
/// result.println("Second line");
- /// assert_eq!(result.deref(), "First line\nSecond line\n");
+ /// assert_eq!(result.to_string(), "First line\nSecond line");
+ /// ```
+ pub fn println(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ println!("{}", text)
+ }
+ self.append_line_to_buffer(text, Stdout);
+ }
+
+ /// Appends the given text to the rendered content, marking it for stderr output.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.eprint("Hello");
+ /// result.eprint(", world!");
+ /// assert_eq!(result.to_string(), "Hello, world!");
/// ```
- pub fn println(&mut self, text: impl AsRef<str>) {
- self.render_text.push_str(text.as_ref());
- self.render_text.push('\n');
+ pub fn eprint(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ eprint!("{}", text)
+ }
+ self.append_to_buffer(text, Stderr);
+ }
+
+ /// Appends the given text followed by a newline to the rendered content, marking it for stderr output.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.eprintln("First line");
+ /// result.eprintln("Second line");
+ /// assert_eq!(result.to_string(), "First line\nSecond line");
+ /// ```
+ pub fn eprintln(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ println!("{}", text)
+ }
+ self.append_line_to_buffer(text, Stderr);
}
/// Clears all rendered content.
@@ -174,7 +304,148 @@ impl RenderResult {
/// assert!(result.is_empty());
/// ```
pub fn clear(&mut self) {
- self.render_text.clear();
+ self.render_buffer.clear();
+ }
+
+ /// Outputs all buffered content to stdout and stderr according to their respective modes.
+ ///
+ /// Iterates through the render buffer and prints each buffered string to the appropriate
+ /// output stream — stdout for `Stdout` entries and stderr for `Stderr` entries.
+ ///
+ /// This method is typically used to flush the buffered output at the end of rendering,
+ /// ensuring that all output is displayed in the correct order and to the correct stream.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_to_buffer("Error", RenderResultMode::Stderr);
+ /// result.std_print(); // prints "Hello" to stdout and "Error" to stderr
+ /// ```
+ pub fn std_print(&self) {
+ for (content, mode) in self.render_buffer.iter() {
+ match mode {
+ Stdout => print!("{}", content),
+ Stderr => eprint!("{}", content),
+ }
+ }
+ }
+
+ /// Returns the total number of characters (in terms of `char` count) in the buffered render output.
+ ///
+ /// This counts the length across all buffered entries, regardless of whether they are
+ /// destined for stdout or stderr.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.print("Hello");
+ /// result.print(", 世界");
+ /// assert_eq!(result.len(), 9); // "Hello, 世界" has 9 chars
+ /// ```
+ pub fn len(&self) -> usize {
+ self.render_buffer
+ .iter()
+ .map(|(s, _)| s.chars().count())
+ .sum()
+ }
+
+ /// Returns `true` if the buffered render output contains no characters.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// assert!(result.is_empty());
+ /// result.print("Hello");
+ /// assert!(!result.is_empty());
+ /// ```
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Trims leading and trailing whitespace from the buffered render output.
+ ///
+ /// This method processes the render buffer as follows:
+ /// - If the buffer is empty, it returns `self` unchanged.
+ /// - If there is only one entry, whitespace is trimmed from both the start and end of that
+ /// single entry.
+ /// - If there are multiple entries, whitespace is trimmed from the start of the first entry
+ /// and the end of the last entry.
+ ///
+ /// Whitespace in the middle entries is preserved. This is useful for cleaning up output
+ /// without removing intentional spacing between separately buffered segments.
+ ///
+ /// # Returns
+ ///
+ /// A new `RenderResult` with the same `immediate_output` flag and `exit_code`, but with
+ /// trimmed text content.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.print(" Hello, world! ");
+ /// let trimmed = result.trim_buffer();
+ /// assert_eq!(trimmed.to_string().trim(), "Hello, world!");
+ /// ```
+ pub fn trim_buffer(self) -> RenderResult {
+ if self.render_buffer.is_empty() {
+ return self;
+ }
+
+ let mut buffer = self.render_buffer;
+ if buffer.len() == 1 {
+ // Only one entry: trim both start and end of this single entry
+ let (text, mode) = buffer.remove(0);
+ buffer.push((text.trim().to_string(), mode));
+ } else {
+ // Multiple entries: trim start of first, trim end of last
+ let first_len = buffer.len();
+
+ // Trim start of first entry
+ let (first_text, first_mode) = buffer.remove(0);
+ let trimmed_first = first_text.trim_start().to_string();
+ buffer.insert(0, (trimmed_first, first_mode));
+
+ // Trim end of last entry
+ let (last_text, last_mode) = buffer.remove(first_len - 1);
+ let trimmed_last = last_text.trim_end().to_string();
+ buffer.push((trimmed_last, last_mode));
+ }
+
+ RenderResult {
+ render_buffer: buffer,
+ immediate_output: self.immediate_output,
+ exit_code: self.exit_code,
+ }
+ }
+}
+
+#[inline(always)]
+fn render_result_to_string(result: &RenderResult) -> String {
+ let mut buffer = String::new();
+ for item in result.render_buffer.iter() {
+ buffer += &item.0;
+ }
+ buffer
+}
+
+#[inline(always)]
+fn string_to_render_result(string: impl Into<String>, mode: RenderResultMode) -> RenderResult {
+ RenderResult {
+ render_buffer: vec![(string.into(), mode)],
+ ..Default::default()
}
}
@@ -191,20 +462,6 @@ mod tests {
}
#[test]
- fn print_appends_text() {
- let mut result = RenderResult::default();
- result.print("Hello");
- assert_eq!(result.deref(), "Hello");
- }
-
- #[test]
- fn println_appends_text_with_newline() {
- let mut result = RenderResult::default();
- result.println("Hello");
- assert_eq!(result.deref(), "Hello\n");
- }
-
- #[test]
fn clear_empties_content() {
let mut result = RenderResult::default();
result.print("something");
@@ -222,14 +479,6 @@ mod tests {
}
#[test]
- fn write_appends_utf8_bytes() {
- let mut result = RenderResult::default();
- let n = IoWrite::write(&mut result, b"hello").unwrap();
- assert_eq!(n, 5);
- assert_eq!(result.deref(), "hello");
- }
-
- #[test]
fn write_with_invalid_utf8_returns_error() {
let mut result = RenderResult::default();
let err = IoWrite::write(&mut result, &[0xff, 0xfe]).unwrap_err();
@@ -241,16 +490,7 @@ mod tests {
let mut result = RenderResult::default();
result.print(" hello world \n");
let formatted = format!("{}", result);
- assert_eq!(formatted, "hello world\n");
- }
-
- #[test]
- fn deref_exposes_inner_text_as_str() {
- let mut result = RenderResult::default();
- result.print("test");
-
- let s: &str = &result;
- assert_eq!(s, "test");
+ assert_eq!(formatted, "hello world");
}
#[test]
@@ -270,4 +510,72 @@ mod tests {
// original is still usable
assert!(!result.is_empty());
}
+
+ #[test]
+ fn trim_empty_buffer_returns_self() {
+ let result = RenderResult::default();
+ let trimmed = result.trim_buffer();
+ assert!(trimmed.is_empty());
+ assert_eq!(trimmed.exit_code, 0);
+ }
+
+ #[test]
+ fn trim_single_entry_trims_both_ends() {
+ let mut result = RenderResult::default();
+ result.print(" Hello, world! ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.to_string(), "Hello, world!");
+ }
+
+ #[test]
+ fn trim_single_entry_nothing_to_trim() {
+ let mut result = RenderResult::default();
+ result.print("Hello");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.to_string(), "Hello");
+ }
+
+ #[test]
+ fn trim_multiple_entries_trims_first_start_and_last_end() {
+ let mut result = RenderResult::default();
+ result.print(" Hello");
+ result.print(" World ");
+ result.print("! ");
+ let trimmed = result.trim_buffer();
+ // first entry trim_start: "Hello"
+ // middle entry unchanged: " World "
+ // last entry trim_end: "!"
+ assert_eq!(trimmed.to_string(), "Hello World !");
+ }
+
+ #[test]
+ fn trim_multiple_entries_only_whitespace_first_entry() {
+ let mut result = RenderResult::default();
+ result.print(" ");
+ result.print("Hello");
+ result.print(" ");
+ let trimmed = result.trim_buffer();
+ // first entry trim_start: ""
+ // middle entry unchanged: "Hello"
+ // last entry trim_end: ""
+ assert_eq!(trimmed.to_string(), "Hello");
+ }
+
+ #[test]
+ fn trim_preserves_exit_code() {
+ let mut result = RenderResult::new();
+ result.exit_code = 42;
+ result.print(" test ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.exit_code, 42);
+ }
+
+ #[test]
+ fn trim_preserves_stderr_mode() {
+ let mut result = RenderResult::default();
+ result.eprint(" error ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.render_buffer[0].1, RenderResultMode::Stderr);
+ assert_eq!(trimmed.to_string(), "error");
+ }
}
diff --git a/mingling_core/tests/test-all/tests/integration.rs b/mingling_core/tests/test-all/tests/integration.rs
index 3581702..08703b0 100644
--- a/mingling_core/tests/test-all/tests/integration.rs
+++ b/mingling_core/tests/test-all/tests/integration.rs
@@ -1,13 +1,13 @@
use mingling::Flag;
-use mingling::StructuralRenderer;
-use mingling::StructuralRendererSetting;
use mingling::MockProgramCollect;
use mingling::NextProcess;
-use mingling::StructuralData;
use mingling::Node;
use mingling::Program;
use mingling::RenderResult;
use mingling::StringVec;
+use mingling::StructuralData;
+use mingling::StructuralRenderer;
+use mingling::StructuralRendererSetting;
use mingling::comp::{ShellContext, ShellFlag, Suggest};
use mingling::core_res::ResREPL;
use mingling::hook::ProgramHook;
@@ -86,7 +86,7 @@ fn test_render_result_default() {
fn test_render_result_print() {
let mut r = RenderResult::default();
r.print("hello");
- assert_eq!(&*r, "hello");
+ assert_eq!(r.to_string().as_str(), "hello");
}
// StructuralRenderer
diff --git a/mingling_core/tests/test-basic/tests/integration.rs b/mingling_core/tests/test-basic/tests/integration.rs
index 7cd7b8c..e51992c 100644
--- a/mingling_core/tests/test-basic/tests/integration.rs
+++ b/mingling_core/tests/test-basic/tests/integration.rs
@@ -60,7 +60,7 @@ fn test_render_result_default() {
fn test_render_result_print() {
let mut r = RenderResult::default();
r.print("hello");
- assert_eq!(&*r, "hello");
+ assert_eq!(r.to_string().as_str(), "hello");
}
#[test]
diff --git a/mingling_core/tests/test-dispatch-tree/tests/integration.rs b/mingling_core/tests/test-dispatch-tree/tests/integration.rs
index 7cd7b8c..e51992c 100644
--- a/mingling_core/tests/test-dispatch-tree/tests/integration.rs
+++ b/mingling_core/tests/test-dispatch-tree/tests/integration.rs
@@ -60,7 +60,7 @@ fn test_render_result_default() {
fn test_render_result_print() {
let mut r = RenderResult::default();
r.print("hello");
- assert_eq!(&*r, "hello");
+ assert_eq!(r.to_string().as_str(), "hello");
}
#[test]
diff --git a/mingling_core/tests/test-repl/tests/integration.rs b/mingling_core/tests/test-repl/tests/integration.rs
index 1792525..4de0e8f 100644
--- a/mingling_core/tests/test-repl/tests/integration.rs
+++ b/mingling_core/tests/test-repl/tests/integration.rs
@@ -59,5 +59,5 @@ fn test_render_result_default() {
fn test_render_result_print() {
let mut r = RenderResult::default();
r.print("hello");
- assert_eq!(&*r, "hello");
+ assert_eq!(r.to_string().as_str(), "hello");
}
diff --git a/mingling_core/tests/test-structural-renderer/tests/integration.rs b/mingling_core/tests/test-structural-renderer/tests/integration.rs
index 3c3c6db..0bcf53a 100644
--- a/mingling_core/tests/test-structural-renderer/tests/integration.rs
+++ b/mingling_core/tests/test-structural-renderer/tests/integration.rs
@@ -1,4 +1,4 @@
-use mingling::{StructuralRenderer, StructuralRendererSetting, RenderResult, StructuralData};
+use mingling::{RenderResult, StructuralData, StructuralRenderer, StructuralRendererSetting};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize, StructuralData)]
@@ -17,7 +17,8 @@ fn test_data() -> TestData {
#[test]
fn test_render_disable() {
let mut r = RenderResult::default();
- let result = StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Disable, &mut r);
+ let result =
+ StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Disable, &mut r);
assert!(result.is_ok());
assert!(r.is_empty());
}
diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs
index e81b544..0f77b2b 100644
--- a/mingling_macros/src/func/r_print.rs
+++ b/mingling_macros/src/func/r_print.rs
@@ -60,3 +60,11 @@ pub(crate) fn r_println(input: TokenStream) -> TokenStream {
pub(crate) fn r_print(input: TokenStream) -> TokenStream {
expand_print(input, "print")
}
+
+pub(crate) fn r_eprintln(input: TokenStream) -> TokenStream {
+ expand_print(input, "eprintln")
+}
+
+pub(crate) fn r_eprint(input: TokenStream) -> TokenStream {
+ expand_print(input, "eprint")
+}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 56ed999..5e7a40f 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -1412,6 +1412,72 @@ pub fn r_print(input: TokenStream) -> TokenStream {
func::r_print::r_print(input)
}
+/// Prints text to a `RenderResult` buffer (standard error style), with a trailing newline.
+///
+/// This macro works identically to `r_println!` but conceptually targets
+/// "error output" — it writes into a `RenderResult` buffer with a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_eprintln};
+///
+/// #[buffer]
+/// fn render() {
+/// r_eprintln!("Error: {}", err_msg);
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// Pass a `RenderResult` variable as the first argument:
+///
+/// ```rust,ignore
+/// use mingling::macros::r_eprintln;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_eprintln!(r, "error: {}", 42);
+/// assert_eq!(&*r, "error: 42\n");
+/// ```
+#[proc_macro]
+pub fn r_eprintln(input: TokenStream) -> TokenStream {
+ func::r_print::r_eprintln(input)
+}
+
+/// Prints text to a `RenderResult` buffer (standard error style), without a trailing newline.
+///
+/// This macro works identically to `r_print!` but conceptually targets
+/// "error output" — it writes into a `RenderResult` buffer without a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_eprint};
+///
+/// #[buffer]
+/// fn render() {
+/// r_eprint!("Error: ");
+/// r_eprintln!("something went wrong");
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// ```rust,ignore
+/// use mingling::macros::r_eprint;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_eprint!(r, "error: ");
+/// r_eprintln!(r, "42");
+/// assert_eq!(&*r, "error: 42\n");
+/// ```
+#[proc_macro]
+pub fn r_eprint(input: TokenStream) -> TokenStream {
+ func::r_print::r_eprint(input)
+}
+
/// Derive macro for automatically implementing the `Grouped` trait on a struct.
///
/// The `#[derive(Grouped)]` macro: