Skip to main content

shape_runtime/renderers/
mod.rs

1//! Content renderers for various output targets.
2//!
3//! Each renderer implements `ContentRenderer` to produce formatted output
4//! from a `ContentNode` tree.
5
6pub mod html;
7pub mod json;
8pub mod markdown;
9pub mod plain;
10pub mod terminal;
11pub mod terminal_chart;
12
13#[cfg(test)]
14mod cross_renderer_tests {
15    use crate::content_renderer::ContentRenderer;
16    use crate::renderers::{
17        html::HtmlRenderer, json::JsonRenderer, markdown::MarkdownRenderer, plain::PlainRenderer,
18        terminal::TerminalRenderer,
19    };
20    use shape_value::content::*;
21
22    /// Build a representative ContentNode tree for cross-renderer testing.
23    fn sample_tree() -> ContentNode {
24        ContentNode::Fragment(vec![
25            ContentNode::plain("Hello ")
26                .with_bold()
27                .with_fg(Color::Named(NamedColor::Red)),
28            ContentNode::plain("world").with_italic(),
29            ContentNode::Table(ContentTable {
30                headers: vec!["Name".into(), "Value".into()],
31                rows: vec![
32                    vec![ContentNode::plain("alpha"), ContentNode::plain("1")],
33                    vec![ContentNode::plain("beta"), ContentNode::plain("2")],
34                ],
35                border: BorderStyle::Rounded,
36                max_rows: None,
37                column_types: Some(vec!["string".into(), "number".into()]),
38                total_rows: None,
39                sortable: true,
40            }),
41            ContentNode::Code {
42                language: Some("rust".into()),
43                source: "fn main() {}".into(),
44            },
45        ])
46    }
47
48    #[test]
49    fn terminal_contains_ansi_codes() {
50        let tree = sample_tree();
51        let output = TerminalRenderer::new().render(&tree);
52        assert!(
53            output.contains("\x1b["),
54            "terminal output should contain ANSI codes"
55        );
56        assert!(output.contains("Hello"));
57        assert!(output.contains("world"));
58        assert!(output.contains("alpha"));
59    }
60
61    #[test]
62    fn html_contains_tags() {
63        let tree = sample_tree();
64        let output = HtmlRenderer::new().render(&tree);
65        assert!(output.contains("<span"), "HTML should contain <span> tags");
66        assert!(output.contains("<table>"), "HTML should contain <table>");
67        assert!(
68            output.contains("<pre><code"),
69            "HTML should contain <pre><code>"
70        );
71        assert!(output.contains("Hello"));
72    }
73
74    #[test]
75    fn plain_has_no_escape_codes() {
76        let tree = sample_tree();
77        let output = PlainRenderer.render(&tree);
78        assert!(
79            !output.contains("\x1b["),
80            "plain output should not contain ANSI codes"
81        );
82        assert!(
83            !output.contains("<span"),
84            "plain output should not contain HTML tags"
85        );
86        assert!(output.contains("Hello"));
87        assert!(output.contains("alpha"));
88    }
89
90    #[test]
91    fn markdown_uses_gfm_tables() {
92        let tree = sample_tree();
93        let output = MarkdownRenderer.render(&tree);
94        assert!(
95            output.contains("| Name"),
96            "markdown should use pipe table syntax"
97        );
98        assert!(
99            output.contains("```rust"),
100            "markdown should use fenced code blocks"
101        );
102        assert!(output.contains("Hello"));
103    }
104
105    #[test]
106    fn json_is_valid_json() {
107        let tree = sample_tree();
108        let output = JsonRenderer.render(&tree);
109        let parsed: serde_json::Value =
110            serde_json::from_str(&output).expect("JSON renderer output should be valid JSON");
111        assert!(parsed.get("type").is_some(), "JSON should have 'type' key");
112    }
113
114    #[test]
115    fn all_renderers_agree_on_text_content() {
116        let node = ContentNode::plain("consistent");
117        let terminal = TerminalRenderer::new().render(&node);
118        let html = HtmlRenderer::new().render(&node);
119        let plain = PlainRenderer.render(&node);
120        let markdown = MarkdownRenderer.render(&node);
121
122        // All should contain the plain text (terminal/html may have extra formatting)
123        assert!(terminal.contains("consistent"));
124        assert!(html.contains("consistent"));
125        assert!(plain.contains("consistent"));
126        assert!(markdown.contains("consistent"));
127    }
128}