seer_core/output/markdown/
mod.rs1use std::fmt::{self, Write as _};
2
3use super::OutputFormatter;
4
5pub(super) use super::grouping::render_grouped;
7pub(super) use crate::caa::{CaaPolicy, IssuerCaaMatch};
8pub(super) use crate::dns::{DnsRecord, FollowIteration, FollowResult, PropagationResult};
9pub(super) use crate::lookup::LookupResult;
10pub(super) use crate::rdap::RdapResponse;
11pub(super) use crate::status::StatusResponse;
12pub(super) use crate::whois::WhoisResponse;
13
14mod diff;
15mod dns;
16mod domain_info;
17mod lookup;
18mod propagation;
19mod rdap;
20mod status;
21mod whois;
22
23pub(super) struct MdSafe<'a>(pub &'a str);
32
33impl fmt::Display for MdSafe<'_> {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 let mut iter = self.0.chars();
36 while let Some(c) = iter.next() {
37 match c {
38 '\x1b' => {
39 let _ = iter.next();
47 for inner in iter.by_ref().take(64) {
48 if matches!(inner as u32, 0x40..=0x7E) || inner == '\x07' {
49 break;
50 }
51 }
52 }
53 '\n' | '\r' | '\t' => f.write_str(" ")?,
54 '`' => f.write_str("'")?,
55 '|' => f.write_str("\\|")?,
60 c if c.is_control() => {}
61 c => f.write_char(c)?,
62 }
63 }
64 Ok(())
65 }
66}
67
68pub struct MarkdownFormatter;
70
71impl Default for MarkdownFormatter {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl MarkdownFormatter {
78 pub fn new() -> Self {
79 Self
80 }
81
82 fn render_caa_section(&self, caa: &CaaPolicy) -> Vec<String> {
85 let mut out = Vec::new();
86 out.push(String::new());
87 out.push("### CAA Policy".to_string());
88 out.push(String::new());
89
90 if !caa.has_policy {
91 out.push("*No CAA records (any CA may issue)*".to_string());
92 } else {
93 if let Some(ref eff) = caa.effective_domain {
94 out.push(format!("- **Found at**: `{}`", MdSafe(eff)));
95 }
96 out.push(String::new());
97 out.push("| Flags | Tag | Value |".to_string());
98 out.push("| --- | --- | --- |".to_string());
99 for r in &caa.records {
100 out.push(format!(
101 "| {} | `{}` | `{}` |",
102 r.flags,
103 MdSafe(&r.tag),
104 MdSafe(&r.value)
105 ));
106 }
107 }
108
109 if let Some(m) = caa.issuer_match {
110 let rendered = match m {
111 IssuerCaaMatch::NoPolicy => "no policy — any CA permitted",
112 IssuerCaaMatch::Permitted => "issuer permitted by current CAA policy",
113 IssuerCaaMatch::Mismatch => "issuer not in current CAA policy (informational)",
114 IssuerCaaMatch::Indeterminate => "CAA present but no issue/issuewild tags",
115 };
116 out.push(String::new());
117 out.push(format!("- **Issuer vs CAA**: {}", rendered));
118 }
119
120 out.push(String::new());
121 out.push(format!("> **Note:** {}", caa.note));
122 out
123 }
124
125 fn format_rdap_contact(
127 &self,
128 output: &mut Vec<String>,
129 label: &str,
130 contact: &crate::rdap::ContactInfo,
131 ) {
132 if !contact.has_info() {
133 return;
134 }
135 output.push(String::new());
136 output.push(format!("### {}", label));
137 output.push(String::new());
138 if let Some(ref name) = contact.name {
139 output.push(format!("- **Name**: {}", MdSafe(name)));
140 }
141 if let Some(ref org) = contact.organization {
142 output.push(format!("- **Organization**: {}", MdSafe(org)));
143 }
144 if let Some(ref email) = contact.email {
145 output.push(format!("- **Email**: `{}`", MdSafe(email)));
146 }
147 if let Some(ref phone) = contact.phone {
148 output.push(format!("- **Phone**: {}", MdSafe(phone)));
149 }
150 if let Some(ref address) = contact.address {
151 output.push(format!("- **Address**: {}", MdSafe(address)));
152 }
153 if let Some(ref country) = contact.country {
154 output.push(format!("- **Country**: {}", MdSafe(country)));
155 }
156 }
157
158 fn format_whois_contact(
160 &self,
161 output: &mut Vec<String>,
162 label: &str,
163 name: &Option<String>,
164 organization: &Option<String>,
165 email: &Option<String>,
166 phone: &Option<String>,
167 ) {
168 let has_info =
169 name.is_some() || organization.is_some() || email.is_some() || phone.is_some();
170 if !has_info {
171 return;
172 }
173 output.push(String::new());
174 output.push(format!("### {}", label));
175 output.push(String::new());
176 if let Some(ref v) = *name {
177 output.push(format!("- **Name**: {}", MdSafe(v)));
178 }
179 if let Some(ref v) = *organization {
180 output.push(format!("- **Organization**: {}", MdSafe(v)));
181 }
182 if let Some(ref v) = *email {
183 output.push(format!("- **Email**: `{}`", MdSafe(v)));
184 }
185 if let Some(ref v) = *phone {
186 output.push(format!("- **Phone**: {}", MdSafe(v)));
187 }
188 }
189}
190
191impl OutputFormatter for MarkdownFormatter {
195 fn format_whois(&self, response: &WhoisResponse) -> String {
196 self.format_whois(response)
197 }
198 fn format_rdap(&self, response: &RdapResponse) -> String {
199 self.format_rdap(response)
200 }
201 fn format_dns(&self, records: &[DnsRecord]) -> String {
202 self.format_dns(records)
203 }
204 fn format_propagation(&self, result: &PropagationResult) -> String {
205 self.format_propagation(result)
206 }
207 fn format_lookup(&self, result: &LookupResult) -> String {
208 self.format_lookup(result)
209 }
210 fn format_status(&self, response: &StatusResponse) -> String {
211 self.format_status(response)
212 }
213 fn format_follow_iteration(&self, iteration: &FollowIteration) -> String {
214 self.format_follow_iteration(iteration)
215 }
216 fn format_follow(&self, result: &FollowResult) -> String {
217 self.format_follow(result)
218 }
219 fn format_availability(&self, result: &crate::availability::AvailabilityResult) -> String {
220 self.format_availability(result)
221 }
222 fn format_tld(&self, info: &crate::tld::TldInfo) -> String {
223 self.format_tld(info)
224 }
225 fn format_dnssec(&self, report: &crate::dns::DnssecReport) -> String {
226 self.format_dnssec(report)
227 }
228 fn format_dns_comparison(&self, comparison: &crate::dns::DnsComparison) -> String {
229 self.format_dns_comparison(comparison)
230 }
231 fn format_subdomains(&self, result: &crate::subdomains::SubdomainResult) -> String {
232 self.format_subdomains(result)
233 }
234 fn format_diff(&self, diff: &crate::diff::DomainDiff) -> String {
235 self.format_diff(diff)
236 }
237 fn format_ssl(&self, report: &crate::ssl::SslReport) -> String {
238 self.format_ssl(report)
239 }
240 fn format_watch(&self, report: &crate::watchlist::WatchReport) -> String {
241 self.format_watch(report)
242 }
243 fn format_domain_info(&self, info: &crate::domain_info::DomainInfo) -> String {
244 self.format_domain_info(info)
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn md(s: &str) -> String {
255 format!("{}", MdSafe(s))
256 }
257
258 #[test]
259 fn test_mdsafe_strips_ansi_escape() {
260 assert_eq!(md("\x1b[31mfoo\x1b[0m"), "foo");
261 }
262
263 #[test]
264 fn test_mdsafe_collapses_newlines_cr_tab() {
265 assert_eq!(md("a\nb"), "a b");
266 assert_eq!(md("a\rb"), "a b");
267 assert_eq!(md("a\tb"), "a b");
268 assert_eq!(md("a\r\nb"), "a b");
271 }
272
273 #[test]
274 fn test_mdsafe_neutralizes_backticks() {
275 assert_eq!(md("`bad`"), "'bad'");
276 assert_eq!(md("a `b` c"), "a 'b' c");
277 }
278
279 #[test]
280 fn test_mdsafe_escapes_table_pipe() {
281 assert_eq!(md("a|b"), "a\\|b");
286 assert_eq!(md("x | y | z"), "x \\| y \\| z");
287 }
288
289 #[test]
290 fn test_mdsafe_drops_other_control_chars() {
291 assert_eq!(md("a\0b\x7fc"), "abc");
293 }
294
295 #[test]
296 fn test_mdsafe_preserves_unicode() {
297 assert_eq!(md("café — résumé"), "café — résumé");
298 }
299}