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, 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
371    let engine = MiniJinjaEngine::new();
372    let data_value = serde_json::to_value(data)?;
373    let template_output = engine.render_template(template, &data_value)?;
374
375    // Pass 2: BBParser style tag processing
376    let final_output = apply_style_tags(&template_output, &styles, output_mode);
377
378    Ok(final_output)
379}
380
381/// Renders a template with additional variables injected into the context.
382///
383/// This is a convenience function for adding simple key-value pairs to the template
384/// context without the complexity of the full [`ContextRegistry`] system. The data
385/// fields take precedence over the injected variables.
386///
387/// # Arguments
388///
389/// * `template` - A minijinja template string
390/// * `data` - The primary serializable data to render
391/// * `theme` - Theme definitions for style tag processing
392/// * `vars` - Additional variables to inject into the template context
393///
394/// # Example
395///
396/// ```rust
397/// use standout_render::{render_with_vars, Theme, OutputMode};
398/// use serde::Serialize;
399/// use std::collections::HashMap;
400///
401/// #[derive(Serialize)]
402/// struct User { name: String }
403///
404/// let theme = Theme::new();
405/// let user = User { name: "Alice".into() };
406///
407/// let mut vars = HashMap::new();
408/// vars.insert("version", "1.0.0");
409/// vars.insert("app_name", "MyApp");
410///
411/// let output = render_with_vars(
412///     "{{ name }} - {{ app_name }} v{{ version }}",
413///     &user,
414///     &theme,
415///     OutputMode::Text,
416///     vars,
417/// ).unwrap();
418///
419/// assert_eq!(output, "Alice - MyApp v1.0.0");
420/// ```
421pub fn render_with_vars<T, K, V, I>(
422    template: &str,
423    data: &T,
424    theme: &Theme,
425    mode: OutputMode,
426    vars: I,
427) -> Result<String, RenderError>
428where
429    T: Serialize,
430    K: AsRef<str>,
431    V: Into<serde_json::Value>,
432    I: IntoIterator<Item = (K, V)>,
433{
434    let color_mode = detect_color_mode();
435    let styles = theme.resolve_styles(Some(color_mode));
436
437    // Validate style aliases before rendering
438    styles
439        .validate()
440        .map_err(|e| RenderError::StyleError(e.to_string()))?;
441
442    // Build context from vars
443    let mut context: HashMap<String, serde_json::Value> = HashMap::new();
444    for (key, value) in vars {
445        context.insert(key.as_ref().to_string(), value.into());
446    }
447
448    // Pass 1: Template rendering with context
449    let engine = MiniJinjaEngine::new();
450    let data_value = serde_json::to_value(data)?;
451    let template_output = engine.render_with_context(template, &data_value, context)?;
452
453    // Pass 2: BBParser style tag processing
454    let final_output = apply_style_tags(&template_output, &styles, mode);
455
456    Ok(final_output)
457}
458
459/// Auto-dispatches between template rendering and direct serialization.
460///
461/// This is the recommended function when you want to support both human-readable
462/// output (terminal, text) and machine-readable output (JSON, YAML, etc.). For
463/// structured modes like `Json`, the data is serialized directly, skipping
464/// template rendering entirely.
465///
466/// # Arguments
467///
468/// * `template` - A minijinja template string (ignored for structured modes)
469/// * `data` - Any serializable data to render or serialize
470/// * `theme` - Theme definitions for the `style` filter (ignored for structured modes)
471/// * `mode` - Output mode determining the output format
472///
473/// # Example
474///
475/// ```rust
476/// use standout_render::{render_auto, Theme, OutputMode};
477/// use console::Style;
478/// use serde::Serialize;
479///
480/// #[derive(Serialize)]
481/// struct Report { title: String, count: usize }
482///
483/// let theme = Theme::new().add("title", Style::new().bold());
484/// let data = Report { title: "Summary".into(), count: 42 };
485///
486/// // Terminal output uses the template
487/// let term = render_auto(
488///     r#"[title]{{ title }}[/title]: {{ count }}"#,
489///     &data,
490///     &theme,
491///     OutputMode::Text,
492/// ).unwrap();
493/// assert_eq!(term, "Summary: 42");
494///
495/// // JSON output serializes directly
496/// let json = render_auto(
497///     r#"[title]{{ title }}[/title]: {{ count }}"#,
498///     &data,
499///     &theme,
500///     OutputMode::Json,
501/// ).unwrap();
502/// assert!(json.contains("\"title\": \"Summary\""));
503/// assert!(json.contains("\"count\": 42"));
504/// ```
505pub fn render_auto<T: Serialize>(
506    template: &str,
507    data: &T,
508    theme: &Theme,
509    mode: OutputMode,
510) -> Result<String, RenderError> {
511    if mode.is_structured() {
512        match mode {
513            OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
514            OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
515            OutputMode::Xml => Ok(quick_xml::se::to_string(data)?),
516            OutputMode::Csv => {
517                let value = serde_json::to_value(data)?;
518                let (headers, rows) = crate::util::flatten_json_for_csv(&value);
519
520                let mut wtr = csv::Writer::from_writer(Vec::new());
521                wtr.write_record(&headers)?;
522                for row in rows {
523                    wtr.write_record(&row)?;
524                }
525                let bytes = wtr.into_inner()?;
526                Ok(String::from_utf8(bytes)?)
527            }
528            _ => unreachable!("is_structured() returned true for non-structured mode"),
529        }
530    } else {
531        render_with_output(template, data, theme, mode)
532    }
533}
534
535/// Auto-dispatches with granular control over structured output.
536///
537/// Similar to `render_auto`, but allows passing an optional `FlatDataSpec`.
538/// This is particularly useful for controlling CSV output structure (columns, headers)
539/// instead of relying on automatic JSON flattening.
540///
541/// # Arguments
542///
543/// * `template` - A minijinja template string
544/// * `data` - Any serializable data to render or serialize
545/// * `theme` - Theme definitions for the `style` filter
546/// * `mode` - Output mode determining the output format
547/// * `spec` - Optional `FlatDataSpec` for defining CSV/Table structure
548pub fn render_auto_with_spec<T: Serialize>(
549    template: &str,
550    data: &T,
551    theme: &Theme,
552    mode: OutputMode,
553    spec: Option<&FlatDataSpec>,
554) -> Result<String, RenderError> {
555    if mode.is_structured() {
556        match mode {
557            OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
558            OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
559            OutputMode::Xml => Ok(quick_xml::se::to_string(data)?),
560            OutputMode::Csv => {
561                let value = serde_json::to_value(data)?;
562
563                let (headers, rows) = if let Some(s) = spec {
564                    // Use the spec for explicit extraction
565                    let headers = s.extract_header();
566                    let rows: Vec<Vec<String>> = match value {
567                        serde_json::Value::Array(items) => {
568                            items.iter().map(|item| s.extract_row(item)).collect()
569                        }
570                        _ => vec![s.extract_row(&value)],
571                    };
572                    (headers, rows)
573                } else {
574                    // Use automatic flattening
575                    crate::util::flatten_json_for_csv(&value)
576                };
577
578                let mut wtr = csv::Writer::from_writer(Vec::new());
579                wtr.write_record(&headers)?;
580                for row in rows {
581                    wtr.write_record(&row)?;
582                }
583                let bytes = wtr.into_inner()?;
584                Ok(String::from_utf8(bytes)?)
585            }
586            _ => unreachable!("is_structured() returned true for non-structured mode"),
587        }
588    } else {
589        render_with_output(template, data, theme, mode)
590    }
591}
592
593/// Renders a template with additional context objects injected.
594///
595/// This is the most flexible rendering function, allowing you to inject
596/// additional objects into the template context beyond the serialized data.
597/// Use this when templates need access to utilities, formatters, or runtime
598/// values that cannot be represented as JSON.
599///
600/// # Arguments
601///
602/// * `template` - A minijinja template string
603/// * `data` - Any serializable data to pass to the template
604/// * `theme` - Theme definitions for the `style` filter
605/// * `mode` - Output mode: `Auto`, `Term`, `Text`, etc.
606/// * `context_registry` - Additional context objects to inject
607/// * `render_context` - Information about the render environment
608///
609/// # Context Resolution
610///
611/// Context objects are resolved from the registry using the provided
612/// `RenderContext`. Each registered provider is called to produce a value,
613/// which is then merged into the template context.
614///
615/// If a context key conflicts with a data field, the data field wins.
616/// Context is supplementary to the handler's data, not a replacement.
617///
618/// # Example
619///
620/// ```rust
621/// use standout_render::{render_with_context, Theme, OutputMode};
622/// use standout_render::context::{RenderContext, ContextRegistry};
623/// use minijinja::Value;
624/// use serde::Serialize;
625///
626/// #[derive(Serialize)]
627/// struct Data { name: String }
628///
629/// let theme = Theme::new();
630/// let data = Data { name: "Alice".into() };
631///
632/// // Create context with a static value
633/// let mut registry = ContextRegistry::new();
634/// registry.add_static("version", Value::from("1.0.0"));
635///
636/// // Create render context
637/// let json_data = serde_json::to_value(&data).unwrap();
638/// let render_ctx = RenderContext::new(
639///     OutputMode::Text,
640///     Some(80),
641///     &theme,
642///     &json_data,
643/// );
644///
645/// let output = render_with_context(
646///     "{{ name }} (v{{ version }})",
647///     &data,
648///     &theme,
649///     OutputMode::Text,
650///     &registry,
651///     &render_ctx,
652///     None,
653/// ).unwrap();
654///
655/// assert_eq!(output, "Alice (v1.0.0)");
656/// ```
657pub fn render_with_context<T: Serialize>(
658    template: &str,
659    data: &T,
660    theme: &Theme,
661    mode: OutputMode,
662    context_registry: &ContextRegistry,
663    render_context: &RenderContext,
664    template_registry: Option<&super::TemplateRegistry>,
665) -> Result<String, RenderError> {
666    let color_mode = detect_color_mode();
667    let styles = theme.resolve_styles(Some(color_mode));
668
669    // Validate style aliases before rendering
670    styles
671        .validate()
672        .map_err(|e| RenderError::StyleError(e.to_string()))?;
673
674    let mut engine = MiniJinjaEngine::new();
675
676    // Check if template is a registry key (name) or inline content.
677    // If the registry contains a template with this name, use its content.
678    // Otherwise, treat the template string as inline content.
679    let template_content = if let Some(registry) = template_registry {
680        if let Ok(content) = registry.get_content(template) {
681            content
682        } else {
683            template.to_string()
684        }
685    } else {
686        template.to_string()
687    };
688
689    // Load all templates from registry if available (enables {% include %})
690    if let Some(registry) = template_registry {
691        for name in registry.names() {
692            if let Ok(content) = registry.get_content(name) {
693                engine.add_template(name, &content)?;
694            }
695        }
696    }
697
698    // Build the combined context: data + injected context
699    // Data fields take precedence over context fields
700    let context = build_combined_context(data, context_registry, render_context)?;
701
702    // Pass 1: Template rendering with context
703    let data_value = serde_json::to_value(data)?;
704    let template_output = engine.render_with_context(&template_content, &data_value, context)?;
705
706    // Pass 2: BBParser style tag processing
707    let final_output = apply_style_tags(&template_output, &styles, mode);
708
709    Ok(final_output)
710}
711
712/// Auto-dispatches with context injection support.
713///
714/// This combines `render_with_context` with JSON serialization support.
715/// For structured modes like `Json`, the data is serialized directly,
716/// skipping template rendering (and context injection).
717///
718/// # Arguments
719///
720/// * `template` - A minijinja template string (ignored for structured modes)
721/// * `data` - Any serializable data to render or serialize
722/// * `theme` - Theme definitions for the `style` filter
723/// * `mode` - Output mode determining the output format
724/// * `context_registry` - Additional context objects to inject
725/// * `render_context` - Information about the render environment
726///
727/// # Example
728///
729/// ```rust
730/// use standout_render::{render_auto_with_context, Theme, OutputMode};
731/// use standout_render::context::{RenderContext, ContextRegistry};
732/// use minijinja::Value;
733/// use serde::Serialize;
734///
735/// #[derive(Serialize)]
736/// struct Report { title: String, count: usize }
737///
738/// let theme = Theme::new();
739/// let data = Report { title: "Summary".into(), count: 42 };
740///
741/// let mut registry = ContextRegistry::new();
742/// registry.add_provider("terminal_width", |ctx: &RenderContext| {
743///     Value::from(ctx.terminal_width.unwrap_or(80))
744/// });
745///
746/// let json_data = serde_json::to_value(&data).unwrap();
747/// let render_ctx = RenderContext::new(
748///     OutputMode::Text,
749///     Some(120),
750///     &theme,
751///     &json_data,
752/// );
753///
754/// // Text mode uses the template with context
755/// let text = render_auto_with_context(
756///     "{{ title }} (width={{ terminal_width }}): {{ count }}",
757///     &data,
758///     &theme,
759///     OutputMode::Text,
760///     &registry,
761///     &render_ctx,
762///     None,
763/// ).unwrap();
764/// assert_eq!(text, "Summary (width=120): 42");
765///
766/// // JSON mode ignores template and context, serializes data directly
767/// let json = render_auto_with_context(
768///     "unused",
769///     &data,
770///     &theme,
771///     OutputMode::Json,
772///     &registry,
773///     &render_ctx,
774///     None,
775/// ).unwrap();
776/// assert!(json.contains("\"title\": \"Summary\""));
777/// ```
778pub fn render_auto_with_context<T: Serialize>(
779    template: &str,
780    data: &T,
781    theme: &Theme,
782    mode: OutputMode,
783    context_registry: &ContextRegistry,
784    render_context: &RenderContext,
785    template_registry: Option<&super::TemplateRegistry>,
786) -> Result<String, RenderError> {
787    if mode.is_structured() {
788        match mode {
789            OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
790            OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
791            OutputMode::Xml => Ok(quick_xml::se::to_string(data)?),
792            OutputMode::Csv => {
793                let value = serde_json::to_value(data)?;
794                let (headers, rows) = crate::util::flatten_json_for_csv(&value);
795
796                let mut wtr = csv::Writer::from_writer(Vec::new());
797                wtr.write_record(&headers)?;
798                for row in rows {
799                    wtr.write_record(&row)?;
800                }
801                let bytes = wtr.into_inner()?;
802                Ok(String::from_utf8(bytes)?)
803            }
804            _ => unreachable!("is_structured() returned true for non-structured mode"),
805        }
806    } else {
807        render_with_context(
808            template,
809            data,
810            theme,
811            mode,
812            context_registry,
813            render_context,
814            template_registry,
815        )
816    }
817}
818
819/// Builds a combined context from data and injected context.
820///
821/// Data fields take precedence over context fields.
822fn build_combined_context<T: Serialize>(
823    data: &T,
824    context_registry: &ContextRegistry,
825    render_context: &RenderContext,
826) -> Result<HashMap<String, serde_json::Value>, RenderError> {
827    // First, resolve all context providers
828    let context_values = context_registry.resolve(render_context);
829
830    // Convert data to a map of values
831    let data_value = serde_json::to_value(data)?;
832
833    let mut combined: HashMap<String, serde_json::Value> = HashMap::new();
834
835    // Add context values first (lower priority)
836    for (key, value) in context_values {
837        // Convert minijinja::Value to serde_json::Value
838        // This is a bit inefficient but necessary for the abstraction
839        // In the future, ContextRegistry should probably return serde_json::Value
840        let json_val =
841            serde_json::to_value(value).map_err(|e| RenderError::ContextError(e.to_string()))?;
842        combined.insert(key, json_val);
843    }
844
845    // Add data values (higher priority - overwrites context)
846    if let Some(obj) = data_value.as_object() {
847        for (key, value) in obj {
848            combined.insert(key.clone(), value.clone());
849        }
850    }
851
852    Ok(combined)
853}
854
855/// Auto-dispatches rendering using a provided TemplateEngine.
856///
857/// This is similar to `render_auto_with_context` but allows using a pre-configured
858/// `TemplateEngine` trait object instead of creating a new dictionary-based engine.
859pub fn render_auto_with_engine(
860    engine: &dyn super::TemplateEngine,
861    template: &str,
862    data: &serde_json::Value,
863    theme: &Theme,
864    mode: OutputMode,
865    context_registry: &ContextRegistry,
866    render_context: &RenderContext,
867) -> Result<String, RenderError> {
868    if mode.is_structured() {
869        match mode {
870            OutputMode::Json => Ok(serde_json::to_string_pretty(data)?),
871            OutputMode::Yaml => Ok(serde_yaml::to_string(data)?),
872            OutputMode::Xml => Ok(quick_xml::se::to_string(data)?),
873            OutputMode::Csv => {
874                let (headers, rows) = crate::util::flatten_json_for_csv(data);
875
876                let mut wtr = csv::Writer::from_writer(Vec::new());
877                wtr.write_record(&headers)?;
878                for row in rows {
879                    wtr.write_record(&row)?;
880                }
881                let bytes = wtr.into_inner()?;
882                Ok(String::from_utf8(bytes)?)
883            }
884            _ => unreachable!("is_structured() returned true for non-structured mode"),
885        }
886    } else {
887        let color_mode = detect_color_mode();
888        let styles = theme.resolve_styles(Some(color_mode));
889
890        // Validate style aliases before rendering
891        styles
892            .validate()
893            .map_err(|e| RenderError::StyleError(e.to_string()))?;
894
895        // Build the combined context: data + injected context
896        // Note: data is already Value, but build_combined_context expects T: Serialize
897        // We can pass &data directly since Value implements Serialize
898        let context_map = build_combined_context(data, context_registry, render_context)?;
899
900        // Merge into a single Value for the engine
901        let combined_value = serde_json::Value::Object(context_map.into_iter().collect());
902
903        // Render template
904        let template_output = if engine.has_template(template) {
905            engine.render_named(template, &combined_value)?
906        } else {
907            engine.render_template(template, &combined_value)?
908        };
909
910        // Apply styles
911        let final_output = apply_style_tags(&template_output, &styles, mode);
912
913        Ok(final_output)
914    }
915}
916
917/// Auto-dispatches rendering and returns both formatted and raw output.
918///
919/// This is similar to `render_auto_with_engine` but returns a `RenderResult`
920/// containing both the formatted output (with ANSI codes) and the raw output
921/// (with style tags but no ANSI codes). The raw output is useful for piping
922/// to external commands.
923///
924/// For structured modes (JSON, YAML, etc.), both formatted and raw are the same
925/// since no style processing occurs.
926pub fn render_auto_with_engine_split(
927    engine: &dyn super::TemplateEngine,
928    template: &str,
929    data: &serde_json::Value,
930    theme: &Theme,
931    mode: OutputMode,
932    context_registry: &ContextRegistry,
933    render_context: &RenderContext,
934) -> Result<RenderResult, RenderError> {
935    if mode.is_structured() {
936        // For structured modes, no style processing, so raw == formatted
937        let output = match mode {
938            OutputMode::Json => serde_json::to_string_pretty(data)?,
939            OutputMode::Yaml => serde_yaml::to_string(data)?,
940            OutputMode::Xml => quick_xml::se::to_string(data)?,
941            OutputMode::Csv => {
942                let (headers, rows) = crate::util::flatten_json_for_csv(data);
943
944                let mut wtr = csv::Writer::from_writer(Vec::new());
945                wtr.write_record(&headers)?;
946                for row in rows {
947                    wtr.write_record(&row)?;
948                }
949                let bytes = wtr.into_inner()?;
950                String::from_utf8(bytes)?
951            }
952            _ => unreachable!("is_structured() returned true for non-structured mode"),
953        };
954        Ok(RenderResult::plain(output))
955    } else {
956        let color_mode = detect_color_mode();
957        let styles = theme.resolve_styles(Some(color_mode));
958
959        // Validate style aliases before rendering
960        styles
961            .validate()
962            .map_err(|e| RenderError::StyleError(e.to_string()))?;
963
964        // Build the combined context: data + injected context
965        let context_map = build_combined_context(data, context_registry, render_context)?;
966
967        // Merge into a single Value for the engine
968        let combined_value = serde_json::Value::Object(context_map.into_iter().collect());
969
970        // Pass 1: Render template (this is the raw/intermediate output)
971        let raw_output = if engine.has_template(template) {
972            engine.render_named(template, &combined_value)?
973        } else {
974            engine.render_template(template, &combined_value)?
975        };
976
977        // Pass 2: Apply styles to get formatted output
978        let formatted_output = apply_style_tags(&raw_output, &styles, mode);
979
980        // For raw output, strip style tags (OutputMode::Text behavior)
981        let stripped_output = apply_style_tags(&raw_output, &styles, OutputMode::Text);
982
983        Ok(RenderResult::new(formatted_output, stripped_output))
984    }
985}
986
987#[cfg(test)]
988mod tests {
989    use super::*;
990    use crate::tabular::{Column, FlatDataSpec, Width};
991    use crate::Theme;
992    use console::Style;
993    use minijinja::Value;
994    use serde::Serialize;
995    use serde_json::json;
996
997    #[derive(Serialize)]
998    struct SimpleData {
999        message: String,
1000    }
1001
1002    #[derive(Serialize)]
1003    struct ListData {
1004        items: Vec<String>,
1005        count: usize,
1006    }
1007
1008    #[test]
1009    fn test_render_with_output_text_no_ansi() {
1010        let theme = Theme::new().add("red", Style::new().red());
1011        let data = SimpleData {
1012            message: "test".into(),
1013        };
1014
1015        let output = render_with_output(
1016            r#"[red]{{ message }}[/red]"#,
1017            &data,
1018            &theme,
1019            OutputMode::Text,
1020        )
1021        .unwrap();
1022
1023        assert_eq!(output, "test");
1024        assert!(!output.contains("\x1b["));
1025    }
1026
1027    #[test]
1028    fn test_render_with_output_term_has_ansi() {
1029        let theme = Theme::new().add("green", Style::new().green().force_styling(true));
1030        let data = SimpleData {
1031            message: "success".into(),
1032        };
1033
1034        let output = render_with_output(
1035            r#"[green]{{ message }}[/green]"#,
1036            &data,
1037            &theme,
1038            OutputMode::Term,
1039        )
1040        .unwrap();
1041
1042        assert!(output.contains("success"));
1043        assert!(output.contains("\x1b["));
1044    }
1045
1046    #[test]
1047    fn test_render_unknown_style_shows_indicator() {
1048        let theme = Theme::new();
1049        let data = SimpleData {
1050            message: "hello".into(),
1051        };
1052
1053        let output = render_with_output(
1054            r#"[unknown]{{ message }}[/unknown]"#,
1055            &data,
1056            &theme,
1057            OutputMode::Term,
1058        )
1059        .unwrap();
1060
1061        // Unknown tags in passthrough mode get ? marker on both open and close tags
1062        assert_eq!(output, "[unknown?]hello[/unknown?]");
1063    }
1064
1065    #[test]
1066    fn test_render_unknown_style_stripped_in_text_mode() {
1067        let theme = Theme::new();
1068        let data = SimpleData {
1069            message: "hello".into(),
1070        };
1071
1072        let output = render_with_output(
1073            r#"[unknown]{{ message }}[/unknown]"#,
1074            &data,
1075            &theme,
1076            OutputMode::Text,
1077        )
1078        .unwrap();
1079
1080        // In text mode (Remove), unknown tags are stripped like known tags
1081        assert_eq!(output, "hello");
1082    }
1083
1084    #[test]
1085    fn test_render_template_with_loop() {
1086        let theme = Theme::new().add("item", Style::new().cyan());
1087        let data = ListData {
1088            items: vec!["one".into(), "two".into()],
1089            count: 2,
1090        };
1091
1092        let template = r#"{% for item in items %}[item]{{ item }}[/item]
1093{% endfor %}"#;
1094
1095        let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1096        assert_eq!(output, "one\ntwo\n");
1097    }
1098
1099    #[test]
1100    fn test_render_mixed_styled_and_plain() {
1101        let theme = Theme::new().add("count", Style::new().bold());
1102        let data = ListData {
1103            items: vec![],
1104            count: 42,
1105        };
1106
1107        let template = r#"Total: [count]{{ count }}[/count] items"#;
1108        let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1109
1110        assert_eq!(output, "Total: 42 items");
1111    }
1112
1113    #[test]
1114    fn test_render_literal_string_styled() {
1115        let theme = Theme::new().add("header", Style::new().bold());
1116
1117        #[derive(Serialize)]
1118        struct Empty {}
1119
1120        let output = render_with_output(
1121            r#"[header]Header[/header]"#,
1122            &Empty {},
1123            &theme,
1124            OutputMode::Text,
1125        )
1126        .unwrap();
1127
1128        assert_eq!(output, "Header");
1129    }
1130
1131    #[test]
1132    fn test_empty_template() {
1133        let theme = Theme::new();
1134
1135        #[derive(Serialize)]
1136        struct Empty {}
1137
1138        let output = render_with_output("", &Empty {}, &theme, OutputMode::Text).unwrap();
1139        assert_eq!(output, "");
1140    }
1141
1142    #[test]
1143    fn test_template_syntax_error() {
1144        let theme = Theme::new();
1145
1146        #[derive(Serialize)]
1147        struct Empty {}
1148
1149        let result = render_with_output("{{ unclosed", &Empty {}, &theme, OutputMode::Text);
1150        assert!(result.is_err());
1151    }
1152
1153    #[test]
1154    fn test_style_tag_with_nested_data() {
1155        #[derive(Serialize)]
1156        struct Item {
1157            name: String,
1158            value: i32,
1159        }
1160
1161        #[derive(Serialize)]
1162        struct Container {
1163            items: Vec<Item>,
1164        }
1165
1166        let theme = Theme::new().add("name", Style::new().bold());
1167        let data = Container {
1168            items: vec![
1169                Item {
1170                    name: "foo".into(),
1171                    value: 1,
1172                },
1173                Item {
1174                    name: "bar".into(),
1175                    value: 2,
1176                },
1177            ],
1178        };
1179
1180        let template = r#"{% for item in items %}[name]{{ item.name }}[/name]={{ item.value }}
1181{% endfor %}"#;
1182
1183        let output = render_with_output(template, &data, &theme, OutputMode::Text).unwrap();
1184        assert_eq!(output, "foo=1\nbar=2\n");
1185    }
1186
1187    #[test]
1188    fn test_render_with_output_term_debug() {
1189        let theme = Theme::new()
1190            .add("title", Style::new().bold())
1191            .add("count", Style::new().cyan());
1192
1193        #[derive(Serialize)]
1194        struct Data {
1195            name: String,
1196            value: usize,
1197        }
1198
1199        let data = Data {
1200            name: "Test".into(),
1201            value: 42,
1202        };
1203
1204        let output = render_with_output(
1205            r#"[title]{{ name }}[/title]: [count]{{ value }}[/count]"#,
1206            &data,
1207            &theme,
1208            OutputMode::TermDebug,
1209        )
1210        .unwrap();
1211
1212        assert_eq!(output, "[title]Test[/title]: [count]42[/count]");
1213    }
1214
1215    #[test]
1216    fn test_render_with_output_term_debug_preserves_tags() {
1217        let theme = Theme::new().add("known", Style::new().bold());
1218
1219        #[derive(Serialize)]
1220        struct Data {
1221            message: String,
1222        }
1223
1224        let data = Data {
1225            message: "hello".into(),
1226        };
1227
1228        // In TermDebug (Keep mode), unknown tags are preserved as-is
1229        let output = render_with_output(
1230            r#"[unknown]{{ message }}[/unknown]"#,
1231            &data,
1232            &theme,
1233            OutputMode::TermDebug,
1234        )
1235        .unwrap();
1236
1237        assert_eq!(output, "[unknown]hello[/unknown]");
1238
1239        // Known tags are also preserved as-is in debug mode
1240        let output = render_with_output(
1241            r#"[known]{{ message }}[/known]"#,
1242            &data,
1243            &theme,
1244            OutputMode::TermDebug,
1245        )
1246        .unwrap();
1247
1248        assert_eq!(output, "[known]hello[/known]");
1249    }
1250
1251    #[test]
1252    fn test_render_auto_json_mode() {
1253        use serde_json::json;
1254
1255        let theme = Theme::new();
1256        let data = json!({"name": "test", "count": 42});
1257
1258        let output = render_auto("unused template", &data, &theme, OutputMode::Json).unwrap();
1259
1260        assert!(output.contains("\"name\": \"test\""));
1261        assert!(output.contains("\"count\": 42"));
1262    }
1263
1264    #[test]
1265    fn test_render_auto_text_mode_uses_template() {
1266        use serde_json::json;
1267
1268        let theme = Theme::new();
1269        let data = json!({"name": "test"});
1270
1271        let output = render_auto("Name: {{ name }}", &data, &theme, OutputMode::Text).unwrap();
1272
1273        assert_eq!(output, "Name: test");
1274    }
1275
1276    #[test]
1277    fn test_render_auto_term_mode_uses_template() {
1278        use serde_json::json;
1279
1280        let theme = Theme::new().add("bold", Style::new().bold().force_styling(true));
1281        let data = json!({"name": "test"});
1282
1283        let output = render_auto(
1284            r#"[bold]{{ name }}[/bold]"#,
1285            &data,
1286            &theme,
1287            OutputMode::Term,
1288        )
1289        .unwrap();
1290
1291        assert!(output.contains("\x1b[1m"));
1292        assert!(output.contains("test"));
1293    }
1294
1295    #[test]
1296    fn test_render_auto_json_with_struct() {
1297        #[derive(Serialize)]
1298        struct Report {
1299            title: String,
1300            items: Vec<String>,
1301        }
1302
1303        let theme = Theme::new();
1304        let data = Report {
1305            title: "Summary".into(),
1306            items: vec!["one".into(), "two".into()],
1307        };
1308
1309        let output = render_auto("unused", &data, &theme, OutputMode::Json).unwrap();
1310
1311        assert!(output.contains("\"title\": \"Summary\""));
1312        assert!(output.contains("\"items\""));
1313        assert!(output.contains("\"one\""));
1314    }
1315
1316    #[test]
1317    fn test_render_with_alias() {
1318        let theme = Theme::new()
1319            .add("base", Style::new().bold())
1320            .add("alias", "base");
1321
1322        let output = render_with_output(
1323            r#"[alias]text[/alias]"#,
1324            &serde_json::json!({}),
1325            &theme,
1326            OutputMode::Text,
1327        )
1328        .unwrap();
1329
1330        assert_eq!(output, "text");
1331    }
1332
1333    #[test]
1334    fn test_render_with_alias_chain() {
1335        let theme = Theme::new()
1336            .add("muted", Style::new().dim())
1337            .add("disabled", "muted")
1338            .add("timestamp", "disabled");
1339
1340        let output = render_with_output(
1341            r#"[timestamp]12:00[/timestamp]"#,
1342            &serde_json::json!({}),
1343            &theme,
1344            OutputMode::Text,
1345        )
1346        .unwrap();
1347
1348        assert_eq!(output, "12:00");
1349    }
1350
1351    #[test]
1352    fn test_render_fails_with_dangling_alias() {
1353        let theme = Theme::new().add("orphan", "missing");
1354
1355        let result = render_with_output(
1356            r#"[orphan]text[/orphan]"#,
1357            &serde_json::json!({}),
1358            &theme,
1359            OutputMode::Text,
1360        );
1361
1362        assert!(result.is_err());
1363        let err = result.unwrap_err();
1364        assert!(err.to_string().contains("orphan"));
1365        assert!(err.to_string().contains("missing"));
1366    }
1367
1368    #[test]
1369    fn test_render_fails_with_cycle() {
1370        let theme = Theme::new().add("a", "b").add("b", "a");
1371
1372        let result = render_with_output(
1373            r#"[a]text[/a]"#,
1374            &serde_json::json!({}),
1375            &theme,
1376            OutputMode::Text,
1377        );
1378
1379        assert!(result.is_err());
1380        assert!(result.unwrap_err().to_string().contains("cycle"));
1381    }
1382
1383    #[test]
1384    fn test_three_layer_styling_pattern() {
1385        let theme = Theme::new()
1386            .add("dim_style", Style::new().dim())
1387            .add("cyan_bold", Style::new().cyan().bold())
1388            .add("yellow_bg", Style::new().on_yellow())
1389            .add("muted", "dim_style")
1390            .add("accent", "cyan_bold")
1391            .add("highlighted", "yellow_bg")
1392            .add("timestamp", "muted")
1393            .add("title", "accent")
1394            .add("selected_item", "highlighted");
1395
1396        assert!(theme.validate().is_ok());
1397
1398        let output = render_with_output(
1399            r#"[timestamp]{{ time }}[/timestamp] - [title]{{ name }}[/title]"#,
1400            &serde_json::json!({"time": "12:00", "name": "Report"}),
1401            &theme,
1402            OutputMode::Text,
1403        )
1404        .unwrap();
1405
1406        assert_eq!(output, "12:00 - Report");
1407    }
1408
1409    // ============================================================================
1410    // YAML/XML/CSV Output Tests
1411    // ============================================================================
1412
1413    #[test]
1414    fn test_render_auto_yaml_mode() {
1415        use serde_json::json;
1416
1417        let theme = Theme::new();
1418        let data = json!({"name": "test", "count": 42});
1419
1420        let output = render_auto("unused template", &data, &theme, OutputMode::Yaml).unwrap();
1421
1422        assert!(output.contains("name: test"));
1423        assert!(output.contains("count: 42"));
1424    }
1425
1426    #[test]
1427    fn test_render_auto_xml_mode() {
1428        let theme = Theme::new();
1429
1430        #[derive(Serialize)]
1431        #[serde(rename = "root")]
1432        struct Data {
1433            name: String,
1434            count: usize,
1435        }
1436
1437        let data = Data {
1438            name: "test".into(),
1439            count: 42,
1440        };
1441
1442        let output = render_auto("unused template", &data, &theme, OutputMode::Xml).unwrap();
1443
1444        assert!(output.contains("<root>"));
1445        assert!(output.contains("<name>test</name>"));
1446    }
1447
1448    #[test]
1449    fn test_render_auto_csv_mode_auto_flatten() {
1450        use serde_json::json;
1451
1452        let theme = Theme::new();
1453        let data = json!([
1454            {"name": "Alice", "stats": {"score": 10}},
1455            {"name": "Bob", "stats": {"score": 20}}
1456        ]);
1457
1458        let output = render_auto("unused", &data, &theme, OutputMode::Csv).unwrap();
1459
1460        assert!(output.contains("name,stats.score"));
1461        assert!(output.contains("Alice,10"));
1462        assert!(output.contains("Bob,20"));
1463    }
1464
1465    #[test]
1466    fn test_render_auto_csv_mode_with_spec() {
1467        let theme = Theme::new();
1468        let data = json!([
1469            {"name": "Alice", "meta": {"age": 30, "role": "admin"}},
1470            {"name": "Bob", "meta": {"age": 25, "role": "user"}}
1471        ]);
1472
1473        let spec = FlatDataSpec::builder()
1474            .column(Column::new(Width::Fixed(10)).key("name"))
1475            .column(
1476                Column::new(Width::Fixed(10))
1477                    .key("meta.role")
1478                    .header("Role"),
1479            )
1480            .build();
1481
1482        let output =
1483            render_auto_with_spec("unused", &data, &theme, OutputMode::Csv, Some(&spec)).unwrap();
1484
1485        let lines: Vec<&str> = output.lines().collect();
1486        assert_eq!(lines[0], "name,Role");
1487        assert!(lines.contains(&"Alice,admin"));
1488        assert!(lines.contains(&"Bob,user"));
1489        assert!(!output.contains("30"));
1490    }
1491
1492    // ============================================================================
1493    // Context Injection Tests
1494    // ============================================================================
1495
1496    #[test]
1497    fn test_render_with_context_basic() {
1498        use crate::context::{ContextRegistry, RenderContext};
1499
1500        #[derive(Serialize)]
1501        struct Data {
1502            name: String,
1503        }
1504
1505        let theme = Theme::new();
1506        let data = Data {
1507            name: "Alice".into(),
1508        };
1509        let json_data = serde_json::to_value(&data).unwrap();
1510
1511        let mut registry = ContextRegistry::new();
1512        registry.add_static("version", Value::from("1.0.0"));
1513
1514        let render_ctx = RenderContext::new(OutputMode::Text, Some(80), &theme, &json_data);
1515
1516        let output = render_with_context(
1517            "{{ name }} (v{{ version }})",
1518            &data,
1519            &theme,
1520            OutputMode::Text,
1521            &registry,
1522            &render_ctx,
1523            None,
1524        )
1525        .unwrap();
1526
1527        assert_eq!(output, "Alice (v1.0.0)");
1528    }
1529
1530    #[test]
1531    fn test_render_with_context_dynamic_provider() {
1532        use crate::context::{ContextRegistry, RenderContext};
1533
1534        #[derive(Serialize)]
1535        struct Data {
1536            message: String,
1537        }
1538
1539        let theme = Theme::new();
1540        let data = Data {
1541            message: "Hello".into(),
1542        };
1543        let json_data = serde_json::to_value(&data).unwrap();
1544
1545        let mut registry = ContextRegistry::new();
1546        registry.add_provider("terminal_width", |ctx: &RenderContext| {
1547            Value::from(ctx.terminal_width.unwrap_or(80))
1548        });
1549
1550        let render_ctx = RenderContext::new(OutputMode::Text, Some(120), &theme, &json_data);
1551
1552        let output = render_with_context(
1553            "{{ message }} (width={{ terminal_width }})",
1554            &data,
1555            &theme,
1556            OutputMode::Text,
1557            &registry,
1558            &render_ctx,
1559            None,
1560        )
1561        .unwrap();
1562
1563        assert_eq!(output, "Hello (width=120)");
1564    }
1565
1566    #[test]
1567    fn test_render_with_context_data_takes_precedence() {
1568        use crate::context::{ContextRegistry, RenderContext};
1569
1570        #[derive(Serialize)]
1571        struct Data {
1572            value: String,
1573        }
1574
1575        let theme = Theme::new();
1576        let data = Data {
1577            value: "from_data".into(),
1578        };
1579        let json_data = serde_json::to_value(&data).unwrap();
1580
1581        let mut registry = ContextRegistry::new();
1582        registry.add_static("value", Value::from("from_context"));
1583
1584        let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1585
1586        let output = render_with_context(
1587            "{{ value }}",
1588            &data,
1589            &theme,
1590            OutputMode::Text,
1591            &registry,
1592            &render_ctx,
1593            None,
1594        )
1595        .unwrap();
1596
1597        assert_eq!(output, "from_data");
1598    }
1599
1600    #[test]
1601    fn test_render_with_context_empty_registry() {
1602        use crate::context::{ContextRegistry, RenderContext};
1603
1604        #[derive(Serialize)]
1605        struct Data {
1606            name: String,
1607        }
1608
1609        let theme = Theme::new();
1610        let data = Data {
1611            name: "Test".into(),
1612        };
1613        let json_data = serde_json::to_value(&data).unwrap();
1614
1615        let registry = ContextRegistry::new();
1616        let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1617
1618        let output = render_with_context(
1619            "{{ name }}",
1620            &data,
1621            &theme,
1622            OutputMode::Text,
1623            &registry,
1624            &render_ctx,
1625            None,
1626        )
1627        .unwrap();
1628
1629        assert_eq!(output, "Test");
1630    }
1631
1632    #[test]
1633    fn test_render_auto_with_context_json_mode() {
1634        use crate::context::{ContextRegistry, RenderContext};
1635
1636        #[derive(Serialize)]
1637        struct Data {
1638            count: usize,
1639        }
1640
1641        let theme = Theme::new();
1642        let data = Data { count: 42 };
1643        let json_data = serde_json::to_value(&data).unwrap();
1644
1645        let mut registry = ContextRegistry::new();
1646        registry.add_static("extra", Value::from("ignored"));
1647
1648        let render_ctx = RenderContext::new(OutputMode::Json, None, &theme, &json_data);
1649
1650        let output = render_auto_with_context(
1651            "unused template {{ extra }}",
1652            &data,
1653            &theme,
1654            OutputMode::Json,
1655            &registry,
1656            &render_ctx,
1657            None,
1658        )
1659        .unwrap();
1660
1661        assert!(output.contains("\"count\": 42"));
1662        assert!(!output.contains("ignored"));
1663    }
1664
1665    #[test]
1666    fn test_render_auto_with_context_text_mode() {
1667        use crate::context::{ContextRegistry, RenderContext};
1668
1669        #[derive(Serialize)]
1670        struct Data {
1671            count: usize,
1672        }
1673
1674        let theme = Theme::new();
1675        let data = Data { count: 42 };
1676        let json_data = serde_json::to_value(&data).unwrap();
1677
1678        let mut registry = ContextRegistry::new();
1679        registry.add_static("label", Value::from("Items"));
1680
1681        let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1682
1683        let output = render_auto_with_context(
1684            "{{ label }}: {{ count }}",
1685            &data,
1686            &theme,
1687            OutputMode::Text,
1688            &registry,
1689            &render_ctx,
1690            None,
1691        )
1692        .unwrap();
1693
1694        assert_eq!(output, "Items: 42");
1695    }
1696
1697    #[test]
1698    fn test_render_with_context_provider_uses_output_mode() {
1699        use crate::context::{ContextRegistry, RenderContext};
1700
1701        #[derive(Serialize)]
1702        struct Data {}
1703
1704        let theme = Theme::new();
1705        let data = Data {};
1706        let json_data = serde_json::to_value(&data).unwrap();
1707
1708        let mut registry = ContextRegistry::new();
1709        registry.add_provider("mode", |ctx: &RenderContext| {
1710            Value::from(format!("{:?}", ctx.output_mode))
1711        });
1712
1713        let render_ctx = RenderContext::new(OutputMode::Term, None, &theme, &json_data);
1714
1715        let output = render_with_context(
1716            "Mode: {{ mode }}",
1717            &data,
1718            &theme,
1719            OutputMode::Term,
1720            &registry,
1721            &render_ctx,
1722            None,
1723        )
1724        .unwrap();
1725
1726        assert_eq!(output, "Mode: Term");
1727    }
1728
1729    #[test]
1730    fn test_render_with_context_nested_data() {
1731        use crate::context::{ContextRegistry, RenderContext};
1732
1733        #[derive(Serialize)]
1734        struct Item {
1735            name: String,
1736        }
1737
1738        #[derive(Serialize)]
1739        struct Data {
1740            items: Vec<Item>,
1741        }
1742
1743        let theme = Theme::new();
1744        let data = Data {
1745            items: vec![Item { name: "one".into() }, Item { name: "two".into() }],
1746        };
1747        let json_data = serde_json::to_value(&data).unwrap();
1748
1749        let mut registry = ContextRegistry::new();
1750        registry.add_static("prefix", Value::from("- "));
1751
1752        let render_ctx = RenderContext::new(OutputMode::Text, None, &theme, &json_data);
1753
1754        let output = render_with_context(
1755            "{% for item in items %}{{ prefix }}{{ item.name }}\n{% endfor %}",
1756            &data,
1757            &theme,
1758            OutputMode::Text,
1759            &registry,
1760            &render_ctx,
1761            None,
1762        )
1763        .unwrap();
1764
1765        assert_eq!(output, "- one\n- two\n");
1766    }
1767
1768    #[test]
1769    fn test_render_with_mode_forces_color_mode() {
1770        use console::Style;
1771
1772        #[derive(Serialize)]
1773        struct Data {
1774            status: String,
1775        }
1776
1777        // Create an adaptive theme with different colors for light/dark
1778        // Note: force_styling(true) is needed in tests since there's no TTY
1779        let theme = Theme::new().add_adaptive(
1780            "status",
1781            Style::new(),                                   // Base
1782            Some(Style::new().black().force_styling(true)), // Light mode
1783            Some(Style::new().white().force_styling(true)), // Dark mode
1784        );
1785
1786        let data = Data {
1787            status: "test".into(),
1788        };
1789
1790        // Force dark mode
1791        let dark_output = render_with_mode(
1792            r#"[status]{{ status }}[/status]"#,
1793            &data,
1794            &theme,
1795            OutputMode::Term,
1796            ColorMode::Dark,
1797        )
1798        .unwrap();
1799
1800        // Force light mode
1801        let light_output = render_with_mode(
1802            r#"[status]{{ status }}[/status]"#,
1803            &data,
1804            &theme,
1805            OutputMode::Term,
1806            ColorMode::Light,
1807        )
1808        .unwrap();
1809
1810        // They should be different (different colors applied)
1811        assert_ne!(dark_output, light_output);
1812
1813        // Dark mode should use white (ANSI 37)
1814        assert!(
1815            dark_output.contains("\x1b[37"),
1816            "Expected white (37) in dark mode"
1817        );
1818
1819        // Light mode should use black (ANSI 30)
1820        assert!(
1821            light_output.contains("\x1b[30"),
1822            "Expected black (30) in light mode"
1823        );
1824    }
1825
1826    // ============================================================================
1827    // BBParser Tag Syntax Tests
1828    // ============================================================================
1829
1830    #[test]
1831    fn test_tag_syntax_text_mode() {
1832        let theme = Theme::new().add("title", Style::new().bold());
1833
1834        #[derive(Serialize)]
1835        struct Data {
1836            name: String,
1837        }
1838
1839        let output = render_with_output(
1840            "[title]{{ name }}[/title]",
1841            &Data {
1842                name: "Hello".into(),
1843            },
1844            &theme,
1845            OutputMode::Text,
1846        )
1847        .unwrap();
1848
1849        // Tags should be stripped in text mode
1850        assert_eq!(output, "Hello");
1851    }
1852
1853    #[test]
1854    fn test_tag_syntax_term_mode() {
1855        let theme = Theme::new().add("bold", Style::new().bold().force_styling(true));
1856
1857        #[derive(Serialize)]
1858        struct Data {
1859            name: String,
1860        }
1861
1862        let output = render_with_output(
1863            "[bold]{{ name }}[/bold]",
1864            &Data {
1865                name: "Hello".into(),
1866            },
1867            &theme,
1868            OutputMode::Term,
1869        )
1870        .unwrap();
1871
1872        // Should contain ANSI bold codes
1873        assert!(output.contains("\x1b[1m"));
1874        assert!(output.contains("Hello"));
1875    }
1876
1877    #[test]
1878    fn test_tag_syntax_debug_mode() {
1879        let theme = Theme::new().add("title", Style::new().bold());
1880
1881        #[derive(Serialize)]
1882        struct Data {
1883            name: String,
1884        }
1885
1886        let output = render_with_output(
1887            "[title]{{ name }}[/title]",
1888            &Data {
1889                name: "Hello".into(),
1890            },
1891            &theme,
1892            OutputMode::TermDebug,
1893        )
1894        .unwrap();
1895
1896        // Tags should be preserved in debug mode
1897        assert_eq!(output, "[title]Hello[/title]");
1898    }
1899
1900    #[test]
1901    fn test_tag_syntax_unknown_tag_passthrough() {
1902        // Passthrough with ? marker only applies in Apply mode (Term)
1903        let theme = Theme::new().add("known", Style::new().bold());
1904
1905        #[derive(Serialize)]
1906        struct Data {
1907            name: String,
1908        }
1909
1910        // In Term mode, unknown tags get ? marker
1911        let output = render_with_output(
1912            "[unknown]{{ name }}[/unknown]",
1913            &Data {
1914                name: "Hello".into(),
1915            },
1916            &theme,
1917            OutputMode::Term,
1918        )
1919        .unwrap();
1920
1921        // Unknown tags get ? marker in passthrough mode
1922        assert!(output.contains("[unknown?]"));
1923        assert!(output.contains("[/unknown?]"));
1924        assert!(output.contains("Hello"));
1925
1926        // In Text mode, all tags are stripped (Remove transform)
1927        let text_output = render_with_output(
1928            "[unknown]{{ name }}[/unknown]",
1929            &Data {
1930                name: "Hello".into(),
1931            },
1932            &theme,
1933            OutputMode::Text,
1934        )
1935        .unwrap();
1936
1937        // Text mode strips all tags
1938        assert_eq!(text_output, "Hello");
1939    }
1940
1941    #[test]
1942    fn test_tag_syntax_nested() {
1943        let theme = Theme::new()
1944            .add("bold", Style::new().bold().force_styling(true))
1945            .add("red", Style::new().red().force_styling(true));
1946
1947        #[derive(Serialize)]
1948        struct Data {
1949            word: String,
1950        }
1951
1952        let output = render_with_output(
1953            "[bold][red]{{ word }}[/red][/bold]",
1954            &Data {
1955                word: "test".into(),
1956            },
1957            &theme,
1958            OutputMode::Term,
1959        )
1960        .unwrap();
1961
1962        // Should contain both bold and red ANSI codes
1963        assert!(output.contains("\x1b[1m")); // Bold
1964        assert!(output.contains("\x1b[31m")); // Red
1965        assert!(output.contains("test"));
1966    }
1967
1968    #[test]
1969    fn test_tag_syntax_multiple_styles() {
1970        let theme = Theme::new()
1971            .add("title", Style::new().bold())
1972            .add("count", Style::new().cyan());
1973
1974        #[derive(Serialize)]
1975        struct Data {
1976            name: String,
1977            num: usize,
1978        }
1979
1980        let output = render_with_output(
1981            r#"[title]{{ name }}[/title]: [count]{{ num }}[/count]"#,
1982            &Data {
1983                name: "Items".into(),
1984                num: 42,
1985            },
1986            &theme,
1987            OutputMode::Text,
1988        )
1989        .unwrap();
1990
1991        assert_eq!(output, "Items: 42");
1992    }
1993
1994    #[test]
1995    fn test_tag_syntax_in_loop() {
1996        let theme = Theme::new().add("item", Style::new().cyan());
1997
1998        #[derive(Serialize)]
1999        struct Data {
2000            items: Vec<String>,
2001        }
2002
2003        let output = render_with_output(
2004            "{% for item in items %}[item]{{ item }}[/item]\n{% endfor %}",
2005            &Data {
2006                items: vec!["one".into(), "two".into()],
2007            },
2008            &theme,
2009            OutputMode::Text,
2010        )
2011        .unwrap();
2012
2013        assert_eq!(output, "one\ntwo\n");
2014    }
2015
2016    #[test]
2017    fn test_tag_syntax_literal_brackets() {
2018        // Tags that don't match our pattern should pass through
2019        let theme = Theme::new();
2020
2021        #[derive(Serialize)]
2022        struct Data {
2023            msg: String,
2024        }
2025
2026        let output = render_with_output(
2027            "Array: [1, 2, 3] and {{ msg }}",
2028            &Data { msg: "done".into() },
2029            &theme,
2030            OutputMode::Text,
2031        )
2032        .unwrap();
2033
2034        // Non-tag brackets preserved
2035        assert_eq!(output, "Array: [1, 2, 3] and done");
2036    }
2037
2038    // ============================================================================
2039    // Template Validation Tests
2040    // ============================================================================
2041
2042    #[test]
2043    fn test_validate_template_all_known_tags() {
2044        let theme = Theme::new()
2045            .add("title", Style::new().bold())
2046            .add("count", Style::new().cyan());
2047
2048        #[derive(Serialize)]
2049        struct Data {
2050            name: String,
2051        }
2052
2053        let result = validate_template(
2054            "[title]{{ name }}[/title]",
2055            &Data {
2056                name: "Hello".into(),
2057            },
2058            &theme,
2059        );
2060
2061        assert!(result.is_ok());
2062    }
2063
2064    #[test]
2065    fn test_validate_template_unknown_tag_fails() {
2066        let theme = Theme::new().add("known", Style::new().bold());
2067
2068        #[derive(Serialize)]
2069        struct Data {
2070            name: String,
2071        }
2072
2073        let result = validate_template(
2074            "[unknown]{{ name }}[/unknown]",
2075            &Data {
2076                name: "Hello".into(),
2077            },
2078            &theme,
2079        );
2080
2081        assert!(result.is_err());
2082        let err = result.unwrap_err();
2083        let errors = err
2084            .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2085            .expect("Expected UnknownTagErrors");
2086        assert_eq!(errors.len(), 2); // open and close tags
2087    }
2088
2089    #[test]
2090    fn test_validate_template_multiple_unknown_tags() {
2091        let theme = Theme::new().add("known", Style::new().bold());
2092
2093        #[derive(Serialize)]
2094        struct Data {
2095            a: String,
2096            b: String,
2097        }
2098
2099        let result = validate_template(
2100            "[foo]{{ a }}[/foo] and [bar]{{ b }}[/bar]",
2101            &Data {
2102                a: "x".into(),
2103                b: "y".into(),
2104            },
2105            &theme,
2106        );
2107
2108        assert!(result.is_err());
2109        let err = result.unwrap_err();
2110        let errors = err
2111            .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2112            .expect("Expected UnknownTagErrors");
2113        assert_eq!(errors.len(), 4); // foo open/close + bar open/close
2114    }
2115
2116    #[test]
2117    fn test_validate_template_plain_text_passes() {
2118        let theme = Theme::new();
2119
2120        #[derive(Serialize)]
2121        struct Data {
2122            msg: String,
2123        }
2124
2125        let result = validate_template("Just plain {{ msg }}", &Data { msg: "hi".into() }, &theme);
2126
2127        assert!(result.is_ok());
2128    }
2129
2130    #[test]
2131    fn test_validate_template_mixed_known_and_unknown() {
2132        let theme = Theme::new().add("known", Style::new().bold());
2133
2134        #[derive(Serialize)]
2135        struct Data {
2136            a: String,
2137            b: String,
2138        }
2139
2140        let result = validate_template(
2141            "[known]{{ a }}[/known] [unknown]{{ b }}[/unknown]",
2142            &Data {
2143                a: "x".into(),
2144                b: "y".into(),
2145            },
2146            &theme,
2147        );
2148
2149        assert!(result.is_err());
2150        let err = result.unwrap_err();
2151        let errors = err
2152            .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2153            .expect("Expected UnknownTagErrors");
2154        // Only unknown tags should be reported
2155        assert_eq!(errors.len(), 2);
2156        assert!(errors.errors.iter().any(|e| e.tag == "unknown"));
2157    }
2158
2159    #[test]
2160    fn test_validate_template_syntax_error_fails() {
2161        let theme = Theme::new();
2162        #[derive(Serialize)]
2163        struct Data {}
2164
2165        // Missing closing braces
2166        let result = validate_template("{{ unclosed", &Data {}, &theme);
2167        assert!(result.is_err());
2168
2169        let err = result.unwrap_err();
2170        // Should NOT be UnknownTagErrors
2171        assert!(err
2172            .downcast_ref::<standout_bbparser::UnknownTagErrors>()
2173            .is_none());
2174        // Should be a minijinja error
2175        let msg = err.to_string();
2176        assert!(
2177            msg.contains("syntax error") || msg.contains("unexpected"),
2178            "Got: {}",
2179            msg
2180        );
2181    }
2182
2183    #[test]
2184    fn test_render_auto_with_context_yaml_mode() {
2185        use crate::context::{ContextRegistry, RenderContext};
2186        use serde_json::json;
2187
2188        let theme = Theme::new();
2189        let data = json!({"name": "test", "count": 42});
2190
2191        // Setup context registry (though strictly not used for structured output)
2192        let registry = ContextRegistry::new();
2193        let render_ctx = RenderContext::new(OutputMode::Yaml, Some(80), &theme, &data);
2194
2195        // This call previously panicked
2196        let output = render_auto_with_context(
2197            "unused template",
2198            &data,
2199            &theme,
2200            OutputMode::Yaml,
2201            &registry,
2202            &render_ctx,
2203            None,
2204        )
2205        .unwrap();
2206
2207        assert!(output.contains("name: test"));
2208        assert!(output.contains("count: 42"));
2209    }
2210}