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
10pub 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, "- **{}**: `{}` → `{}`", 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, " - `{}`: `{}` → `{}`", 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 "- `{}`: {} → {}",
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}