Skip to main content

karpal_diagram/
render.rs

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