Skip to main content

karpal_diagram/
render.rs

1use alloc::{format, string::String};
2
3use crate::diagram::{Diagram, DiagramKind, NormalizationRule, NormalizationTrace};
4
5pub struct TextRenderer;
6pub struct SvgRenderer;
7
8impl TextRenderer {
9    pub fn render(diagram: &Diagram) -> String {
10        let mut out = String::new();
11        out.push_str(&format!(
12            "diagram {} -> {}\n",
13            diagram.input_arity, diagram.output_arity
14        ));
15        for (index, stage) in diagram.sequence_chain().iter().enumerate() {
16            out.push_str(&format!("{index}: {}\n", Self::stage(stage)));
17        }
18        out
19    }
20
21    pub fn render_trace(trace: &NormalizationTrace) -> String {
22        let mut out = String::new();
23        out.push_str("normalization trace\n");
24        out.push_str("rules:\n");
25        if trace.rules.is_empty() {
26            out.push_str("- none\n");
27        } else {
28            for rule in &trace.rules {
29                out.push_str(&format!("- {}\n", Self::rule_name(*rule)));
30            }
31        }
32        out.push_str("normalized:\n");
33        out.push_str(&Self::render(&trace.normalized));
34        out
35    }
36
37    fn stage(diagram: &Diagram) -> String {
38        match &diagram.kind {
39            DiagramKind::Identity => format!("id[{}]", diagram.input_arity),
40            DiagramKind::Box { label } => format!(
41                "box({label}) {} -> {}",
42                diagram.input_arity, diagram.output_arity
43            ),
44            DiagramKind::Parallel(left, right) => {
45                format!("parallel({}, {})", Self::stage(left), Self::stage(right))
46            }
47            DiagramKind::Swap { left, right } => format!("swap[{left}|{right}]"),
48            DiagramKind::Cup { arity } => format!("cup[{arity}]"),
49            DiagramKind::Cap { arity } => format!("cap[{arity}]"),
50            DiagramKind::Sequence(_, _) => {
51                unreachable!("sequence chains are flattened before stage rendering")
52            }
53        }
54    }
55
56    fn rule_name(rule: NormalizationRule) -> &'static str {
57        match rule {
58            NormalizationRule::FlattenSequence => "flatten-sequence",
59            NormalizationRule::FlattenParallel => "flatten-parallel",
60            NormalizationRule::ElideIdentitySequenceStage => "elide-identity-sequence-stage",
61            NormalizationRule::CollapseIdentityParallel => "collapse-identity-parallel",
62            NormalizationRule::CancelAdjacentSwaps => "cancel-adjacent-swaps",
63            NormalizationRule::YankCupCap => "yank-cup-cap",
64        }
65    }
66}
67
68impl SvgRenderer {
69    pub fn render(diagram: &Diagram) -> String {
70        let stages = diagram.sequence_chain();
71        let width = 180 * stages.len().max(1);
72        let height = 80 + 40 * diagram.input_arity.max(diagram.output_arity).max(1);
73        let mut out =
74            format!("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\">");
75        out.push_str("<style>text{font-family:monospace;font-size:12px}rect{fill:#1f2937;stroke:#94a3b8}line{stroke:#64748b;stroke-width:2}</style>");
76
77        for wire in 0..diagram.input_arity.max(1) {
78            let y = 40 + wire * 30;
79            out.push_str(&format!(
80                "<line x1=\"10\" y1=\"{y}\" x2=\"{}\" y2=\"{y}\" />",
81                width - 10
82            ));
83        }
84
85        for (index, stage) in stages.iter().enumerate() {
86            let x = 30 + index * 160;
87            out.push_str(&format!(
88                "<rect x=\"{x}\" y=\"20\" width=\"120\" height=\"40\" rx=\"6\" />"
89            ));
90            out.push_str(&format!(
91                "<text x=\"{}\" y=\"45\">{}</text>",
92                x + 10,
93                TextRenderer::stage(stage)
94            ));
95        }
96
97        out.push_str("</svg>");
98        out
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::diagram::Diagram;
106
107    #[test]
108    fn text_renderer_includes_swap_and_boxes() {
109        let diagram = Diagram::box_("double", 1, 1)
110            .parallel(Diagram::box_("increment", 1, 1))
111            .then(Diagram::swap(1, 1));
112        let rendered = TextRenderer::render(&diagram);
113        assert!(rendered.contains("parallel"));
114        assert!(rendered.contains("swap[1|1]"));
115    }
116
117    #[test]
118    fn svg_renderer_emits_svg_tag() {
119        let diagram = Diagram::box_("double", 1, 1);
120        let rendered = SvgRenderer::render(&diagram);
121        assert!(rendered.starts_with("<svg"));
122        assert!(rendered.contains("box(double)"));
123    }
124
125    #[test]
126    fn text_renderer_trace_lists_applied_rules() {
127        let trace = Diagram::identity(2)
128            .then(Diagram::swap(1, 1))
129            .then(Diagram::swap(1, 1))
130            .normalize_with_trace();
131
132        let rendered = TextRenderer::render_trace(&trace);
133        assert!(rendered.contains("normalization trace"));
134        assert!(rendered.contains("flatten-sequence"));
135        assert!(rendered.contains("cancel-adjacent-swaps"));
136        assert!(rendered.contains("diagram 2 -> 2"));
137    }
138
139    #[test]
140    fn text_renderer_includes_cups_and_caps() {
141        let diagram = Diagram::cup(1)
142            .parallel(Diagram::identity(1))
143            .then(Diagram::identity(1).parallel(Diagram::cap(1)));
144
145        let rendered = TextRenderer::render(&diagram);
146        assert!(rendered.contains("cup[1]"));
147        assert!(rendered.contains("cap[1]"));
148    }
149
150    #[test]
151    fn text_renderer_trace_lists_yanking_rule() {
152        let trace = Diagram::cup(1)
153            .parallel(Diagram::identity(1))
154            .then(Diagram::identity(1).parallel(Diagram::cap(1)))
155            .normalize_with_trace();
156
157        let rendered = TextRenderer::render_trace(&trace);
158        assert!(rendered.contains("yank-cup-cap"));
159    }
160}