Skip to main content

sbom_diff/renderer/
markdown.rs

1use super::{
2    kind_suffix, write_full, write_summary, FieldChangeFormatter, FullFormatter, RenderOptions,
3    Renderer, SectionKind, SummaryFormatter, SummaryRenderer,
4};
5use crate::{Diff, EcosystemCounts, EdgeDiff};
6use sbom_model::Component;
7use std::collections::BTreeMap;
8use std::io::Write;
9
10/// GitHub-flavored markdown renderer for PR comments.
11///
12/// produces collapsible sections using `<details>` tags.
13pub struct MarkdownRenderer;
14
15impl FieldChangeFormatter for MarkdownRenderer {
16    fn field_change<W: Write>(
17        &self,
18        w: &mut W,
19        name: &str,
20        old: &str,
21        new: &str,
22    ) -> std::io::Result<()> {
23        writeln!(w, "- **{}**: `{}` &rarr; `{}`", name, old, new)
24    }
25
26    fn hash_header<W: Write>(&self, w: &mut W, downgrade: bool) -> std::io::Result<()> {
27        if downgrade {
28            writeln!(w, "- **Hashes (algorithm downgrade)**:")
29        } else {
30            writeln!(w, "- **Hashes**:")
31        }
32    }
33
34    fn hash_removed<W: Write>(&self, w: &mut W, algo: &str, digest: &str) -> std::io::Result<()> {
35        writeln!(w, "  - `{}`: removed `{}`", algo, digest)
36    }
37
38    fn hash_changed<W: Write>(
39        &self,
40        w: &mut W,
41        algo: &str,
42        old: &str,
43        new: &str,
44    ) -> std::io::Result<()> {
45        writeln!(w, "  - `{}`: `{}` &rarr; `{}`", algo, old, new)
46    }
47
48    fn hash_added<W: Write>(&self, w: &mut W, algo: &str, digest: &str) -> std::io::Result<()> {
49        writeln!(w, "  - `{}`: added `{}`", algo, digest)
50    }
51
52    fn component_header<W: Write>(&self, w: &mut W, id: &str) -> std::io::Result<()> {
53        writeln!(w, "#### `{}`", id)
54    }
55}
56
57impl FullFormatter for MarkdownRenderer {
58    fn full_warnings<W: Write>(&self, w: &mut W, opts: &RenderOptions) -> std::io::Result<()> {
59        writeln!(
60            w,
61            "<details><summary><b>Warnings ({})</b></summary>",
62            opts.warning_count()
63        )?;
64        writeln!(w)?;
65        for warning in &opts.old_warnings {
66            writeln!(w, "- **old:** {}", warning)?;
67        }
68        for warning in &opts.new_warnings {
69            writeln!(w, "- **new:** {}", warning)?;
70        }
71        writeln!(w, "</details>")?;
72        writeln!(w)
73    }
74
75    fn full_count_header<W: Write>(&self, w: &mut W, diff: &Diff) -> std::io::Result<()> {
76        self.write_counts(w, diff)?;
77        writeln!(w)
78    }
79
80    fn full_ecosystem_breakdown<W: Write>(
81        &self,
82        w: &mut W,
83        breakdown: &BTreeMap<String, EcosystemCounts>,
84    ) -> std::io::Result<()> {
85        writeln!(w, "#### By Ecosystem")?;
86        writeln!(w)?;
87        writeln!(w, "| Ecosystem | Added | Removed | Changed |")?;
88        writeln!(w, "| --- | --- | --- | --- |")?;
89        for (eco, counts) in breakdown {
90            writeln!(
91                w,
92                "| {} | {} | {} | {} |",
93                eco, counts.added, counts.removed, counts.changed
94            )?;
95        }
96        writeln!(w)
97    }
98
99    fn full_ecosystem_header<W: Write>(&self, w: &mut W, ecosystem: &str) -> std::io::Result<()> {
100        writeln!(w, "#### {}", ecosystem)?;
101        writeln!(w)
102    }
103
104    fn section_open<W: Write>(
105        &self,
106        w: &mut W,
107        kind: SectionKind,
108        count: usize,
109    ) -> std::io::Result<()> {
110        let label = match kind {
111            SectionKind::Added => "Added",
112            SectionKind::Removed => "Removed",
113            SectionKind::Changed => "Changed",
114        };
115        writeln!(
116            w,
117            "<details><summary><b>{} ({})</b></summary>",
118            label, count
119        )?;
120        writeln!(w)
121    }
122
123    fn section_close<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
124        writeln!(w, "</details>")?;
125        writeln!(w)
126    }
127
128    fn component_list<W: Write>(&self, w: &mut W, components: &[Component]) -> std::io::Result<()> {
129        for c in components {
130            writeln!(w, "- `{}`", c.purl.as_deref().unwrap_or(c.id.as_str()))?;
131        }
132        Ok(())
133    }
134
135    fn edge_open<W: Write>(&self, w: &mut W, count: usize) -> std::io::Result<()> {
136        writeln!(
137            w,
138            "<details><summary><b>Edge Changes ({})</b></summary>",
139            count
140        )?;
141        writeln!(w)
142    }
143
144    fn edge_entry<W: Write>(&self, w: &mut W, diff: &Diff, edge: &EdgeDiff) -> std::io::Result<()> {
145        writeln!(w, "#### `{}`", diff.display_name(&edge.parent))?;
146        if !edge.removed.is_empty() {
147            writeln!(w, "**Removed dependencies:**")?;
148            for (removed, kind) in &edge.removed {
149                writeln!(w, "- `{}`{}", diff.display_name(removed), kind_suffix(kind))?;
150            }
151        }
152        if !edge.added.is_empty() {
153            writeln!(w, "**Added dependencies:**")?;
154            for (added, kind) in &edge.added {
155                writeln!(w, "- `{}`{}", diff.display_name(added), kind_suffix(kind))?;
156            }
157        }
158        if !edge.kind_changed.is_empty() {
159            writeln!(w, "**Kind changed:**")?;
160            for (changed, (old_kind, new_kind)) in &edge.kind_changed {
161                writeln!(
162                    w,
163                    "- `{}`: {} &rarr; {}",
164                    diff.display_name(changed),
165                    old_kind,
166                    new_kind
167                )?;
168            }
169        }
170        writeln!(w)
171    }
172
173    fn edge_close<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
174        writeln!(w, "</details>")
175    }
176
177    fn metadata_open<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
178        writeln!(w, "<details><summary><b>Metadata Changes</b></summary>")?;
179        writeln!(w)
180    }
181
182    fn metadata_close<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
183        writeln!(w, "</details>")
184    }
185}
186
187impl Renderer for MarkdownRenderer {
188    fn render<W: Write>(
189        &self,
190        diff: &Diff,
191        opts: &RenderOptions,
192        writer: &mut W,
193    ) -> anyhow::Result<()> {
194        write_full(self, diff, opts, writer)?;
195        Ok(())
196    }
197}
198
199impl SummaryFormatter for MarkdownRenderer {
200    fn write_warnings<W: Write>(&self, w: &mut W, opts: &RenderOptions) -> std::io::Result<()> {
201        writeln!(
202            w,
203            "<details><summary><b>Warnings ({})</b></summary>",
204            opts.warning_count()
205        )?;
206        writeln!(w)?;
207        for warning in &opts.old_warnings {
208            writeln!(w, "- **old:** {}", warning)?;
209        }
210        for warning in &opts.new_warnings {
211            writeln!(w, "- **new:** {}", warning)?;
212        }
213        writeln!(w, "</details>")?;
214        writeln!(w)
215    }
216
217    fn write_counts<W: Write>(&self, w: &mut W, diff: &Diff) -> std::io::Result<()> {
218        writeln!(w, "### SBOM Diff Summary")?;
219        writeln!(w)?;
220        writeln!(w, "| Metric | Count |")?;
221        writeln!(w, "| --- | --- |")?;
222        writeln!(w, "| Old total | {} |", diff.old_total)?;
223        writeln!(w, "| New total | {} |", diff.new_total)?;
224        writeln!(w, "| Unchanged | {} |", diff.unchanged)?;
225        writeln!(w, "| Added | {} |", diff.added.len())?;
226        writeln!(w, "| Removed | {} |", diff.removed.len())?;
227        writeln!(w, "| Changed | {} |", diff.changed.len())?;
228        writeln!(w, "| Edge changes | {} |", diff.edge_diffs.len())?;
229        writeln!(
230            w,
231            "| Metadata changed | {} |",
232            if diff.metadata_changed.is_some() {
233                "yes"
234            } else {
235                "no"
236            }
237        )
238    }
239
240    fn write_ecosystem_breakdown<W: Write>(
241        &self,
242        w: &mut W,
243        breakdown: &BTreeMap<String, EcosystemCounts>,
244    ) -> std::io::Result<()> {
245        writeln!(w)?;
246        writeln!(w, "#### By Ecosystem")?;
247        writeln!(w)?;
248        writeln!(w, "| Ecosystem | Added | Removed | Changed |")?;
249        writeln!(w, "| --- | --- | --- | --- |")?;
250        for (eco, counts) in breakdown {
251            writeln!(
252                w,
253                "| {} | {} | {} | {} |",
254                eco, counts.added, counts.removed, counts.changed
255            )?;
256        }
257        Ok(())
258    }
259}
260
261impl SummaryRenderer for MarkdownRenderer {
262    fn render_summary<W: Write>(
263        &self,
264        diff: &Diff,
265        opts: &RenderOptions,
266        writer: &mut W,
267    ) -> anyhow::Result<()> {
268        write_summary(self, diff, opts, writer)?;
269        Ok(())
270    }
271}