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