Skip to main content

ralph_workflow/rendering/xml/
mod.rs

1//! Semantic XML renderers for user-friendly output.
2//!
3//! This module routes XML rendering to type-specific modules.
4//! Each XML output type (`DevelopmentResult`, `DevelopmentPlan`, etc.) has
5//! a dedicated renderer that transforms raw XML into user-friendly
6//! terminal output.
7//!
8//! # Graceful Degradation
9//!
10//! If XML parsing fails, renderers fall back to displaying the raw XML
11//! with a warning message. This ensures users always see output even if
12//! the format is unexpected.
13
14mod commit_message;
15mod development_plan;
16mod development_result;
17mod fix_result;
18mod helpers;
19mod review_issues;
20
21use crate::files::llm_output_extraction::SkillsMcp;
22use crate::reducer::ui_event::{XmlOutputContext, XmlOutputType};
23
24/// Render skills-mcp recommendations in markdown format.
25///
26/// Renders structured skills and MCPs with optional reasons, or raw content if no structured data.
27pub fn render_skills_mcp_markdown(output: &mut String, skills_mcp: Option<&SkillsMcp>) {
28    use std::fmt::Write as _;
29
30    if let Some(sm) = skills_mcp {
31        let has_structured = !sm.skills.is_empty() || !sm.mcps.is_empty();
32        if has_structured || sm.raw_content.is_some() {
33            output.push_str("  - Skills & MCP:\n");
34            for skill in &sm.skills {
35                if let Some(ref reason) = skill.reason {
36                    writeln!(output, "    - skill: {} \u{2014} {}", skill.name, reason).unwrap();
37                } else {
38                    writeln!(output, "    - skill: {}", skill.name).unwrap();
39                }
40            }
41            for mcp in &sm.mcps {
42                if let Some(ref reason) = mcp.reason {
43                    writeln!(output, "    - mcp: {} \u{2014} {}", mcp.name, reason).unwrap();
44                } else {
45                    writeln!(output, "    - mcp: {}", mcp.name).unwrap();
46                }
47            }
48            if let Some(ref raw) = sm.raw_content {
49                let trimmed: &str = raw.trim();
50                if !trimmed.is_empty() && !has_structured {
51                    writeln!(output, "    - {trimmed}").unwrap();
52                }
53            }
54        }
55    }
56}
57
58/// Render XML content based on its type.
59///
60/// Returns formatted string for terminal display.
61/// Falls back to raw XML with warning if parsing fails.
62#[must_use]
63pub fn render_xml(
64    xml_type: &XmlOutputType,
65    content: &str,
66    output_context: &Option<XmlOutputContext>,
67) -> String {
68    match xml_type {
69        XmlOutputType::DevelopmentResult => {
70            development_result::render(content, output_context.as_ref())
71        }
72        XmlOutputType::DevelopmentPlan => development_plan::render(content),
73        XmlOutputType::ReviewIssues => review_issues::render(content, output_context.as_ref()),
74        XmlOutputType::FixResult => fix_result::render(content, output_context.as_ref()),
75        XmlOutputType::CommitMessage => commit_message::render(content),
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_render_xml_routes_to_development_result() {
85        let content = r"<ralph-development-result>
86<ralph-status>completed</ralph-status>
87<ralph-summary>Done</ralph-summary>
88</ralph-development-result>";
89
90        let output = render_xml(&XmlOutputType::DevelopmentResult, content, &None);
91        assert!(
92            output.contains("✅"),
93            "Should route to development result renderer"
94        );
95    }
96
97    #[test]
98    fn test_render_xml_routes_to_review_issues() {
99        let content = r"<ralph-issues>
100<ralph-issue>Test issue</ralph-issue>
101</ralph-issues>";
102
103        let output = render_xml(&XmlOutputType::ReviewIssues, content, &None);
104        assert!(
105            output.contains("1 issue"),
106            "Should route to issues renderer"
107        );
108    }
109
110    #[test]
111    fn test_render_xml_routes_to_commit_message() {
112        let content = r"<ralph-commit>
113<ralph-subject>feat: add feature</ralph-subject>
114</ralph-commit>";
115
116        let output = render_xml(&XmlOutputType::CommitMessage, content, &None);
117        assert!(
118            output.contains("feat: add feature"),
119            "Should route to commit renderer"
120        );
121    }
122}