Skip to main content

standout_render/template/
functions.rs

1//! Core rendering functions.
2//!
3//! # Function Hierarchy
4//!
5//! The render functions form a layered hierarchy, from simple to fully explicit:
6//!
7//! ## Basic Rendering (template → styled string)
8//!
9//! | Function | Output Mode | Color Mode | Use When |
10//! |----------|-------------|------------|----------|
11//! | [`render`] | Auto-detect | Auto-detect | Simple cases, let Standout decide |
12//! | [`render_with_output`] | Explicit | Auto-detect | Honoring `--output` CLI flag |
13//! | [`render_with_mode`] | Explicit | Explicit | Tests, or forcing light/dark mode |
14//!
15//! ## Auto-Dispatch (render or serialize based on mode)
16//!
17//! For structured modes (Json, Yaml, Csv, Xml), these skip templating and
18//! serialize data directly. For text modes, they render the template.
19//!
20//! | Function | Extra Features |
21//! |----------|----------------|
22//! | [`render_auto`] | Basic auto-dispatch |
23//! | [`render_auto_with_spec`] | CSV column specification |
24//! | [`render_auto_with_context`] | Context injection |
25//!
26//! ## With Context Injection
27//!
28//! Inject additional values (beyond handler data) into templates:
29//!
30//! | Function | Structured Output |
31//! |----------|-------------------|
32//! | [`render_with_context`] | No (template only) |
33//! | [`render_auto_with_context`] | Yes (auto-dispatch) |
34//!
35//! # Two-Pass Rendering
36//!
37//! Templates use tag-based syntax for styling: `[name]content[/name]`
38//!
39//! The rendering process works in two passes:
40//! 1. MiniJinja pass: Variable substitution and template logic
41//! 2. BBParser pass: Style tag processing (`[tag]...[/tag]`)
42//!
43//! This allows templates like:
44//! ```text
45//! [title]{{ data.title }}[/title]: [count]{{ items | length }}[/count] items
46//! ```
47//!
48//! # Feature Support Matrix
49//!
50//! Different rendering approaches support different features:
51//!
52//! | Approach | Includes | Per-call Mode | Styles | Use Case |
53//! |----------|----------|---------------|--------|----------|
54//! | [`Renderer`] | ✓ | ✓* | ✓ | Pre-compiled templates, hot reload |
55//! | [`App::render`] | ✓ | ✓ | ✓ | CLI apps with embedded templates |
56//! | [`render`] / [`render_auto`] | ✗ | ✓ | ✓ | One-off template strings |
57//!
58//! *Use [`Renderer::set_output_mode`] to change mode between renders.
59//!
60//! ## Template Includes
61//!
62//! Template includes (`{% include "partial" %}`) require a template registry.
63//! The standalone `render*` functions take a template string, not a name,
64//! so they cannot resolve includes to other templates.
65//!
66//! For includes, use either:
67//! - [`Renderer`] with [`add_template`](Renderer::add_template) or
68//!   [`with_embedded_source`](Renderer::with_embedded_source)
69//! - `standout_render::cli::App` with embedded templates via the builder (requires `standout` crate)
70//!
71//! [`Renderer`]: super::renderer::Renderer
72//! [`Renderer::set_output_mode`]: super::renderer::Renderer::set_output_mode
73
74use serde::Serialize;
75use standout_bbparser::{BBParser, TagTransform, UnknownTagBehavior};
76use std::collections::HashMap;
77
78use super::engine::{MiniJinjaEngine, TemplateEngine};
79use crate::context::{ContextRegistry, RenderContext};
80use crate::error::RenderError;
81use crate::output::OutputMode;
82use crate::style::Styles;
83use crate::tabular::FlatDataSpec;
84use crate::theme::{detect_color_mode, detect_icon_mode, ColorMode, Theme};
85
86/// Maps OutputMode to BBParser's TagTransform.
87fn output_mode_to_transform(mode: OutputMode) -> TagTransform {
88    match mode {
89        OutputMode::Auto => {
90            if mode.should_use_color() {
91                TagTransform::Apply
92            } else {
93                TagTransform::Remove
94            }
95        }
96        OutputMode::Term => TagTransform::Apply,
97        OutputMode::Text => TagTransform::Remove,
98        OutputMode::TermDebug => TagTransform::Keep,
99        // Structured modes shouldn't reach here (filtered out before)
100        OutputMode::Json | OutputMode::Yaml | OutputMode::Xml | OutputMode::Csv => {
101            TagTransform::Remove
102        }
103    }
104}
105
106/// Post-processes rendered output with BBParser to apply style tags.
107///
108/// This is the second pass of the two-pass rendering system.
109pub fn apply_style_tags(output: &str, styles: &Styles, mode: OutputMode) -> String {
110    let transform = output_mode_to_transform(mode);
111    let resolved_styles = styles.to_resolved_map();
112    let parser =
113        BBParser::new(resolved_styles, transform).unknown_behavior(UnknownTagBehavior::Passthrough);
114    parser.parse(output)
115}
116
117/// Result of rendering that includes both formatted and raw output.
118///
119/// This struct is used when the caller needs both the terminal-formatted output
120/// (with ANSI codes) and the raw output (with style tags but no ANSI codes).
121/// The raw output is useful for piping to external commands.
122#[derive(Debug, Clone)]
123pub struct RenderResult {
124    /// The formatted output with ANSI codes applied (for terminal display)
125    pub formatted: String,
126    /// The raw output with `[tag]...[/tag]` markers but no ANSI codes.
127    /// This is the intermediate output after template rendering but before
128    /// style tag processing. Suitable for piping.
129    pub raw: String,
130}
131
132impl RenderResult {
133    /// Creates a new RenderResult with both formatted and raw versions.
134    pub fn new(formatted: String, raw: String) -> Self {
135        Self { formatted, raw }
136    }
137
138    /// Creates a RenderResult where formatted and raw are the same.
139    /// Use this for output that doesn't need style tag processing
140    /// (e.g., JSON output, error messages).
141    pub fn plain(text: String) -> Self {
142        Self {
143            formatted: text.clone(),
144            raw: text,
145        }
146    }
147}
148
149/// Validates a template for unknown style tags.
150///
151/// This function renders the template (performing variable substitution) and then
152/// checks for any style tags that are not defined in the theme. Use this during
153/// development or CI to catch typos in templates.
154///
155/// # Arguments
156///
157/// * `template` - A minijinja template string
158/// * `data` - Any serializable data to pass to the template
159/// * `theme` - Theme definitions that define valid style names
160///
161/// # Returns
162///
163/// Returns `Ok(())` if all style tags in the template are defined in the theme.
164/// Returns `Err` with the list of unknown tags if any are found.
165///
166/// # Example
167///
168/// ```rust
169/// use standout_render::{validate_template, Theme};
170/// use console::Style;
171/// use serde::Serialize;
172///
173/// #[derive(Serialize)]
174/// struct Data { name: String }
175///
176/// let theme = Theme::new().add("title", Style::new().bold());
177///
178/// // Valid template passes
179/// let result = validate_template(
180///     "[title]{{ name }}[/title]",
181///     &Data { name: "Hello".into() },
182///     &theme,
183/// );
184/// assert!(result.is_ok());
185///
186/// // Unknown tag fails validation
187/// let result = validate_template(
188///     "[unknown]{{ name }}[/unknown]",
189///     &Data { name: "Hello".into() },
190///     &theme,
191/// );
192/// assert!(result.is_err());
193/// ```
194pub fn validate_template<T: Serialize>(
195    template: &str,
196    data: &T,
197    theme: &Theme,
198) -> Result<(), Box<dyn std::error::Error>> {
199    let color_mode = detect_color_mode();
200    let styles = theme.resolve_styles(Some(color_mode));
201
202    // First render with the engine to get the final output
203    let engine = MiniJinjaEngine::new();
204    let data_value = serde_json::to_value(data)?;
205    let minijinja_output = engine.render_template(template, &data_value)?;
206
207    // Now validate the style tags
208    let resolved_styles = styles.to_resolved_map();
209    let parser = BBParser::new(resolved_styles, TagTransform::Remove);
210    parser.validate(&minijinja_output)?;
211
212    Ok(())
213}
214
215/// Renders a template with automatic terminal color detection.
216///
217/// This is the simplest way to render styled output. It automatically detects
218/// whether stdout supports colors and applies styles accordingly. Color mode
219/// (light/dark) is detected from OS settings.
220///
221/// # Arguments
222///
223/// * `template` - A minijinja template string
224/// * `data` - Any serializable data to pass to the template
225/// * `theme` - Theme definitions to use for the `style` filter
226///
227/// # Example
228///
229/// ```rust
230/// use standout_render::{render, Theme};
231/// use console::Style;
232/// use serde::Serialize;
233///
234/// #[derive(Serialize)]
235/// struct Data { message: String }
236///
237/// let theme = Theme::new().add("ok", Style::new().green());
238/// let output = render(
239///     r#"[ok]{{ message }}[/ok]"#,
240///     &Data { message: "Success!".into() },
241///     &theme,
242/// ).unwrap();
243/// ```
244pub fn render<T: Serialize>(
245    template: &str,
246    data: &T,
247    theme: &Theme,
248) -> Result<String, RenderError> {
249    render_with_output(template, data, theme, OutputMode::Auto)
250}
251
252/// Renders a template with explicit output mode control.
253///
254/// Use this when you need to override automatic terminal detection,
255/// for example when honoring a `--output=text` CLI flag. Color mode
256/// (light/dark) is detected from OS settings.
257///
258/// # Arguments
259///
260/// * `template` - A minijinja template string
261/// * `data` - Any serializable data to pass to the template
262/// * `theme` - Theme definitions to use for styling
263/// * `mode` - Output mode: `Auto`, `Term`, or `Text`
264///
265/// # Example
266///
267/// ```rust
268/// use standout_render::{render_with_output, Theme, OutputMode};
269/// use console::Style;
270/// use serde::Serialize;
271///
272/// #[derive(Serialize)]
273/// struct Data { status: String }
274///
275/// let theme = Theme::new().add("ok", Style::new().green());
276///
277/// // Force plain text output
278/// let plain = render_with_output(
279///     r#"[ok]{{ status }}[/ok]"#,
280///     &Data { status: "done".into() },
281///     &theme,
282///     OutputMode::Text,
283/// ).unwrap();
284/// assert_eq!(plain, "done"); // No ANSI codes
285///
286/// // Force terminal output (with ANSI codes)
287/// let term = render_with_output(
288///     r#"[ok]{{ status }}[/ok]"#,
289///     &Data { status: "done".into() },
290///     &theme,
291///     OutputMode::Term,
292/// ).unwrap();
293/// // Contains ANSI codes for green
294/// ```
295pub fn render_with_output<T: Serialize>(
296    template: &str,
297    data: &T,
298    theme: &Theme,
299    mode: OutputMode,
300) -> Result<String, RenderError> {
301    // Detect color mode and render with explicit mode
302    let color_mode = detect_color_mode();
303    render_with_mode(template, data, theme, mode, color_mode)
304}
305
306/// Renders a template with explicit output mode and color mode control.
307///
308/// Use this when you need to force a specific color mode (light/dark),
309/// for example in tests or when honoring user preferences.
310///
311/// # Arguments
312///
313/// * `template` - A minijinja template string
314/// * `data` - Any serializable data to pass to the template
315/// * `theme` - Theme definitions to use for the `style` filter
316/// * `output_mode` - Output mode: `Auto`, `Term`, `Text`, etc.
317/// * `color_mode` - Color mode: `Light` or `Dark`
318///
319/// # Example
320///
321/// ```rust
322/// use standout_render::{render_with_mode, Theme, OutputMode, ColorMode};
323/// use console::Style;
324/// use serde::Serialize;
325///
326/// #[derive(Serialize)]
327/// struct Data { status: String }
328///
329/// let theme = Theme::new()
330///     .add_adaptive(
331///         "panel",
332///         Style::new(),
333///         Some(Style::new().black()),  // Light mode
334///         Some(Style::new().white()),  // Dark mode
335///     );
336///
337/// // Force dark mode rendering
338/// let dark = render_with_mode(
339///     r#"[panel]{{ status }}[/panel]"#,
340///     &Data { status: "test".into() },
341///     &theme,
342///     OutputMode::Term,
343///     ColorMode::Dark,
344/// ).unwrap();
345///
346/// // Force light mode rendering
347/// let light = render_with_mode(
348///     r#"[panel]{{ status }}[/panel]"#,
349///     &Data { status: "test".into() },
350///     &theme,
351///     OutputMode::Term,
352///     ColorMode::Light,
353/// ).unwrap();
354/// ```
355pub fn render_with_mode<T: Serialize>(
356    template: &str,
357    data: &T,
358    theme: &Theme,
359    output_mode: OutputMode,
360    color_mode: ColorMode,
361) -> Result<String, RenderError> {
362    // Validate style aliases before rendering
363    theme
364        .validate()
365        .map_err(|e| RenderError::StyleError(e.to_string()))?;
366
367    // Resolve styles for the specified color mode
368    let styles = theme.resolve_styles(Some(color_mode));
369
370    // Pass 1: Template rendering (with icons if defined)
371    let engine = MiniJinjaEngine::new();
372    let data_value = serde_json::to_value(data)?;
373    let icon_context = build_icon_context(theme);
374    let template_output = if icon_context.is_empty() {
375        engine.render_template(template, &data_value)?
376    } else {
377        engine.render_with_context(template, &data_value, icon_context)?
378    };
379
380    // Pass 2: BBParser style tag processing
381    let final_output = apply_style_tags(&template_output, &styles, output_mode);
382
383    Ok(final_output)
384}
385
386/// Renders a template with additional variables injected into the context.
387///
388/// This is a convenience function for adding simple key-value pairs to the template
389/// context without the complexity of the full [`ContextRegistry`] system. The data
390/// fields take precedence over the injected variables.
391///
392/// # Arguments
393///
394/// * `template` - A minijinja template string
395/// * `data` - The primary serializable data to render
396/// * `theme` - Theme definitions for style tag processing
397/// * `vars` - Additional variables to inject into the template context
398///
399/// # Example
400///
401/// ```rust
402/// use standout_render::{render_with_vars, Theme, OutputMode};
403/// use serde::Serialize;
404/// use std::collections::HashMap;
405///
406/// #[derive(Serialize)]
407/// struct User { name: String }
408///
409/// let theme = Theme::new();
410/// let user = User { name: "Alice".into() };
411///
412/// let mut vars = HashMap::new();
413/// vars.insert("version", "1.0.0");
414/// vars.insert("app_name", "MyApp");
415///
416/// let output = render_with_vars(
417///     "{{ name }} - {{ app_name }} v{{ version }}",
418///     &user,
419///     &theme,
420///     OutputMode::Text,
421///     vars,
422/// ).unwrap();
423///
424/// assert_eq!(output, "Alice - MyApp v1.0.0");
425/// ```
426pub fn render_with_vars<T, K, V, I>(
427    template: &str,
428    data: &T,
429    theme: &Theme,
430    mode: OutputMode,
431    vars: I,
432) -> Result<String, RenderError>
433where
434    T: Serialize,
435    K: AsRef<str>,
436    V: Into<serde_json::Value>,
437    I: IntoIterator<Item = (K, V)>,
438{
439    let color_mode = detect_color_mode();
440    let styles = theme.resolve_styles(Some(color_mode));
441
442    // Validate style aliases before rendering
443    styles
444        .validate()
445        .map_err(|e| RenderError::StyleError(e.to_string()))?;
446
447    // Build context from icons + vars (vars take precedence over icons)
448    let mut context: HashMap<String, serde_json::Value> = build_icon_context(theme);
449    for (key, value) in vars {
450        context.insert(key.as_ref().to_string(), value.into());
451    }
452
453    // Pass 1: Template rendering with context
454    let engine = MiniJinjaEngine::new();
455    let data_value = serde_json::to_value(data)?;
456    let template_output = engine.render_with_context(template, &data_value, context)?;
457
458    // Pass 2: BBParser style tag processing
459    let final_output = apply_style_tags(&template_output, &styles, mode);
460
461    Ok(final_output)
462}
463
464/// Auto-dispatches between template rendering and direct serialization.
465///
466/// This is the recommended function when you want to support both human-readable
467/// output (terminal, text) and machine-readable output (JSON, YAML, etc.). For
468/// structured modes like `Json`, the data is serialized directly, skipping
469/// template rendering entirely.
470///
471/// # Arguments
472///
473/// * `template` - A minijinja template string (ignored for structured modes)
474/// * `data` - Any serializable data to render or serialize
475/// * `theme` - Theme definitions for the `style` filter (ignored for structured modes)
476/// * `mode` - Output mode determining the output format
477///
478/// # Example
479///
480/// ```rust
481/// use standout_render::{render_auto, Theme, OutputMode};
482/// use console::Style;
483/// use serde::Serialize;
484///
485/// #[derive(Serialize)]
486/// struct Report { title: String, count: usize }
487///
488/// let theme = Theme::new().add("title", Style::new().bold());
489/// let data = Report { title: "Summary".into(), count: 42 };
490///
491/// // Terminal output uses the template
492/// let term = render_auto(
493///     r#"[title]{{ title }}[/title]: {{ count }}"#,
494///     &data,
495///     &theme,
496///     OutputMode::Text,
497/// ).unwrap();
498/// assert_eq!(term, "Summary: 42");
499///
500/// // JSON output serializes directly
501/// let json = render_auto(
502///     r#"[title]{{ title }}[/title]: {{ count }}"#,
503///     &data,
504///     &theme,
505///     OutputMode::Json,
506/// ).unwrap();
507/// assert!(json.contains("\"title\": \"Summary\""));
508/// assert!(json.contains("\"count\": 42"));
509/// ```
510pub fn render_auto<T: Serialize>(
511    template: &str,
512    data: &T,
513    theme: &Theme,
514    mode: OutputMode,
515) -> Result<String, RenderError> {
516    if mode.is_structured() {
517        match mode {
518            OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
519            OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
520            OutputMode::Xml => Ok(crate::util::serialize_to_xml(data)?),
521            OutputMode::Csv => {
522                let value = serde_json::to_value(data)?;
523                let (headers, rows) = crate::util::flatten_json_for_csv(&value);
524
525                let mut wtr = csv::Writer::from_writer(Vec::new());
526                wtr.write_record(&headers)?;
527                for row in rows {
528                    wtr.write_record(&row)?;
529                }
530                let bytes = wtr.into_inner()?;
531                Ok(String::from_utf8(bytes)?)
532            }
533            _ => unreachable!("is_structured() returned true for non-structured mode"),
534        }
535    } else {
536        render_with_output(template, data, theme, mode)
537    }
538}
539
540/// Auto-dispatches with granular control over structured output.
541///
542/// Similar to `render_auto`, but allows passing an optional `FlatDataSpec`.
543/// This is particularly useful for controlling CSV output structure (columns, headers)
544/// instead of relying on automatic JSON flattening.
545///
546/// # Arguments
547///
548/// * `template` - A minijinja template string
549/// * `data` - Any serializable data to render or serialize
550/// * `theme` - Theme definitions for the `style` filter
551/// * `mode` - Output mode determining the output format
552/// * `spec` - Optional `FlatDataSpec` for defining CSV/Table structure
553pub fn render_auto_with_spec<T: Serialize>(
554    template: &str,
555    data: &T,
556    theme: &Theme,
557    mode: OutputMode,
558    spec: Option<&FlatDataSpec>,
559) -> Result<String, RenderError> {
560    if mode.is_structured() {
561        match mode {
562            OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
563            OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
564            OutputMode::Xml => Ok(crate::util::serialize_to_xml(data)?),
565            OutputMode::Csv => {
566                let value = serde_json::to_value(data)?;
567
568                let (headers, rows) = if let Some(s) = spec {
569                    // Use the spec for explicit extraction
570                    let headers = s.extract_header();
571                    let rows: Vec<Vec<String>> = match value {
572                        serde_json::Value::Array(items) => {
573                            items.iter().map(|item| s.extract_row(item)).collect()
574                        }
575                        _ => vec![s.extract_row(&value)],
576                    };
577                    (headers, rows)
578                } else {
579                    // Use automatic flattening
580                    crate::util::flatten_json_for_csv(&value)
581                };
582
583                let mut wtr = csv::Writer::from_writer(Vec::new());
584                wtr.write_record(&headers)?;
585                for row in rows {
586                    wtr.write_record(&row)?;
587                }
588                let bytes = wtr.into_inner()?;
589                Ok(String::from_utf8(bytes)?)
590            }
591            _ => unreachable!("is_structured() returned true for non-structured mode"),
592        }
593    } else {
594        render_with_output(template, data, theme, mode)
595    }
596}
597
598/// Renders a template with additional context objects injected.
599///
600/// This is the most flexible rendering function, allowing you to inject
601/// additional objects into the template context beyond the serialized data.
602/// Use this when templates need access to utilities, formatters, or runtime
603/// values that cannot be represented as JSON.
604///
605/// # Arguments
606///
607/// * `template` - A minijinja template string
608/// * `data` - Any serializable data to pass to the template
609/// * `theme` - Theme definitions for the `style` filter
610/// * `mode` - Output mode: `Auto`, `Term`, `Text`, etc.
611/// * `context_registry` - Additional context objects to inject
612/// * `render_context` - Information about the render environment
613///
614/// # Context Resolution
615///
616/// Context objects are resolved from the registry using the provided
617/// `RenderContext`. Each registered provider is called to produce a value,
618/// which is then merged into the template context.
619///
620/// If a context key conflicts with a data field, the data field wins.
621/// Context is supplementary to the handler's data, not a replacement.
622///
623/// # Example
624///
625/// ```rust
626/// use standout_render::{render_with_context, Theme, OutputMode};
627/// use standout_render::context::{RenderContext, ContextRegistry};
628/// use minijinja::Value;
629/// use serde::Serialize;
630///
631/// #[derive(Serialize)]
632/// struct Data { name: String }
633///
634/// let theme = Theme::new();
635/// let data = Data { name: "Alice".into() };
636///
637/// // Create context with a static value
638/// let mut registry = ContextRegistry::new();
639/// registry.add_static("version", Value::from("1.0.0"));
640///
641/// // Create render context
642/// let json_data = serde_json::to_value(&data).unwrap();
643/// let render_ctx = RenderContext::new(
644///     OutputMode::Text,
645///     Some(80),
646///     &theme,
647///     &json_data,
648/// );
649///
650/// let output = render_with_context(
651///     "{{ name }} (v{{ version }})",
652///     &data,
653///     &theme,
654///     OutputMode::Text,
655///     &registry,
656///     &render_ctx,
657///     None,
658/// ).unwrap();
659///
660/// assert_eq!(output, "Alice (v1.0.0)");
661/// ```
662pub fn render_with_context<T: Serialize>(
663    template: &str,
664    data: &T,
665    theme: &Theme,
666    mode: OutputMode,
667    context_registry: &ContextRegistry,
668    render_context: &RenderContext,
669    template_registry: Option<&super::TemplateRegistry>,
670) -> Result<String, RenderError> {
671    let color_mode = detect_color_mode();
672    let styles = theme.resolve_styles(Some(color_mode));
673
674    // Validate style aliases before rendering
675    styles
676        .validate()
677        .map_err(|e| RenderError::StyleError(e.to_string()))?;
678
679    let mut engine = MiniJinjaEngine::new();
680
681    // Check if template is a registry key (name) or inline content.
682    // If the registry contains a template with this name, use its content.
683    // Otherwise, treat the template string as inline content.
684    let template_content = if let Some(registry) = template_registry {
685        if let Ok(content) = registry.get_content(template) {
686            content
687        } else {
688            template.to_string()
689        }
690    } else {
691        template.to_string()
692    };
693
694    // Load all templates from registry if available (enables {% include %})
695    if let Some(registry) = template_registry {
696        for name in registry.names() {
697            if let Ok(content) = registry.get_content(name) {
698                engine.add_template(name, &content)?;
699            }
700        }
701    }
702
703    // Build the combined context: icons + injected context + data
704    let icon_context = build_icon_context(theme);
705    let context = build_combined_context(data, context_registry, render_context, icon_context)?;
706
707    // Pass 1: Template rendering with context
708    let data_value = serde_json::to_value(data)?;
709    let template_output = engine.render_with_context(&template_content, &data_value, context)?;
710
711    // Pass 2: BBParser style tag processing
712    let final_output = apply_style_tags(&template_output, &styles, mode);
713
714    Ok(final_output)
715}
716
717/// Auto-dispatches with context injection support.
718///
719/// This combines `render_with_context` with JSON serialization support.
720/// For structured modes like `Json`, the data is serialized directly,
721/// skipping template rendering (and context injection).
722///
723/// # Arguments
724///
725/// * `template` - A minijinja template string (ignored for structured modes)
726/// * `data` - Any serializable data to render or serialize
727/// * `theme` - Theme definitions for the `style` filter
728/// * `mode` - Output mode determining the output format
729/// * `context_registry` - Additional context objects to inject
730/// * `render_context` - Information about the render environment
731///
732/// # Example
733///
734/// ```rust
735/// use standout_render::{render_auto_with_context, Theme, OutputMode};
736/// use standout_render::context::{RenderContext, ContextRegistry};
737/// use minijinja::Value;
738/// use serde::Serialize;
739///
740/// #[derive(Serialize)]
741/// struct Report { title: String, count: usize }
742///
743/// let theme = Theme::new();
744/// let data = Report { title: "Summary".into(), count: 42 };
745///
746/// let mut registry = ContextRegistry::new();
747/// registry.add_provider("terminal_width", |ctx: &RenderContext| {
748///     Value::from(ctx.terminal_width.unwrap_or(80))
749/// });
750///
751/// let json_data = serde_json::to_value(&data).unwrap();
752/// let render_ctx = RenderContext::new(
753///     OutputMode::Text,
754///     Some(120),
755///     &theme,
756///     &json_data,
757/// );
758///
759/// // Text mode uses the template with context
760/// let text = render_auto_with_context(
761///     "{{ title }} (width={{ terminal_width }}): {{ count }}",
762///     &data,
763///     &theme,
764///     OutputMode::Text,
765///     &registry,
766///     &render_ctx,
767///     None,
768/// ).unwrap();
769/// assert_eq!(text, "Summary (width=120): 42");
770///
771/// // JSON mode ignores template and context, serializes data directly
772/// let json = render_auto_with_context(
773///     "unused",
774///     &data,
775///     &theme,
776///     OutputMode::Json,
777///     &registry,
778///     &render_ctx,
779///     None,
780/// ).unwrap();
781/// assert!(json.contains("\"title\": \"Summary\""));
782/// ```
783pub fn render_auto_with_context<T: Serialize>(
784    template: &str,
785    data: &T,
786    theme: &Theme,
787    mode: OutputMode,
788    context_registry: &ContextRegistry,
789    render_context: &RenderContext,
790    template_registry: Option<&super::TemplateRegistry>,
791) -> Result<String, RenderError> {
792    if mode.is_structured() {
793        match mode {
794            OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
795            OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
796            OutputMode::Xml => Ok(crate::util::serialize_to_xml(data)?),
797            OutputMode::Csv => {
798                let value = serde_json::to_value(data)?;
799                let (headers, rows) = crate::util::flatten_json_for_csv(&value);
800
801                let mut wtr = csv::Writer::from_writer(Vec::new());
802                wtr.write_record(&headers)?;
803                for row in rows {
804                    wtr.write_record(&row)?;
805                }
806                let bytes = wtr.into_inner()?;
807                Ok(String::from_utf8(bytes)?)
808            }
809            _ => unreachable!("is_structured() returned true for non-structured mode"),
810        }
811    } else {
812        render_with_context(
813            template,
814            data,
815            theme,
816            mode,
817            context_registry,
818            render_context,
819            template_registry,
820        )
821    }
822}
823
824/// Builds an icon context from a theme's icon definitions.
825///
826/// Returns a map with a single `"icons"` key mapping to the resolved icon strings,
827/// or an empty map if the theme has no icons defined.
828fn build_icon_context(theme: &Theme) -> HashMap<String, serde_json::Value> {
829    if theme.icons().is_empty() {
830        return HashMap::new();
831    }
832    let icon_mode = detect_icon_mode();
833    let resolved = theme.resolve_icons(icon_mode);
834    let mut ctx = HashMap::new();
835    ctx.insert("icons".to_string(), serde_json::to_value(resolved).unwrap());
836    ctx
837}
838
839/// Builds a combined context from data and injected context.
840///
841/// Data fields take precedence over context fields.
842fn build_combined_context<T: Serialize>(
843    data: &T,
844    context_registry: &ContextRegistry,
845    render_context: &RenderContext,
846    icon_context: HashMap<String, serde_json::Value>,
847) -> Result<HashMap<String, serde_json::Value>, RenderError> {
848    // First, resolve all context providers
849    let context_values = context_registry.resolve(render_context);
850
851    // Convert data to a map of values
852    let data_value = serde_json::to_value(data)?;
853
854    // Start with icon context (lowest priority)
855    let mut combined: HashMap<String, serde_json::Value> = icon_context;
856
857    // Add context values (medium priority)
858    for (key, value) in context_values {
859        // Convert minijinja::Value to serde_json::Value
860        // This is a bit inefficient but necessary for the abstraction
861        // In the future, ContextRegistry should probably return serde_json::Value
862        let json_val =
863            serde_json::to_value(value).map_err(|e| RenderError::ContextError(e.to_string()))?;
864        combined.insert(key, json_val);
865    }
866
867    // Add data values (highest priority - overwrites context)
868    if let Some(obj) = data_value.as_object() {
869        for (key, value) in obj {
870            combined.insert(key.clone(), value.clone());
871        }
872    }
873
874    Ok(combined)
875}
876
877/// Auto-dispatches rendering using a provided TemplateEngine.
878///
879/// This is similar to `render_auto_with_context` but allows using a pre-configured
880/// `TemplateEngine` trait object instead of creating a new dictionary-based engine.
881pub fn render_auto_with_engine(
882    engine: &dyn super::TemplateEngine,
883    template: &str,
884    data: &serde_json::Value,
885    theme: &Theme,
886    mode: OutputMode,
887    context_registry: &ContextRegistry,
888    render_context: &RenderContext,
889) -> Result<String, RenderError> {
890    if mode.is_structured() {
891        match mode {
892            OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
893            OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
894            OutputMode::Xml => Ok(crate::util::serialize_to_xml(data)?),
895            OutputMode::Csv => {
896                let (headers, rows) = crate::util::flatten_json_for_csv(data);
897
898                let mut wtr = csv::Writer::from_writer(Vec::new());
899                wtr.write_record(&headers)?;
900                for row in rows {
901                    wtr.write_record(&row)?;
902                }
903                let bytes = wtr.into_inner()?;
904                Ok(String::from_utf8(bytes)?)
905            }
906            _ => unreachable!("is_structured() returned true for non-structured mode"),
907        }
908    } else {
909        let color_mode = detect_color_mode();
910        let styles = theme.resolve_styles(Some(color_mode));
911
912        // Validate style aliases before rendering
913        styles
914            .validate()
915            .map_err(|e| RenderError::StyleError(e.to_string()))?;
916
917        // Build the combined context: icons + injected context + data
918        let icon_context = build_icon_context(theme);
919        let context_map =
920            build_combined_context(data, context_registry, render_context, icon_context)?;
921
922        // Merge into a single Value for the engine
923        let combined_value = serde_json::Value::Object(context_map.into_iter().collect());
924
925        // Render template
926        let template_output = if engine.has_template(template) {
927            engine.render_named(template, &combined_value)?
928        } else {
929            engine.render_template(template, &combined_value)?
930        };
931
932        // Apply styles
933        let final_output = apply_style_tags(&template_output, &styles, mode);
934
935        Ok(final_output)
936    }
937}
938
939/// Auto-dispatches rendering and returns both formatted and raw output.
940///
941/// This is similar to `render_auto_with_engine` but returns a `RenderResult`
942/// containing both the formatted output (with ANSI codes) and the raw output
943/// (with style tags but no ANSI codes). The raw output is useful for piping
944/// to external commands.
945///
946/// For structured modes (JSON, YAML, etc.), both formatted and raw are the same
947/// since no style processing occurs.
948pub fn render_auto_with_engine_split(
949    engine: &dyn super::TemplateEngine,
950    template: &str,
951    data: &serde_json::Value,
952    theme: &Theme,
953    mode: OutputMode,
954    context_registry: &ContextRegistry,
955    render_context: &RenderContext,
956) -> Result<RenderResult, RenderError> {
957    if mode.is_structured() {
958        // For structured modes, no style processing, so raw == formatted
959        let output = match mode {
960            OutputMode::Json => serde_json::to_string_pretty(data)?,
961            OutputMode::Yaml => serde_yaml::to_string(data)?,
962            OutputMode::Xml => crate::util::serialize_to_xml(data)?,
963            OutputMode::Csv => {
964                let (headers, rows) = crate::util::flatten_json_for_csv(data);
965
966                let mut wtr = csv::Writer::from_writer(Vec::new());
967                wtr.write_record(&headers)?;
968                for row in rows {
969                    wtr.write_record(&row)?;
970                }
971                let bytes = wtr.into_inner()?;
972                String::from_utf8(bytes)?
973            }
974            _ => unreachable!("is_structured() returned true for non-structured mode"),
975        };
976        Ok(RenderResult::plain(output))
977    } else {
978        let color_mode = detect_color_mode();
979        let styles = theme.resolve_styles(Some(color_mode));
980
981        // Validate style aliases before rendering
982        styles
983            .validate()
984            .map_err(|e| RenderError::StyleError(e.to_string()))?;
985
986        // Build the combined context: icons + injected context + data
987        let icon_context = build_icon_context(theme);
988        let context_map =
989            build_combined_context(data, context_registry, render_context, icon_context)?;
990
991        // Merge into a single Value for the engine
992        let combined_value = serde_json::Value::Object(context_map.into_iter().collect());
993
994        // Pass 1: Render template (this is the raw/intermediate output)
995        let raw_output = if engine.has_template(template) {
996            engine.render_named(template, &combined_value)?
997        } else {
998            engine.render_template(template, &combined_value)?
999        };
1000
1001        // Pass 2: Apply styles to get formatted output
1002        let formatted_output = apply_style_tags(&raw_output, &styles, mode);
1003
1004        // For raw output, strip style tags (OutputMode::Text behavior)
1005        let stripped_output = apply_style_tags(&raw_output, &styles, OutputMode::Text);
1006
1007        Ok(RenderResult::new(formatted_output, stripped_output))
1008    }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013    use super::*;
1014    use crate::tabular::{Column, FlatDataSpec, Width};
1015    use crate::Theme;
1016    use console::Style;
1017    use minijinja::Value;
1018    use serde::Serialize;
1019    use serde_json::json;
1020
1021    #[derive(Serialize)]
1022    struct SimpleData {
1023        message: String,
1024    }
1025
1026    #[derive(Serialize)]
1027    struct ListData {
1028        items: Vec<String>,
1029        count: usize,
1030    }
1031
1032    #[test]
1033    fn test_render_with_output_text_no_ansi() {
1034        let theme = Theme::new().add("red", Style::new().red());
1035        let data = SimpleData {
1036            message: "test".into(),
1037        };
1038
1039        let output = render_with_output(
1040            r#"[red]{{ message }}[/red]"#,
1041            &data,
1042            &theme,
1043            OutputMode::Text,
1044        )
1045        .unwrap();
1046
1047        assert_eq!(output, "test");
1048        assert!(!output.contains("\x1b["));
1049    }
1050
1051    #[test]
1052    fn test_render_with_output_term_has_ansi() {
1053        let theme = Theme::new().add("green", Style::new().green().force_styling(true));
1054        let data = SimpleData {
1055            message: "success".into(),
1056        };
1057
1058        let output = render_with_output(
1059            r#"[green]{{ message }}[/green]"#,
1060            &data,
1061            &theme,
1062            OutputMode::Term,
1063        )
1064        .unwrap();
1065
1066        assert!(output.contains("success"));
1067        assert!(output.contains("\x1b["));
1068    }
1069
1070    #[test]
1071    fn test_render_unknown_style_shows_indicator() {
1072        let theme = Theme::new();
1073        let data = SimpleData {
1074            message: "hello".into(),
1075        };
1076
1077        let output = render_with_output(
1078            r#"[unknown]{{ message }}[/unknown]"#,
1079            &data,
1080            &theme,
1081            OutputMode::Term,
1082        )
1083        .unwrap();
1084
1085        // Unknown tags in passthrough mode get ? marker on both open and close tags
1086        assert_eq!(output, "[unknown?]hello[/unknown?]");
1087    }
1088
1089    #[test]
1090    fn test_render_unknown_style_stripped_in_text_mode() {
1091        let theme = Theme::new();
1092        let data = SimpleData {
1093            message: "hello".into(),
1094        };
1095
1096        let output = render_with_output(
1097            r#"[unknown]{{ message }}[/unknown]"#,
1098            &data,
1099            &theme,
1100            OutputMode::Text,
1101        )
1102        .unwrap();
1103
1104        // In text mode (Remove), unknown tags are stripped like known tags
1105        assert_eq!(output, "hello");
1106    }
1107
1108    #[test]
1109    fn test_render_template_with_loop() {
1110        let theme = Theme::new().add("item", Style::new().cyan());
1111        let data = ListData {
1112            items: vec!["one".into(), "two".into()],
1113            count: 2,
1114        };
1115
1116        let template = r#"{% for item in items %}[item]{{ item }}[/item]
1117{% endfor %}"#;
1118
1119        let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1120        assert_eq!(output, "one\ntwo\n");
1121    }
1122
1123    #[test]
1124    fn test_render_mixed_styled_and_plain() {
1125        let theme = Theme::new().add("count", Style::new().bold());
1126        let data = ListData {
1127            items: vec![],
1128            count: 42,
1129        };
1130
1131        let template = r#"Total: [count]{{ count }}[/count] items"#;
1132        let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1133
1134        assert_eq!(output, "Total: 42 items");
1135    }
1136
1137    #[test]
1138    fn test_render_literal_string_styled() {
1139        let theme = Theme::new().add("header", Style::new().bold());
1140
1141        #[derive(Serialize)]
1142        struct Empty {}
1143
1144        let output = render_with_output(
1145            r#"[header]Header[/header]"#,
1146            &Empty {},
1147            &theme,
1148            OutputMode::Text,
1149        )
1150        .unwrap();
1151
1152        assert_eq!(output, "Header");
1153    }
1154
1155    #[test]
1156    fn test_empty_template() {
1157        let theme = Theme::new();
1158
1159        #[derive(Serialize)]
1160        struct Empty {}
1161
1162        let output = render_with_output("", &Empty {}, &theme, OutputMode::Text).unwrap();
1163        assert_eq!(output, "");
1164    }
1165
1166    #[test]
1167    fn test_template_syntax_error() {
1168        let theme = Theme::new();
1169
1170        #[derive(Serialize)]
1171        struct Empty {}
1172
1173        let result = render_with_output("{{ unclosed", &Empty {}, &theme, OutputMode::Text);
1174        assert!(result.is_err());
1175    }
1176
1177    #[test]
1178    fn test_style_tag_with_nested_data() {
1179        #[derive(Serialize)]
1180        struct Item {
1181            name: String,
1182            value: i32,
1183        }
1184
1185        #[derive(Serialize)]
1186        struct Container {
1187            items: Vec<Item>,
1188        }
1189
1190        let theme = Theme::new().add("name", Style::new().bold());
1191        let data = Container {
1192            items: vec![
1193                Item {
1194                    name: "foo".into(),
1195                    value: 1,
1196                },
1197                Item {
1198                    name: "bar".into(),
1199                    value: 2,
1200                },
1201            ],
1202        };
1203
1204        let template = r#"{% for item in items %}[name]{{ item.name }}[/name]={{ item.value }}
1205{% endfor %}"#;
1206
1207        let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1208        assert_eq!(output, "foo=1\nbar=2\n");
1209    }
1210
1211    #[test]
1212    fn test_render_with_output_term_debug() {
1213        let theme = Theme::new()
1214            .add("title", Style::new().bold())
1215            .add("count", Style::new().cyan());
1216
1217        #[derive(Serialize)]
1218        struct Data {
1219            name: String,
1220            value: usize,
1221        }
1222
1223        let data = Data {
1224            name: "Test".into(),
1225            value: 42,
1226        };
1227
1228        let output = render_with_output(
1229            r#"[title]{{ name }}[/title]: [count]{{ value }}[/count]"#,
1230            &data,
1231            &theme,
1232            OutputMode::TermDebug,
1233        )
1234        .unwrap();
1235
1236        assert_eq!(output, "[title]Test[/title]: [count]42[/count]");
1237    }
1238
1239    #[test]
1240    fn test_render_with_output_term_debug_preserves_tags() {
1241        let theme = Theme::new().add("known", Style::new().bold());
1242
1243        #[derive(Serialize)]
1244        struct Data {
1245            message: String,
1246        }
1247
1248        let data = Data {
1249            message: "hello".into(),
1250        };
1251
1252        // In TermDebug (Keep mode), unknown tags are preserved as-is
1253        let output = render_with_output(
1254            r#"[unknown]{{ message }}[/unknown]"#,
1255            &data,
1256            &theme,
1257            OutputMode::TermDebug,
1258        )
1259        .unwrap();
1260
1261        assert_eq!(output, "[unknown]hello[/unknown]");
1262
1263        // Known tags are also preserved as-is in debug mode
1264        let output = render_with_output(
1265            r#"[known]{{ message }}[/known]"#,
1266            &data,
1267            &theme,
1268            OutputMode::TermDebug,
1269        )
1270        .unwrap();
1271
1272        assert_eq!(output, "[known]hello[/known]");
1273    }
1274
1275    #[test]
1276    fn test_render_auto_json_mode() {
1277        use serde_json::json;
1278
1279        let theme = Theme::new();
1280        let data = json!({"name": "test", "count": 42});
1281
1282        let output = render_auto("unused template", &data, &theme, OutputMode::Json).unwrap();
1283
1284        assert!(output.contains("\"name\": \"test\""));
1285        assert!(output.contains("\"count\": 42"));
1286    }
1287
1288    #[test]
1289    fn test_render_auto_text_mode_uses_template() {
1290        use serde_json::json;
1291
1292        let theme = Theme::new();
1293        let data = json!({"name": "test"});
1294
1295        let output = render_auto("Name: {{ name }}", &data, &theme, OutputMode::Text).unwrap();
1296
1297        assert_eq!(output, "Name: test");
1298    }
1299
1300    #[test]
1301    fn test_render_auto_term_mode_uses_template() {
1302        use serde_json::json;
1303
1304        let theme = Theme::new().add("bold", Style::new().bold().force_styling(true));
1305        let data = json!({"name": "test"});
1306
1307        let output = render_auto(
1308            r#"[bold]{{ name }}[/bold]"#,
1309            &data,
1310            &theme,
1311            OutputMode::Term,
1312        )
1313        .unwrap();
1314
1315        assert!(output.contains("\x1b[1m"));
1316        assert!(output.contains("test"));
1317    }
1318
1319    #[test]
1320    fn test_render_auto_json_with_struct() {
1321        #[derive(Serialize)]
1322        struct Report {
1323            title: String,
1324            items: Vec<String>,
1325        }
1326
1327        let theme = Theme::new();
1328        let data = Report {
1329            title: "Summary".into(),
1330            items: vec!["one".into(), "two".into()],
1331        };
1332
1333        let output = render_auto("unused", &data, &theme, OutputMode::Json).unwrap();
1334
1335        assert!(output.contains("\"title\": \"Summary\""));
1336        assert!(output.contains("\"items\""));
1337        assert!(output.contains("\"one\""));
1338    }
1339
1340    #[test]
1341    fn test_render_with_alias() {
1342        let theme = Theme::new()
1343            .add("base", Style::new().bold())
1344            .add("alias", "base");
1345
1346        let output = render_with_output(
1347            r#"[alias]text[/alias]"#,
1348            &serde_json::json!({}),
1349            &theme,
1350            OutputMode::Text,
1351        )
1352        .unwrap();
1353
1354        assert_eq!(output, "text");
1355    }
1356
1357    #[test]
1358    fn test_render_with_alias_chain() {
1359        let theme = Theme::new()
1360            .add("muted", Style::new().dim())
1361            .add("disabled", "muted")
1362            .add("timestamp", "disabled");
1363
1364        let output = render_with_output(
1365            r#"[timestamp]12:00[/timestamp]"#,
1366            &serde_json::json!({}),
1367            &theme,
1368            OutputMode::Text,
1369        )
1370        .unwrap();
1371
1372        assert_eq!(output, "12:00");
1373    }
1374
1375    #[test]
1376    fn test_render_fails_with_dangling_alias() {
1377        let theme = Theme::new().add("orphan", "missing");
1378
1379        let result = render_with_output(
1380            r#"[orphan]text[/orphan]"#,
1381            &serde_json::json!({}),
1382            &theme,
1383            OutputMode::Text,
1384        );
1385
1386        assert!(result.is_err());
1387        let err = result.unwrap_err();
1388        assert!(err.to_string().contains("orphan"));
1389        assert!(err.to_string().contains("missing"));
1390    }
1391
1392    #[test]
1393    fn test_render_fails_with_cycle() {
1394        let theme = Theme::new().add("a", "b").add("b", "a");
1395
1396        let result = render_with_output(
1397            r#"[a]text[/a]"#,
1398            &serde_json::json!({}),
1399            &theme,
1400            OutputMode::Text,
1401        );
1402
1403        assert!(result.is_err());
1404        assert!(result.unwrap_err().to_string().contains("cycle"));
1405    }
1406
1407    #[test]
1408    fn test_three_layer_styling_pattern() {
1409        let theme = Theme::new()
1410            .add("dim_style", Style::new().dim())
1411            .add("cyan_bold", Style::new().cyan().bold())
1412            .add("yellow_bg", Style::new().on_yellow())
1413            .add("muted", "dim_style")
1414            .add("accent", "cyan_bold")
1415            .add("highlighted", "yellow_bg")
1416            .add("timestamp", "muted")
1417            .add("title", "accent")
1418            .add("selected_item", "highlighted");
1419
1420        assert!(theme.validate().is_ok());
1421
1422        let output = render_with_output(
1423            r#"[timestamp]{{ time }}[/timestamp] - [title]{{ name }}[/title]"#,
1424            &serde_json::json!({"time": "12:00", "name": "Report"}),
1425            &theme,
1426            OutputMode::Text,
1427        )
1428        .unwrap();
1429
1430        assert_eq!(output, "12:00 - Report");
1431    }
1432
1433    // ============================================================================
1434    // YAML/XML/CSV Output Tests
1435    // ============================================================================
1436
1437    #[test]
1438    fn test_render_auto_yaml_mode() {
1439        use serde_json::json;
1440
1441        let theme = Theme::new();
1442        let data = json!({"name": "test", "count": 42});
1443
1444        let output = render_auto("unused template", &data, &theme, OutputMode::Yaml).unwrap();
1445
1446        assert!(output.contains("name: test"));
1447        assert!(output.contains("count: 42"));
1448    }
1449
1450    #[test]
1451    fn test_render_auto_xml_mode_named_struct() {
1452        let theme = Theme::new();
1453
1454        #[derive(Serialize)]
1455        #[serde(rename = "root")]
1456        struct Data {
1457            name: String,
1458            count: usize,
1459        }
1460
1461        let data = Data {
1462            name: "test".into(),
1463            count: 42,
1464        };
1465
1466        let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1467
1468        assert!(output.contains("<root>"));
1469        assert!(output.contains("<name>test</name>"));
1470    }
1471
1472    #[test]
1473    fn test_render_auto_xml_mode_json_map() {
1474        use serde_json::json;
1475
1476        let theme = Theme::new();
1477        let data = json!({"name": "test", "count": 42});
1478
1479        let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1480
1481        assert!(output.contains("<data>"));
1482        assert!(output.contains("<name>test</name>"));
1483        assert!(output.contains("<count>42</count>"));
1484    }
1485
1486    #[test]
1487    fn test_render_auto_xml_mode_nested_map() {
1488        use serde_json::json;
1489
1490        let theme = Theme::new();
1491        let data = json!({"user": {"name": "Alice", "age": 30}});
1492
1493        let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1494
1495        assert!(output.contains("<data>"));
1496        assert!(output.contains("<user>"));
1497        assert!(output.contains("<name>Alice</name>"));
1498    }
1499
1500    #[test]
1501    fn test_render_auto_xml_mode_with_array() {
1502        use serde_json::json;
1503
1504        let theme = Theme::new();
1505        let data = json!({"items": ["a", "b", "c"]});
1506
1507        let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1508
1509        assert!(output.contains("<data>"));
1510        assert!(output.contains("<items>a</items>"));
1511    }
1512
1513    #[test]
1514    fn test_render_auto_csv_mode_auto_flatten() {
1515        use serde_json::json;
1516
1517        let theme = Theme::new();
1518        let data = json!([
1519            {"name": "Alice", "stats": {"score": 10}},
1520            {"name": "Bob", "stats": {"score": 20}}
1521        ]);
1522
1523        let output = render_auto("unused", &data, &theme, OutputMode::Csv).unwrap();
1524
1525        assert!(output.contains("name,stats.score"));
1526        assert!(output.contains("Alice,10"));
1527        assert!(output.contains("Bob,20"));
1528    }
1529
1530    #[test]
1531    fn test_render_auto_csv_mode_with_spec() {
1532        let theme = Theme::new();
1533        let data = json!([
1534            {"name": "Alice", "meta": {"age": 30, "role": "admin"}},
1535            {"name": "Bob", "meta": {"age": 25, "role": "user"}}
1536        ]);
1537
1538        let spec = FlatDataSpec::builder()
1539            .column(Column::new(Width::Fixed(10)).key("name"))
1540            .column(
1541                Column::new(Width::Fixed(10))
1542                    .key("meta.role")
1543                    .header("Role"),
1544            )
1545            .build();
1546
1547        let output =
1548            render_auto_with_spec("unused", &data, &theme, OutputMode::Csv, Some(&spec)).unwrap();
1549
1550        let lines: Vec<&str> = output.lines().collect();
1551        assert_eq!(lines[0], "name,Role");
1552        assert!(lines.contains(&"Alice,admin"));
1553        assert!(lines.contains(&"Bob,user"));
1554        assert!(!output.contains("30"));
1555    }
1556
1557    #[test]
1558    fn test_render_auto_csv_mode_with_array_field() {
1559        use serde_json::json;
1560
1561        let theme = Theme::new();
1562        let data = json!([
1563            {"name": "Alice", "tags": ["admin", "user"]},
1564            {"name": "Bob", "tags": ["user"]}
1565        ]);
1566
1567        let output = render_auto("unused", &data, &theme, OutputMode::Csv).unwrap();
1568
1569        // Array fields should be flattened with indexed keys
1570        assert!(output.contains("tags.0"));
1571        assert!(output.contains("tags.1"));
1572        assert!(output.contains("admin"));
1573        assert!(output.contains("user"));
1574        // Should NOT contain JSON array syntax
1575        assert!(!output.contains("[\""));
1576    }
1577
1578    // ============================================================================
1579    // Context Injection Tests
1580    // ============================================================================
1581
1582    #[test]
1583    fn test_render_with_context_basic() {
1584        use crate::context::{ContextRegistry, RenderContext};
1585
1586        #[derive(Serialize)]
1587        struct Data {
1588            name: String,
1589        }
1590
1591        let theme = Theme::new();
1592        let data = Data {
1593            name: "Alice".into(),
1594        };
1595        let json_data = serde_json::to_value(&data).unwrap();
1596
1597        let mut registry = ContextRegistry::new();
1598        registry.add_static("version", Value::from("1.0.0"));
1599
1600        let render_ctx = RenderContext::new(OutputMode::Text, Some(80), &theme, &json_data);
1601
1602        let output = render_with_context(
1603            "{{ name }} (v{{ version }})",
1604            &data,
1605            &theme,
1606            OutputMode::Text,
1607            &registry,
1608            &render_ctx,
1609            None,
1610        )
1611        .unwrap();
1612
1613        assert_eq!(output, "Alice (v1.0.0)");
1614    }
1615
1616    #[test]
1617    fn test_render_with_context_dynamic_provider() {
1618        use crate::context::{ContextRegistry, RenderContext};
1619
1620        #[derive(Serialize)]
1621        struct Data {
1622            message: String,
1623        }
1624
1625        let theme = Theme::new();
1626        let data = Data {
1627            message: "Hello".into(),
1628        };
1629        let json_data = serde_json::to_value(&data).unwrap();
1630
1631        let mut registry = ContextRegistry::new();
1632        registry.add_provider("terminal_width", |ctx: &RenderContext| {
1633            Value::from(ctx.terminal_width.unwrap_or(80))
1634        });
1635
1636        let render_ctx = RenderContext::new(OutputMode::Text, Some(120), &theme, &json_data);
1637
1638        let output = render_with_context(
1639            "{{ message }} (width={{ terminal_width }})",
1640            &data,
1641            &theme,
1642            OutputMode::Text,
1643            &registry,
1644            &render_ctx,
1645            None,
1646        )
1647        .unwrap();
1648
1649        assert_eq!(output, "Hello (width=120)");
1650    }
1651
1652    #[test]
1653    fn test_render_with_context_data_takes_precedence() {
1654        use crate::context::{ContextRegistry, RenderContext};
1655
1656        #[derive(Serialize)]
1657        struct Data {
1658            value: String,
1659        }
1660
1661        let theme = Theme::new();
1662        let data = Data {
1663            value: "from_data".into(),
1664        };
1665        let json_data = serde_json::to_value(&data).unwrap();
1666
1667        let mut registry = ContextRegistry::new();
1668        registry.add_static("value", Value::from("from_context"));
1669
1670        let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1671
1672        let output = render_with_context(
1673            "{{ value }}",
1674            &data,
1675            &theme,
1676            OutputMode::Text,
1677            &registry,
1678            &render_ctx,
1679            None,
1680        )
1681        .unwrap();
1682
1683        assert_eq!(output, "from_data");
1684    }
1685
1686    #[test]
1687    fn test_render_with_context_empty_registry() {
1688        use crate::context::{ContextRegistry, RenderContext};
1689
1690        #[derive(Serialize)]
1691        struct Data {
1692            name: String,
1693        }
1694
1695        let theme = Theme::new();
1696        let data = Data {
1697            name: "Test".into(),
1698        };
1699        let json_data = serde_json::to_value(&data).unwrap();
1700
1701        let registry = ContextRegistry::new();
1702        let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1703
1704        let output = render_with_context(
1705            "{{ name }}",
1706            &data,
1707            &theme,
1708            OutputMode::Text,
1709            &registry,
1710            &render_ctx,
1711            None,
1712        )
1713        .unwrap();
1714
1715        assert_eq!(output, "Test");
1716    }
1717
1718    #[test]
1719    fn test_render_auto_with_context_json_mode() {
1720        use crate::context::{ContextRegistry, RenderContext};
1721
1722        #[derive(Serialize)]
1723        struct Data {
1724            count: usize,
1725        }
1726
1727        let theme = Theme::new();
1728        let data = Data { count: 42 };
1729        let json_data = serde_json::to_value(&data).unwrap();
1730
1731        let mut registry = ContextRegistry::new();
1732        registry.add_static("extra", Value::from("ignored"));
1733
1734        let render_ctx = RenderContext::new(OutputMode::Json, None, &theme, &json_data);
1735
1736        let output = render_auto_with_context(
1737            "unused template {{ extra }}",
1738            &data,
1739            &theme,
1740            OutputMode::Json,
1741            &registry,
1742            &render_ctx,
1743            None,
1744        )
1745        .unwrap();
1746
1747        assert!(output.contains("\"count\": 42"));
1748        assert!(!output.contains("ignored"));
1749    }
1750
1751    #[test]
1752    fn test_render_auto_with_context_text_mode() {
1753        use crate::context::{ContextRegistry, RenderContext};
1754
1755        #[derive(Serialize)]
1756        struct Data {
1757            count: usize,
1758        }
1759
1760        let theme = Theme::new();
1761        let data = Data { count: 42 };
1762        let json_data = serde_json::to_value(&data).unwrap();
1763
1764        let mut registry = ContextRegistry::new();
1765        registry.add_static("label", Value::from("Items"));
1766
1767        let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1768
1769        let output = render_auto_with_context(
1770            "{{ label }}: {{ count }}",
1771            &data,
1772            &theme,
1773            OutputMode::Text,
1774            &registry,
1775            &render_ctx,
1776            None,
1777        )
1778        .unwrap();
1779
1780        assert_eq!(output, "Items: 42");
1781    }
1782
1783    #[test]
1784    fn test_render_with_context_provider_uses_output_mode() {
1785        use crate::context::{ContextRegistry, RenderContext};
1786
1787        #[derive(Serialize)]
1788        struct Data {}
1789
1790        let theme = Theme::new();
1791        let data = Data {};
1792        let json_data = serde_json::to_value(&data).unwrap();
1793
1794        let mut registry = ContextRegistry::new();
1795        registry.add_provider("mode", |ctx: &RenderContext| {
1796            Value::from(format!("{:?}", ctx.output_mode))
1797        });
1798
1799        let render_ctx = RenderContext::new(OutputMode::Term, None, &theme, &json_data);
1800
1801        let output = render_with_context(
1802            "Mode: {{ mode }}",
1803            &data,
1804            &theme,
1805            OutputMode::Term,
1806            &registry,
1807            &render_ctx,
1808            None,
1809        )
1810        .unwrap();
1811
1812        assert_eq!(output, "Mode: Term");
1813    }
1814
1815    #[test]
1816    fn test_render_with_context_nested_data() {
1817        use crate::context::{ContextRegistry, RenderContext};
1818
1819        #[derive(Serialize)]
1820        struct Item {
1821            name: String,
1822        }
1823
1824        #[derive(Serialize)]
1825        struct Data {
1826            items: Vec<Item>,
1827        }
1828
1829        let theme = Theme::new();
1830        let data = Data {
1831            items: vec![Item { name: "one".into() }, Item { name: "two".into() }],
1832        };
1833        let json_data = serde_json::to_value(&data).unwrap();
1834
1835        let mut registry = ContextRegistry::new();
1836        registry.add_static("prefix", Value::from("- "));
1837
1838        let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1839
1840        let output = render_with_context(
1841            "{% for item in items %}{{ prefix }}{{ item.name }}\n{% endfor %}",
1842            &data,
1843            &theme,
1844            OutputMode::Text,
1845            &registry,
1846            &render_ctx,
1847            None,
1848        )
1849        .unwrap();
1850
1851        assert_eq!(output, "- one\n- two\n");
1852    }
1853
1854    #[test]
1855    fn test_render_with_mode_forces_color_mode() {
1856        use console::Style;
1857
1858        #[derive(Serialize)]
1859        struct Data {
1860            status: String,
1861        }
1862
1863        // Create an adaptive theme with different colors for light/dark
1864        // Note: force_styling(true) is needed in tests since there's no TTY
1865        let theme = Theme::new().add_adaptive(
1866            "status",
1867            Style::new(),                                   // Base
1868            Some(Style::new().black().force_styling(true)), // Light mode
1869            Some(Style::new().white().force_styling(true)), // Dark mode
1870        );
1871
1872        let data = Data {
1873            status: "test".into(),
1874        };
1875
1876        // Force dark mode
1877        let dark_output = render_with_mode(
1878            r#"[status]{{ status }}[/status]"#,
1879            &data,
1880            &theme,
1881            OutputMode::Term,
1882            ColorMode::Dark,
1883        )
1884        .unwrap();
1885
1886        // Force light mode
1887        let light_output = render_with_mode(
1888            r#"[status]{{ status }}[/status]"#,
1889            &data,
1890            &theme,
1891            OutputMode::Term,
1892            ColorMode::Light,
1893        )
1894        .unwrap();
1895
1896        // They should be different (different colors applied)
1897        assert_ne!(dark_output, light_output);
1898
1899        // Dark mode should use white (ANSI 37)
1900        assert!(
1901            dark_output.contains("\x1b[37"),
1902            "Expected white (37) in dark mode"
1903        );
1904
1905        // Light mode should use black (ANSI 30)
1906        assert!(
1907            light_output.contains("\x1b[30"),
1908            "Expected black (30) in light mode"
1909        );
1910    }
1911
1912    // ============================================================================
1913    // BBParser Tag Syntax Tests
1914    // ============================================================================
1915
1916    #[test]
1917    fn test_tag_syntax_text_mode() {
1918        let theme = Theme::new().add("title", Style::new().bold());
1919
1920        #[derive(Serialize)]
1921        struct Data {
1922            name: String,
1923        }
1924
1925        let output = render_with_output(
1926            "[title]{{ name }}[/title]",
1927            &Data {
1928                name: "Hello".into(),
1929            },
1930            &theme,
1931            OutputMode::Text,
1932        )
1933        .unwrap();
1934
1935        // Tags should be stripped in text mode
1936        assert_eq!(output, "Hello");
1937    }
1938
1939    #[test]
1940    fn test_tag_syntax_term_mode() {
1941        let theme = Theme::new().add("bold", Style::new().bold().force_styling(true));
1942
1943        #[derive(Serialize)]
1944        struct Data {
1945            name: String,
1946        }
1947
1948        let output = render_with_output(
1949            "[bold]{{ name }}[/bold]",
1950            &Data {
1951                name: "Hello".into(),
1952            },
1953            &theme,
1954            OutputMode::Term,
1955        )
1956        .unwrap();
1957
1958        // Should contain ANSI bold codes
1959        assert!(output.contains("\x1b[1m"));
1960        assert!(output.contains("Hello"));
1961    }
1962
1963    #[test]
1964    fn test_tag_syntax_debug_mode() {
1965        let theme = Theme::new().add("title", Style::new().bold());
1966
1967        #[derive(Serialize)]
1968        struct Data {
1969            name: String,
1970        }
1971
1972        let output = render_with_output(
1973            "[title]{{ name }}[/title]",
1974            &Data {
1975                name: "Hello".into(),
1976            },
1977            &theme,
1978            OutputMode::TermDebug,
1979        )
1980        .unwrap();
1981
1982        // Tags should be preserved in debug mode
1983        assert_eq!(output, "[title]Hello[/title]");
1984    }
1985
1986    #[test]
1987    fn test_tag_syntax_unknown_tag_passthrough() {
1988        // Passthrough with ? marker only applies in Apply mode (Term)
1989        let theme = Theme::new().add("known", Style::new().bold());
1990
1991        #[derive(Serialize)]
1992        struct Data {
1993            name: String,
1994        }
1995
1996        // In Term mode, unknown tags get ? marker
1997        let output = render_with_output(
1998            "[unknown]{{ name }}[/unknown]",
1999            &Data {
2000                name: "Hello".into(),
2001            },
2002            &theme,
2003            OutputMode::Term,
2004        )
2005        .unwrap();
2006
2007        // Unknown tags get ? marker in passthrough mode
2008        assert!(output.contains("[unknown?]"));
2009        assert!(output.contains("[/unknown?]"));
2010        assert!(output.contains("Hello"));
2011
2012        // In Text mode, all tags are stripped (Remove transform)
2013        let text_output = render_with_output(
2014            "[unknown]{{ name }}[/unknown]",
2015            &Data {
2016                name: "Hello".into(),
2017            },
2018            &theme,
2019            OutputMode::Text,
2020        )
2021        .unwrap();
2022
2023        // Text mode strips all tags
2024        assert_eq!(text_output, "Hello");
2025    }
2026
2027    #[test]
2028    fn test_tag_syntax_nested() {
2029        let theme = Theme::new()
2030            .add("bold", Style::new().bold().force_styling(true))
2031            .add("red", Style::new().red().force_styling(true));
2032
2033        #[derive(Serialize)]
2034        struct Data {
2035            word: String,
2036        }
2037
2038        let output = render_with_output(
2039            "[bold][red]{{ word }}[/red][/bold]",
2040            &Data {
2041                word: "test".into(),
2042            },
2043            &theme,
2044            OutputMode::Term,
2045        )
2046        .unwrap();
2047
2048        // Should contain both bold and red ANSI codes
2049        assert!(output.contains("\x1b[1m")); // Bold
2050        assert!(output.contains("\x1b[31m")); // Red
2051        assert!(output.contains("test"));
2052    }
2053
2054    #[test]
2055    fn test_tag_syntax_multiple_styles() {
2056        let theme = Theme::new()
2057            .add("title", Style::new().bold())
2058            .add("count", Style::new().cyan());
2059
2060        #[derive(Serialize)]
2061        struct Data {
2062            name: String,
2063            num: usize,
2064        }
2065
2066        let output = render_with_output(
2067            r#"[title]{{ name }}[/title]: [count]{{ num }}[/count]"#,
2068            &Data {
2069                name: "Items".into(),
2070                num: 42,
2071            },
2072            &theme,
2073            OutputMode::Text,
2074        )
2075        .unwrap();
2076
2077        assert_eq!(output, "Items: 42");
2078    }
2079
2080    #[test]
2081    fn test_tag_syntax_in_loop() {
2082        let theme = Theme::new().add("item", Style::new().cyan());
2083
2084        #[derive(Serialize)]
2085        struct Data {
2086            items: Vec<String>,
2087        }
2088
2089        let output = render_with_output(
2090            "{% for item in items %}[item]{{ item }}[/item]\n{% endfor %}",
2091            &Data {
2092                items: vec!["one".into(), "two".into()],
2093            },
2094            &theme,
2095            OutputMode::Text,
2096        )
2097        .unwrap();
2098
2099        assert_eq!(output, "one\ntwo\n");
2100    }
2101
2102    #[test]
2103    fn test_tag_syntax_literal_brackets() {
2104        // Tags that don't match our pattern should pass through
2105        let theme = Theme::new();
2106
2107        #[derive(Serialize)]
2108        struct Data {
2109            msg: String,
2110        }
2111
2112        let output = render_with_output(
2113            "Array: [1, 2, 3] and {{ msg }}",
2114            &Data { msg: "done".into() },
2115            &theme,
2116            OutputMode::Text,
2117        )
2118        .unwrap();
2119
2120        // Non-tag brackets preserved
2121        assert_eq!(output, "Array: [1, 2, 3] and done");
2122    }
2123
2124    // ============================================================================
2125    // Template Validation Tests
2126    // ============================================================================
2127
2128    #[test]
2129    fn test_validate_template_all_known_tags() {
2130        let theme = Theme::new()
2131            .add("title", Style::new().bold())
2132            .add("count", Style::new().cyan());
2133
2134        #[derive(Serialize)]
2135        struct Data {
2136            name: String,
2137        }
2138
2139        let result = validate_template(
2140            "[title]{{ name }}[/title]",
2141            &Data {
2142                name: "Hello".into(),
2143            },
2144            &theme,
2145        );
2146
2147        assert!(result.is_ok());
2148    }
2149
2150    #[test]
2151    fn test_validate_template_unknown_tag_fails() {
2152        let theme = Theme::new().add("known", Style::new().bold());
2153
2154        #[derive(Serialize)]
2155        struct Data {
2156            name: String,
2157        }
2158
2159        let result = validate_template(
2160            "[unknown]{{ name }}[/unknown]",
2161            &Data {
2162                name: "Hello".into(),
2163            },
2164            &theme,
2165        );
2166
2167        assert!(result.is_err());
2168        let err = result.unwrap_err();
2169        let errors = err
2170            .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2171            .expect("Expected UnknownTagErrors");
2172        assert_eq!(errors.len(), 2); // open and close tags
2173    }
2174
2175    #[test]
2176    fn test_validate_template_multiple_unknown_tags() {
2177        let theme = Theme::new().add("known", Style::new().bold());
2178
2179        #[derive(Serialize)]
2180        struct Data {
2181            a: String,
2182            b: String,
2183        }
2184
2185        let result = validate_template(
2186            "[foo]{{ a }}[/foo] and [bar]{{ b }}[/bar]",
2187            &Data {
2188                a: "x".into(),
2189                b: "y".into(),
2190            },
2191            &theme,
2192        );
2193
2194        assert!(result.is_err());
2195        let err = result.unwrap_err();
2196        let errors = err
2197            .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2198            .expect("Expected UnknownTagErrors");
2199        assert_eq!(errors.len(), 4); // foo open/close + bar open/close
2200    }
2201
2202    #[test]
2203    fn test_validate_template_plain_text_passes() {
2204        let theme = Theme::new();
2205
2206        #[derive(Serialize)]
2207        struct Data {
2208            msg: String,
2209        }
2210
2211        let result = validate_template("Just plain {{ msg }}", &Data { msg: "hi".into() }, &theme);
2212
2213        assert!(result.is_ok());
2214    }
2215
2216    #[test]
2217    fn test_validate_template_mixed_known_and_unknown() {
2218        let theme = Theme::new().add("known", Style::new().bold());
2219
2220        #[derive(Serialize)]
2221        struct Data {
2222            a: String,
2223            b: String,
2224        }
2225
2226        let result = validate_template(
2227            "[known]{{ a }}[/known] [unknown]{{ b }}[/unknown]",
2228            &Data {
2229                a: "x".into(),
2230                b: "y".into(),
2231            },
2232            &theme,
2233        );
2234
2235        assert!(result.is_err());
2236        let err = result.unwrap_err();
2237        let errors = err
2238            .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2239            .expect("Expected UnknownTagErrors");
2240        // Only unknown tags should be reported
2241        assert_eq!(errors.len(), 2);
2242        assert!(errors.errors.iter().any(|e| e.tag == "unknown"));
2243    }
2244
2245    #[test]
2246    fn test_validate_template_syntax_error_fails() {
2247        let theme = Theme::new();
2248        #[derive(Serialize)]
2249        struct Data {}
2250
2251        // Missing closing braces
2252        let result = validate_template("{{ unclosed", &Data {}, &theme);
2253        assert!(result.is_err());
2254
2255        let err = result.unwrap_err();
2256        // Should NOT be UnknownTagErrors
2257        assert!(err
2258            .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2259            .is_none());
2260        // Should be a minijinja error
2261        let msg = err.to_string();
2262        assert!(
2263            msg.contains("syntax error") || msg.contains("unexpected"),
2264            "Got: {}",
2265            msg
2266        );
2267    }
2268
2269    #[test]
2270    fn test_render_auto_with_context_yaml_mode() {
2271        use crate::context::{ContextRegistry, RenderContext};
2272        use serde_json::json;
2273
2274        let theme = Theme::new();
2275        let data = json!({"name": "test", "count": 42});
2276
2277        // Setup context registry (though strictly not used for structured output)
2278        let registry = ContextRegistry::new();
2279        let render_ctx = RenderContext::new(OutputMode::Yaml, Some(80), &theme, &data);
2280
2281        // This call previously panicked
2282        let output = render_auto_with_context(
2283            "unused template",
2284            &data,
2285            &theme,
2286            OutputMode::Yaml,
2287            &registry,
2288            &render_ctx,
2289            None,
2290        )
2291        .unwrap();
2292
2293        assert!(output.contains("name: test"));
2294        assert!(output.contains("count: 42"));
2295    }
2296
2297    // =========================================================================
2298    // Icon integration tests
2299    // =========================================================================
2300
2301    #[test]
2302    #[serial_test::serial]
2303    fn test_render_with_icons_classic() {
2304        use crate::{set_icon_detector, IconDefinition, IconMode};
2305
2306        set_icon_detector(|| IconMode::Classic);
2307
2308        let theme = Theme::new()
2309            .add_icon(
2310                "check",
2311                IconDefinition::new("[ok]").with_nerdfont("\u{f00c}"),
2312            )
2313            .add_icon("arrow", IconDefinition::new(">>"));
2314
2315        let data = SimpleData {
2316            message: "done".into(),
2317        };
2318
2319        let output = render_with_output(
2320            "{{ icons.check }} {{ message }} {{ icons.arrow }}",
2321            &data,
2322            &theme,
2323            OutputMode::Text,
2324        )
2325        .unwrap();
2326
2327        assert_eq!(output, "[ok] done >>");
2328    }
2329
2330    #[test]
2331    #[serial_test::serial]
2332    fn test_render_with_icons_nerdfont() {
2333        use crate::{set_icon_detector, IconDefinition, IconMode};
2334
2335        set_icon_detector(|| IconMode::NerdFont);
2336
2337        let theme = Theme::new().add_icon(
2338            "check",
2339            IconDefinition::new("[ok]").with_nerdfont("\u{f00c}"),
2340        );
2341
2342        let data = SimpleData {
2343            message: "done".into(),
2344        };
2345
2346        let output = render_with_output(
2347            "{{ icons.check }} {{ message }}",
2348            &data,
2349            &theme,
2350            OutputMode::Text,
2351        )
2352        .unwrap();
2353
2354        assert_eq!(output, "\u{f00c} done");
2355
2356        // Reset
2357        set_icon_detector(|| IconMode::Classic);
2358    }
2359
2360    #[test]
2361    fn test_render_without_icons_no_overhead() {
2362        let theme = Theme::new();
2363        let data = SimpleData {
2364            message: "hello".into(),
2365        };
2366
2367        // Should work fine without icons
2368        let output = render_with_output("{{ message }}", &data, &theme, OutputMode::Text).unwrap();
2369
2370        assert_eq!(output, "hello");
2371    }
2372
2373    #[test]
2374    #[serial_test::serial]
2375    fn test_render_with_icons_and_styles() {
2376        use crate::{set_icon_detector, IconDefinition, IconMode};
2377
2378        set_icon_detector(|| IconMode::Classic);
2379
2380        let theme = Theme::new()
2381            .add("title", Style::new().bold())
2382            .add_icon("bullet", IconDefinition::new("-"));
2383
2384        let data = SimpleData {
2385            message: "item".into(),
2386        };
2387
2388        let output = render_with_output(
2389            "{{ icons.bullet }} [title]{{ message }}[/title]",
2390            &data,
2391            &theme,
2392            OutputMode::Text,
2393        )
2394        .unwrap();
2395
2396        assert_eq!(output, "- item");
2397    }
2398
2399    #[test]
2400    #[serial_test::serial]
2401    fn test_render_with_vars_includes_icons() {
2402        use crate::{set_icon_detector, IconDefinition, IconMode};
2403
2404        set_icon_detector(|| IconMode::Classic);
2405
2406        let theme = Theme::new().add_icon("star", IconDefinition::new("*"));
2407
2408        let data = SimpleData {
2409            message: "hello".into(),
2410        };
2411
2412        let vars = std::collections::HashMap::from([("version", "1.0")]);
2413
2414        let output = render_with_vars(
2415            "{{ icons.star }} {{ message }} v{{ version }}",
2416            &data,
2417            &theme,
2418            OutputMode::Text,
2419            vars,
2420        )
2421        .unwrap();
2422
2423        assert_eq!(output, "* hello v1.0");
2424    }
2425
2426    #[test]
2427    #[serial_test::serial]
2428    fn test_render_with_context_includes_icons() {
2429        use crate::context::{ContextRegistry, RenderContext};
2430        use crate::{set_icon_detector, IconDefinition, IconMode};
2431
2432        set_icon_detector(|| IconMode::Classic);
2433
2434        let theme = Theme::new().add_icon("dot", IconDefinition::new("."));
2435
2436        let data = SimpleData {
2437            message: "test".into(),
2438        };
2439
2440        let mut registry = ContextRegistry::new();
2441        registry.add_static("extra", Value::from("ctx"));
2442
2443        let json_data = serde_json::to_value(&data).unwrap();
2444        let render_ctx = RenderContext::new(OutputMode::Text, Some(80), &theme, &json_data);
2445
2446        let output = render_with_context(
2447            "{{ icons.dot }} {{ message }} {{ extra }}",
2448            &data,
2449            &theme,
2450            OutputMode::Text,
2451            &registry,
2452            &render_ctx,
2453            None,
2454        )
2455        .unwrap();
2456
2457        assert_eq!(output, ". test ctx");
2458    }
2459
2460    #[test]
2461    #[serial_test::serial]
2462    fn test_render_yaml_from_theme_with_icons() {
2463        use crate::{set_icon_detector, IconDefinition, IconMode};
2464
2465        set_icon_detector(|| IconMode::Classic);
2466
2467        let theme = Theme::from_yaml(
2468            r#"
2469            title:
2470                fg: cyan
2471                bold: true
2472            icons:
2473                check:
2474                    classic: "[ok]"
2475                    nerdfont: "nf"
2476            "#,
2477        )
2478        .unwrap();
2479
2480        let data = SimpleData {
2481            message: "done".into(),
2482        };
2483
2484        let output = render_with_output(
2485            "{{ icons.check }} [title]{{ message }}[/title]",
2486            &data,
2487            &theme,
2488            OutputMode::Text,
2489        )
2490        .unwrap();
2491
2492        assert_eq!(output, "[ok] done");
2493    }
2494}