1use super::OutputFormatter;
2use crate::dns::{DnsRecord, FollowIteration, FollowResult, PropagationResult};
3use crate::lookup::LookupResult;
4use crate::rdap::RdapResponse;
5use crate::status::StatusResponse;
6use crate::whois::WhoisResponse;
7
8pub struct MarkdownFormatter;
10
11impl Default for MarkdownFormatter {
12 fn default() -> Self {
13 Self::new()
14 }
15}
16
17impl MarkdownFormatter {
18 pub fn new() -> Self {
19 Self
20 }
21
22 fn format_rdap_contact(
24 &self,
25 output: &mut Vec<String>,
26 label: &str,
27 contact: &crate::rdap::ContactInfo,
28 ) {
29 if !contact.has_info() {
30 return;
31 }
32 output.push(String::new());
33 output.push(format!("### {}", label));
34 output.push(String::new());
35 if let Some(ref name) = contact.name {
36 output.push(format!("- **Name**: {}", name));
37 }
38 if let Some(ref org) = contact.organization {
39 output.push(format!("- **Organization**: {}", org));
40 }
41 if let Some(ref email) = contact.email {
42 output.push(format!("- **Email**: `{}`", email));
43 }
44 if let Some(ref phone) = contact.phone {
45 output.push(format!("- **Phone**: {}", phone));
46 }
47 if let Some(ref address) = contact.address {
48 output.push(format!("- **Address**: {}", address));
49 }
50 if let Some(ref country) = contact.country {
51 output.push(format!("- **Country**: {}", country));
52 }
53 }
54
55 fn format_whois_contact(
57 &self,
58 output: &mut Vec<String>,
59 label: &str,
60 name: &Option<String>,
61 organization: &Option<String>,
62 email: &Option<String>,
63 phone: &Option<String>,
64 ) {
65 let has_info =
66 name.is_some() || organization.is_some() || email.is_some() || phone.is_some();
67 if !has_info {
68 return;
69 }
70 output.push(String::new());
71 output.push(format!("### {}", label));
72 output.push(String::new());
73 if let Some(ref v) = *name {
74 output.push(format!("- **Name**: {}", v));
75 }
76 if let Some(ref v) = *organization {
77 output.push(format!("- **Organization**: {}", v));
78 }
79 if let Some(ref v) = *email {
80 output.push(format!("- **Email**: `{}`", v));
81 }
82 if let Some(ref v) = *phone {
83 output.push(format!("- **Phone**: {}", v));
84 }
85 }
86}
87
88impl OutputFormatter for MarkdownFormatter {
89 fn format_whois(&self, response: &WhoisResponse) -> String {
90 let mut output = Vec::new();
91
92 output.push(format!("## WHOIS: {}", response.domain));
93 output.push(String::new());
94
95 if response.is_available() {
96 output.push("Domain is **available** for registration.".to_string());
97 return output.join("\n");
98 }
99
100 if let Some(ref registrar) = response.registrar {
101 output.push(format!("- **Registrar**: {}", registrar));
102 }
103 if let Some(ref registrant) = response.registrant {
104 output.push(format!("- **Registrant**: {}", registrant));
105 }
106 if let Some(ref organization) = response.organization {
107 output.push(format!("- **Organization**: {}", organization));
108 }
109
110 let has_registrant_details = response.registrant_email.is_some()
112 || response.registrant_phone.is_some()
113 || response.registrant_address.is_some()
114 || response.registrant_country.is_some();
115
116 if has_registrant_details {
117 output.push(String::new());
118 output.push("### Registrant Contact".to_string());
119 output.push(String::new());
120 if let Some(ref email) = response.registrant_email {
121 output.push(format!("- **Email**: `{}`", email));
122 }
123 if let Some(ref phone) = response.registrant_phone {
124 output.push(format!("- **Phone**: {}", phone));
125 }
126 if let Some(ref address) = response.registrant_address {
127 output.push(format!("- **Address**: {}", address));
128 }
129 if let Some(ref country) = response.registrant_country {
130 output.push(format!("- **Country**: {}", country));
131 }
132 }
133
134 self.format_whois_contact(
136 &mut output,
137 "Admin Contact",
138 &response.admin_name,
139 &response.admin_organization,
140 &response.admin_email,
141 &response.admin_phone,
142 );
143
144 self.format_whois_contact(
146 &mut output,
147 "Tech Contact",
148 &response.tech_name,
149 &response.tech_organization,
150 &response.tech_email,
151 &response.tech_phone,
152 );
153
154 if let Some(created) = response.creation_date {
155 output.push(format!("- **Created**: `{}`", created.format("%Y-%m-%d")));
156 }
157 if let Some(expires) = response.expiration_date {
158 let days_until = (expires - chrono::Utc::now()).num_days();
159 output.push(format!(
160 "- **Expires**: `{}` ({} days)",
161 expires.format("%Y-%m-%d"),
162 days_until
163 ));
164 }
165 if let Some(updated) = response.updated_date {
166 output.push(format!("- **Updated**: `{}`", updated.format("%Y-%m-%d")));
167 }
168
169 if !response.nameservers.is_empty() {
170 output.push(format!(
171 "- **Nameservers**: {}",
172 response
173 .nameservers
174 .iter()
175 .map(|ns| format!("`{}`", ns))
176 .collect::<Vec<_>>()
177 .join(", ")
178 ));
179 }
180
181 if !response.status.is_empty() {
182 output.push(format!(
183 "- **Status**: {}",
184 response
185 .status
186 .iter()
187 .map(|s| format!("`{}`", s))
188 .collect::<Vec<_>>()
189 .join(", ")
190 ));
191 }
192
193 if let Some(ref dnssec) = response.dnssec {
194 output.push(format!("- **DNSSEC**: {}", dnssec));
195 }
196
197 output.push(format!("- **WHOIS Server**: `{}`", response.whois_server));
198
199 output.join("\n")
200 }
201
202 fn format_rdap(&self, response: &RdapResponse) -> String {
203 let mut output = Vec::new();
204
205 let name = response
206 .domain_name()
207 .or(response.name.as_deref())
208 .unwrap_or("Unknown");
209 output.push(format!("## RDAP: {}", name));
210 output.push(String::new());
211
212 if let Some(ref handle) = response.handle {
213 output.push(format!("- **Handle**: `{}`", handle));
214 }
215 if let Some(registrar) = response.get_registrar() {
216 output.push(format!("- **Registrar**: {}", registrar));
217 }
218 if let Some(registrant) = response.get_registrant() {
219 output.push(format!("- **Registrant**: {}", registrant));
220 }
221 if let Some(organization) = response.get_registrant_organization() {
222 output.push(format!("- **Organization**: {}", organization));
223 }
224
225 if let Some(contact) = response.get_registrant_contact() {
227 self.format_rdap_contact(&mut output, "Registrant Contact", &contact);
228 }
229 if let Some(contact) = response.get_admin_contact() {
230 self.format_rdap_contact(&mut output, "Admin Contact", &contact);
231 }
232 if let Some(contact) = response.get_tech_contact() {
233 self.format_rdap_contact(&mut output, "Tech Contact", &contact);
234 }
235 if let Some(contact) = response.get_billing_contact() {
236 self.format_rdap_contact(&mut output, "Billing Contact", &contact);
237 }
238
239 if let Some(created) = response.creation_date() {
240 output.push(format!("- **Created**: `{}`", created.format("%Y-%m-%d")));
241 }
242 if let Some(expires) = response.expiration_date() {
243 let days_until = (expires - chrono::Utc::now()).num_days();
244 output.push(format!(
245 "- **Expires**: `{}` ({} days)",
246 expires.format("%Y-%m-%d"),
247 days_until
248 ));
249 }
250 if let Some(updated) = response.last_updated() {
251 output.push(format!("- **Updated**: `{}`", updated.format("%Y-%m-%d")));
252 }
253
254 if !response.status.is_empty() {
255 output.push(format!(
256 "- **Status**: {}",
257 response
258 .status
259 .iter()
260 .map(|s| format!("`{}`", s))
261 .collect::<Vec<_>>()
262 .join(", ")
263 ));
264 }
265
266 let nameservers = response.nameserver_names();
267 if !nameservers.is_empty() {
268 output.push(format!(
269 "- **Nameservers**: {}",
270 nameservers
271 .iter()
272 .map(|ns| format!("`{}`", ns))
273 .collect::<Vec<_>>()
274 .join(", ")
275 ));
276 }
277
278 if response.is_dnssec_signed() {
279 output.push("- **DNSSEC**: signed".to_string());
280 }
281
282 if let Some(ref start) = response.start_address {
284 output.push(format!("- **Start Address**: `{}`", start));
285 }
286 if let Some(ref end) = response.end_address {
287 output.push(format!("- **End Address**: `{}`", end));
288 }
289 if let Some(ref country) = response.country {
290 output.push(format!("- **Country**: {}", country));
291 }
292
293 if let Some(start) = response.start_autnum {
295 output.push(format!(
296 "- **AS Number**: `AS{}` - `AS{}`",
297 start,
298 response.end_autnum.unwrap_or(start)
299 ));
300 }
301
302 output.join("\n")
303 }
304
305 fn format_dns(&self, records: &[DnsRecord]) -> String {
306 let mut output = Vec::new();
307
308 if records.is_empty() {
309 output.push("*No records found*".to_string());
310 return output.join("\n");
311 }
312
313 let domain = &records[0].name;
314 let record_type = &records[0].record_type;
315 output.push(format!("## DNS {} Records: {}", record_type, domain));
316 output.push(String::new());
317 output.push("| Name | TTL | Type | Data |".to_string());
318 output.push("| --- | --- | --- | --- |".to_string());
319
320 for record in records {
321 output.push(format!(
322 "| `{}` | {} | {} | `{}` |",
323 record.name, record.ttl, record.record_type, record.data
324 ));
325 }
326
327 output.join("\n")
328 }
329
330 fn format_propagation(&self, result: &PropagationResult) -> String {
331 let mut output = Vec::new();
332
333 output.push(format!(
334 "## Propagation: {} {}",
335 result.domain, result.record_type
336 ));
337 output.push(String::new());
338
339 let percentage = result.propagation_percentage;
341 let status = if percentage >= 100.0 {
342 "Fully propagated"
343 } else if percentage >= 80.0 {
344 "Mostly propagated"
345 } else if percentage >= 50.0 {
346 "Partially propagated"
347 } else {
348 "Not propagated"
349 };
350 output.push(format!("**{:.1}%** - {}", percentage, status));
351 output.push(String::new());
352 output.push(format!(
353 "- **Servers responding**: {}/{}",
354 result.servers_responding, result.servers_checked
355 ));
356
357 if !result.consensus_values.is_empty() {
359 output.push(format!(
360 "- **Consensus values**: {}",
361 result
362 .consensus_values
363 .iter()
364 .map(|v| format!("`{}`", v))
365 .collect::<Vec<_>>()
366 .join(", ")
367 ));
368 }
369
370 if !result.inconsistencies.is_empty() {
372 output.push(String::new());
373 output.push("### Inconsistencies".to_string());
374 output.push(String::new());
375 for inconsistency in &result.inconsistencies {
376 output.push(format!("- {}", inconsistency));
377 }
378 }
379
380 output.push(String::new());
382 output.push("### Results".to_string());
383 output.push(String::new());
384 output.push("| Server | Location | IP | Result | Time |".to_string());
385 output.push("| --- | --- | --- | --- | --- |".to_string());
386
387 for sr in &result.results {
388 let result_str = if sr.success {
389 if sr.records.is_empty() {
390 "NXDOMAIN".to_string()
391 } else {
392 sr.records
393 .iter()
394 .map(|r| r.format_short())
395 .collect::<Vec<_>>()
396 .join(", ")
397 }
398 } else {
399 sr.error.as_deref().unwrap_or("Error").to_string()
400 };
401
402 output.push(format!(
403 "| {} | {} | `{}` | `{}` | {}ms |",
404 sr.server.name, sr.server.location, sr.server.ip, result_str, sr.response_time_ms
405 ));
406 }
407
408 output.join("\n")
409 }
410
411 fn format_lookup(&self, result: &LookupResult) -> String {
412 let mut output = Vec::new();
413
414 let domain = result
415 .domain_name()
416 .unwrap_or_else(|| "Unknown".to_string());
417 let source = match result {
418 LookupResult::Rdap { .. } => "RDAP",
419 LookupResult::Whois { .. } => "WHOIS",
420 LookupResult::Available { .. } => "availability",
421 };
422
423 output.push(format!("## Lookup: {}", domain));
424 output.push(String::new());
425 output.push(format!("- **Source**: {}", source));
426
427 match result {
428 LookupResult::Rdap {
429 data,
430 whois_fallback,
431 } => {
432 if let Some(registrar) = data.get_registrar() {
433 output.push(format!("- **Registrar**: {}", registrar));
434 }
435 if let Some(registrant) = data.get_registrant() {
436 output.push(format!("- **Registrant**: {}", registrant));
437 }
438 if let Some(organization) = data.get_registrant_organization() {
439 output.push(format!("- **Organization**: {}", organization));
440 }
441
442 if let Some(contact) = data.get_registrant_contact() {
444 self.format_rdap_contact(&mut output, "Registrant Contact", &contact);
445 }
446 if let Some(contact) = data.get_admin_contact() {
447 self.format_rdap_contact(&mut output, "Admin Contact", &contact);
448 }
449 if let Some(contact) = data.get_tech_contact() {
450 self.format_rdap_contact(&mut output, "Tech Contact", &contact);
451 }
452
453 if let Some(created) = data.creation_date() {
454 output.push(format!("- **Created**: `{}`", created.format("%Y-%m-%d")));
455 }
456 if let Some(expires) = data.expiration_date() {
457 let days_until = (expires - chrono::Utc::now()).num_days();
458 output.push(format!(
459 "- **Expires**: `{}` ({} days)",
460 expires.format("%Y-%m-%d"),
461 days_until
462 ));
463 }
464
465 if !data.status.is_empty() {
466 output.push(format!(
467 "- **Status**: {}",
468 data.status
469 .iter()
470 .map(|s| format!("`{}`", s))
471 .collect::<Vec<_>>()
472 .join(", ")
473 ));
474 }
475
476 let nameservers = data.nameserver_names();
477 if !nameservers.is_empty() {
478 output.push(format!(
479 "- **Nameservers**: {}",
480 nameservers
481 .iter()
482 .map(|ns| format!("`{}`", ns))
483 .collect::<Vec<_>>()
484 .join(", ")
485 ));
486 }
487
488 if data.is_dnssec_signed() {
489 output.push("- **DNSSEC**: signed".to_string());
490 }
491
492 if let Some(whois) = whois_fallback {
494 let mut extra = Vec::new();
495
496 if data.get_registrant().is_none() {
497 if let Some(ref registrant) = whois.registrant {
498 extra.push(format!("- **Registrant**: {}", registrant));
499 }
500 }
501 if data.get_registrant_organization().is_none() {
502 if let Some(ref org) = whois.organization {
503 extra.push(format!("- **Organization**: {}", org));
504 }
505 }
506
507 let rdap_has_registrant = data
509 .get_registrant_contact()
510 .as_ref()
511 .is_some_and(|c| c.has_info());
512 if !rdap_has_registrant {
513 let has_whois_contact = whois.registrant_email.is_some()
514 || whois.registrant_phone.is_some()
515 || whois.registrant_address.is_some()
516 || whois.registrant_country.is_some();
517 if has_whois_contact {
518 extra.push(String::new());
519 extra.push("### Registrant Contact".to_string());
520 extra.push(String::new());
521 if let Some(ref email) = whois.registrant_email {
522 extra.push(format!("- **Email**: `{}`", email));
523 }
524 if let Some(ref phone) = whois.registrant_phone {
525 extra.push(format!("- **Phone**: {}", phone));
526 }
527 if let Some(ref address) = whois.registrant_address {
528 extra.push(format!("- **Address**: {}", address));
529 }
530 if let Some(ref country) = whois.registrant_country {
531 extra.push(format!("- **Country**: {}", country));
532 }
533 }
534 }
535
536 let rdap_has_admin = data.get_admin_contact().is_some_and(|c| c.has_info());
538 if !rdap_has_admin {
539 let has_whois_admin = whois.admin_name.is_some()
540 || whois.admin_email.is_some()
541 || whois.admin_phone.is_some();
542 if has_whois_admin {
543 extra.push(String::new());
544 extra.push("### Admin Contact".to_string());
545 extra.push(String::new());
546 if let Some(ref name) = whois.admin_name {
547 extra.push(format!("- **Name**: {}", name));
548 }
549 if let Some(ref org) = whois.admin_organization {
550 extra.push(format!("- **Organization**: {}", org));
551 }
552 if let Some(ref email) = whois.admin_email {
553 extra.push(format!("- **Email**: `{}`", email));
554 }
555 if let Some(ref phone) = whois.admin_phone {
556 extra.push(format!("- **Phone**: {}", phone));
557 }
558 }
559 }
560
561 let rdap_has_tech = data.get_tech_contact().is_some_and(|c| c.has_info());
563 if !rdap_has_tech {
564 let has_whois_tech = whois.tech_name.is_some()
565 || whois.tech_email.is_some()
566 || whois.tech_phone.is_some();
567 if has_whois_tech {
568 extra.push(String::new());
569 extra.push("### Tech Contact".to_string());
570 extra.push(String::new());
571 if let Some(ref name) = whois.tech_name {
572 extra.push(format!("- **Name**: {}", name));
573 }
574 if let Some(ref org) = whois.tech_organization {
575 extra.push(format!("- **Organization**: {}", org));
576 }
577 if let Some(ref email) = whois.tech_email {
578 extra.push(format!("- **Email**: `{}`", email));
579 }
580 if let Some(ref phone) = whois.tech_phone {
581 extra.push(format!("- **Phone**: {}", phone));
582 }
583 }
584 }
585
586 if let Some(updated) = whois.updated_date {
587 extra.push(format!("- **Updated**: `{}`", updated.format("%Y-%m-%d")));
588 }
589
590 if !data.is_dnssec_signed() {
591 if let Some(ref dnssec) = whois.dnssec {
592 extra.push(format!("- **DNSSEC**: {}", dnssec));
593 }
594 }
595
596 if !whois.whois_server.is_empty() {
597 extra.push(format!("- **WHOIS Server**: `{}`", whois.whois_server));
598 }
599
600 if !extra.is_empty() {
601 output.push(String::new());
602 output.push("### Additional WHOIS Data".to_string());
603 output.push(String::new());
604 output.extend(extra);
605 }
606 }
607 }
608 LookupResult::Whois {
609 data, rdap_error, ..
610 } => {
611 if let Some(ref error) = rdap_error {
612 output.push(format!("- **RDAP Error**: {}", error));
613 }
614
615 if let Some(ref registrar) = data.registrar {
616 output.push(format!("- **Registrar**: {}", registrar));
617 }
618 if let Some(ref registrant) = data.registrant {
619 output.push(format!("- **Registrant**: {}", registrant));
620 }
621 if let Some(ref organization) = data.organization {
622 output.push(format!("- **Organization**: {}", organization));
623 }
624
625 let has_registrant_details = data.registrant_email.is_some()
627 || data.registrant_phone.is_some()
628 || data.registrant_address.is_some()
629 || data.registrant_country.is_some();
630
631 if has_registrant_details {
632 output.push(String::new());
633 output.push("### Registrant Contact".to_string());
634 output.push(String::new());
635 if let Some(ref email) = data.registrant_email {
636 output.push(format!("- **Email**: `{}`", email));
637 }
638 if let Some(ref phone) = data.registrant_phone {
639 output.push(format!("- **Phone**: {}", phone));
640 }
641 if let Some(ref address) = data.registrant_address {
642 output.push(format!("- **Address**: {}", address));
643 }
644 if let Some(ref country) = data.registrant_country {
645 output.push(format!("- **Country**: {}", country));
646 }
647 }
648
649 self.format_whois_contact(
651 &mut output,
652 "Admin Contact",
653 &data.admin_name,
654 &data.admin_organization,
655 &data.admin_email,
656 &data.admin_phone,
657 );
658
659 self.format_whois_contact(
661 &mut output,
662 "Tech Contact",
663 &data.tech_name,
664 &data.tech_organization,
665 &data.tech_email,
666 &data.tech_phone,
667 );
668
669 if let Some(created) = data.creation_date {
670 output.push(format!("- **Created**: `{}`", created.format("%Y-%m-%d")));
671 }
672 if let Some(expires) = data.expiration_date {
673 let days_until = (expires - chrono::Utc::now()).num_days();
674 output.push(format!(
675 "- **Expires**: `{}` ({} days)",
676 expires.format("%Y-%m-%d"),
677 days_until
678 ));
679 }
680
681 if !data.status.is_empty() {
682 output.push(format!(
683 "- **Status**: {}",
684 data.status
685 .iter()
686 .map(|s| format!("`{}`", s))
687 .collect::<Vec<_>>()
688 .join(", ")
689 ));
690 }
691
692 if !data.nameservers.is_empty() {
693 output.push(format!(
694 "- **Nameservers**: {}",
695 data.nameservers
696 .iter()
697 .map(|ns| format!("`{}`", ns))
698 .collect::<Vec<_>>()
699 .join(", ")
700 ));
701 }
702
703 if let Some(ref dnssec) = data.dnssec {
704 output.push(format!("- **DNSSEC**: {}", dnssec));
705 }
706 }
707 LookupResult::Available {
708 data,
709 rdap_error,
710 whois_error,
711 } => {
712 let avail_str = if data.available {
713 "**AVAILABLE**"
714 } else {
715 "**TAKEN**"
716 };
717 output.push(format!("- **Availability**: {}", avail_str));
718 output.push(format!("- **Confidence**: {}", data.confidence));
719 output.push(format!("- **Method**: {}", data.method));
720 if let Some(ref details) = data.details {
721 output.push(format!("- **Details**: {}", details));
722 }
723 output.push(format!("- **RDAP Error**: {}", rdap_error));
724 output.push(format!("- **WHOIS Error**: {}", whois_error));
725 }
726 }
727
728 output.join("\n")
729 }
730
731 fn format_status(&self, response: &StatusResponse) -> String {
732 let mut output = Vec::new();
733
734 output.push(format!("## Status: {}", response.domain));
735 output.push(String::new());
736
737 if let Some(status) = response.http_status {
739 let status_text = response.http_status_text.as_deref().unwrap_or("Unknown");
740 output.push(format!("- **HTTP Status**: `{}` ({})", status, status_text));
741 }
742
743 if let Some(ref title) = response.title {
745 output.push(format!("- **Site Title**: {}", title));
746 }
747
748 output.push(String::new());
750 if let Some(ref cert) = response.certificate {
751 output.push("### SSL Certificate".to_string());
752 output.push(String::new());
753 output.push(format!("- **Subject**: `{}`", cert.subject));
754 output.push(format!("- **Issuer**: {}", cert.issuer));
755 output.push(format!(
756 "- **Status**: {}",
757 if cert.is_valid { "Valid" } else { "Invalid" }
758 ));
759 output.push(format!(
760 "- **Valid From**: `{}`",
761 cert.valid_from.format("%Y-%m-%d")
762 ));
763 output.push(format!(
764 "- **Expires**: `{}` ({} days)",
765 cert.valid_until.format("%Y-%m-%d"),
766 cert.days_until_expiry
767 ));
768 } else {
769 output.push("### SSL Certificate".to_string());
770 output.push(String::new());
771 output.push("*Not available (HTTPS may not be configured)*".to_string());
772 }
773
774 if let Some(ref expiry) = response.domain_expiration {
776 output.push(String::new());
777 output.push("### Domain Registration".to_string());
778 output.push(String::new());
779 if let Some(ref registrar) = expiry.registrar {
780 output.push(format!("- **Registrar**: {}", registrar));
781 }
782 output.push(format!(
783 "- **Expires**: `{}` ({} days)",
784 expiry.expiration_date.format("%Y-%m-%d"),
785 expiry.days_until_expiry
786 ));
787 }
788
789 output.push(String::new());
791 if let Some(ref dns) = response.dns_resolution {
792 output.push("### DNS Resolution".to_string());
793 output.push(String::new());
794 output.push(format!(
795 "- **Resolves**: {}",
796 if dns.resolves { "Yes" } else { "No" }
797 ));
798
799 if let Some(ref cname) = dns.cname_target {
800 output.push(format!("- **CNAME**: `{}`", cname));
801 }
802 if !dns.a_records.is_empty() {
803 output.push(format!(
804 "- **IPv4 (A)**: {}",
805 dns.a_records
806 .iter()
807 .map(|ip| format!("`{}`", ip))
808 .collect::<Vec<_>>()
809 .join(", ")
810 ));
811 }
812 if !dns.aaaa_records.is_empty() {
813 output.push(format!(
814 "- **IPv6 (AAAA)**: {}",
815 dns.aaaa_records
816 .iter()
817 .map(|ip| format!("`{}`", ip))
818 .collect::<Vec<_>>()
819 .join(", ")
820 ));
821 }
822 if !dns.nameservers.is_empty() {
823 output.push(format!(
824 "- **Nameservers**: {}",
825 dns.nameservers
826 .iter()
827 .map(|ns| format!("`{}`", ns))
828 .collect::<Vec<_>>()
829 .join(", ")
830 ));
831 }
832 } else {
833 output.push("### DNS Resolution".to_string());
834 output.push(String::new());
835 output.push("*Check failed*".to_string());
836 }
837
838 output.join("\n")
839 }
840
841 fn format_follow_iteration(&self, iteration: &FollowIteration) -> String {
842 let time_str = iteration.timestamp.format("%H:%M:%S").to_string();
843
844 if let Some(ref error) = iteration.error {
845 return format!(
846 "[{}] Iteration {}/{}: **ERROR** - {}",
847 time_str, iteration.iteration, iteration.total_iterations, error
848 );
849 }
850
851 let record_count = iteration.record_count();
852 let status = if iteration.iteration == 1 {
853 String::new()
854 } else if iteration.changed {
855 " (**CHANGED**)".to_string()
856 } else {
857 " (unchanged)".to_string()
858 };
859
860 let values: Vec<String> = iteration
861 .records
862 .iter()
863 .map(|r| r.data.to_string().trim_end_matches('.').to_string())
864 .collect();
865
866 let values_str = if values.is_empty() {
867 String::new()
868 } else {
869 format!(" `{}`", values.join(", "))
870 };
871
872 format!(
873 "[{}] Iteration {}/{}: {} record(s){}{}",
874 time_str,
875 iteration.iteration,
876 iteration.total_iterations,
877 record_count,
878 status,
879 values_str
880 )
881 }
882
883 fn format_follow(&self, result: &FollowResult) -> String {
884 let mut output = Vec::new();
885
886 output.push(format!(
887 "## DNS Follow: {} {}",
888 result.domain, result.record_type
889 ));
890 output.push(String::new());
891
892 output.push(format!(
894 "- **Iterations**: {}/{}",
895 result.completed_iterations(),
896 result.iterations_requested
897 ));
898
899 if result.interrupted {
900 output.push("- **Status**: Interrupted".to_string());
901 }
902
903 output.push(format!("- **Total changes**: {}", result.total_changes));
904
905 let duration = result.ended_at - result.started_at;
906 let total_secs = duration.num_seconds();
907 let duration_str = if total_secs < 60 {
908 format!("{}s", total_secs)
909 } else if total_secs < 3600 {
910 format!("{}m {}s", total_secs / 60, total_secs % 60)
911 } else {
912 format!("{}h {}m", total_secs / 3600, (total_secs % 3600) / 60)
913 };
914 output.push(format!("- **Duration**: {}", duration_str));
915
916 if !result.iterations.is_empty() {
918 output.push(String::new());
919 output.push("### Iteration Details".to_string());
920 output.push(String::new());
921 output.push("| # | Time | Records | Status |".to_string());
922 output.push("| --- | --- | --- | --- |".to_string());
923
924 for iteration in &result.iterations {
925 let time_str = iteration.timestamp.format("%H:%M:%S").to_string();
926 let status = if iteration.error.is_some() {
927 "ERROR"
928 } else if iteration.changed {
929 "CHANGED"
930 } else if iteration.iteration == 1 {
931 "initial"
932 } else {
933 "stable"
934 };
935
936 output.push(format!(
937 "| {} | {} | {} | {} |",
938 iteration.iteration,
939 time_str,
940 iteration.record_count(),
941 status
942 ));
943 }
944 }
945
946 output.join("\n")
947 }
948
949 fn format_availability(&self, result: &crate::availability::AvailabilityResult) -> String {
950 let mut output = Vec::new();
951
952 output.push(format!("## Availability: {}", result.domain));
953 output.push(String::new());
954
955 let avail_str = if result.available {
956 "**AVAILABLE**"
957 } else {
958 "**TAKEN**"
959 };
960 output.push(format!("- **Result**: {}", avail_str));
961 output.push(format!("- **Confidence**: {}", result.confidence));
962 output.push(format!("- **Method**: {}", result.method));
963 if let Some(ref details) = result.details {
964 output.push(format!("- **Details**: {}", details));
965 }
966
967 output.join("\n")
968 }
969
970 fn format_tld(&self, info: &crate::tld::TldInfo) -> String {
971 let mut output = Vec::new();
972
973 output.push(format!("## TLD Info: .{}", info.tld));
974 output.push(String::new());
975
976 output.push(format!("- **Type**: {}", info.tld_type));
977
978 match info.whois_server {
979 Some(ref server) => output.push(format!("- **WHOIS Server**: `{}`", server)),
980 None => output.push("- **WHOIS Server**: *not available*".to_string()),
981 }
982
983 match info.rdap_url {
984 Some(ref url) => output.push(format!("- **RDAP URL**: `{}`", url)),
985 None => output.push("- **RDAP URL**: *not available*".to_string()),
986 }
987
988 match info.registry_url {
989 Some(ref url) => output.push(format!("- **Registry URL**: {}", url)),
990 None => output.push("- **Registry URL**: *not available*".to_string()),
991 }
992
993 output.join("\n")
994 }
995
996 fn format_dnssec(&self, report: &crate::dns::DnssecReport) -> String {
997 let mut output = Vec::new();
998
999 output.push(format!("## DNSSEC: {}", report.domain));
1000 output.push(String::new());
1001
1002 output.push(format!("- **Status**: `{}`", report.status));
1003 output.push(format!(
1004 "- **Chain Valid**: {}",
1005 if report.chain_valid { "yes" } else { "no" }
1006 ));
1007 output.push(format!("- **Enabled**: {}", report.enabled));
1008 output.push(format!("- **DS Records**: {}", report.ds_records.len()));
1009 output.push(format!(
1010 "- **DNSKEY Records**: {}",
1011 report.dnskey_records.len()
1012 ));
1013
1014 if !report.ds_records.is_empty() {
1015 output.push(String::new());
1016 output.push("### DS Records".to_string());
1017 output.push(String::new());
1018 output.push("| Key Tag | Algorithm | Digest Type | Matched | Verified |".to_string());
1019 output.push("| --- | --- | --- | --- | --- |".to_string());
1020 for ds in &report.ds_records {
1021 output.push(format!(
1022 "| {} | {} ({}) | {} ({}) | {} | {} |",
1023 ds.key_tag,
1024 ds.algorithm,
1025 ds.algorithm_name,
1026 ds.digest_type,
1027 ds.digest_type_name,
1028 if ds.matched_key { "yes" } else { "no" },
1029 if ds.digest_verified { "yes" } else { "no" },
1030 ));
1031 }
1032 }
1033
1034 if !report.dnskey_records.is_empty() {
1035 output.push(String::new());
1036 output.push("### DNSKEY Records".to_string());
1037 output.push(String::new());
1038 output.push("| Key Tag | Flags | Role | Algorithm |".to_string());
1039 output.push("| --- | --- | --- | --- |".to_string());
1040 for key in &report.dnskey_records {
1041 let role = if key.is_ksk {
1042 "KSK"
1043 } else if key.is_zsk {
1044 "ZSK"
1045 } else {
1046 "Other"
1047 };
1048 output.push(format!(
1049 "| {} | {} | {} | {} ({}) |",
1050 key.key_tag, key.flags, role, key.algorithm, key.algorithm_name
1051 ));
1052 }
1053 }
1054
1055 if !report.issues.is_empty() {
1056 output.push(String::new());
1057 output.push("### Issues".to_string());
1058 output.push(String::new());
1059 for issue in &report.issues {
1060 output.push(format!("- {}", issue));
1061 }
1062 }
1063
1064 output.join("\n")
1065 }
1066
1067 fn format_dns_comparison(&self, comparison: &crate::dns::DnsComparison) -> String {
1068 let mut output = Vec::new();
1069
1070 output.push(format!(
1071 "## DNS Comparison: {} {}",
1072 comparison.domain, comparison.record_type
1073 ));
1074 output.push(String::new());
1075
1076 if comparison.matches {
1077 output.push("**Result**: Records match".to_string());
1078 } else {
1079 output.push("**Result**: Records differ".to_string());
1080 }
1081 output.push(String::new());
1082
1083 output.push(format!("### Server A ({})", comparison.server_a.nameserver));
1085 output.push(String::new());
1086 if let Some(ref err) = comparison.server_a.error {
1087 output.push(format!("**Error**: {}", err));
1088 } else if comparison.server_a.records.is_empty() {
1089 output.push("*No records found*".to_string());
1090 } else {
1091 output.push("| Record |".to_string());
1092 output.push("| --- |".to_string());
1093 for record in &comparison.server_a.records {
1094 output.push(format!("| `{}` |", record.format_short()));
1095 }
1096 }
1097 output.push(String::new());
1098
1099 output.push(format!("### Server B ({})", comparison.server_b.nameserver));
1101 output.push(String::new());
1102 if let Some(ref err) = comparison.server_b.error {
1103 output.push(format!("**Error**: {}", err));
1104 } else if comparison.server_b.records.is_empty() {
1105 output.push("*No records found*".to_string());
1106 } else {
1107 output.push("| Record |".to_string());
1108 output.push("| --- |".to_string());
1109 for record in &comparison.server_b.records {
1110 output.push(format!("| `{}` |", record.format_short()));
1111 }
1112 }
1113 output.push(String::new());
1114
1115 output.push("### Comparison".to_string());
1117 output.push(String::new());
1118
1119 if comparison.common.is_empty() {
1120 output.push("- **Common**: *(none)*".to_string());
1121 } else {
1122 output.push(format!(
1123 "- **Common**: {}",
1124 comparison
1125 .common
1126 .iter()
1127 .map(|r| format!("`{}`", r))
1128 .collect::<Vec<_>>()
1129 .join(", ")
1130 ));
1131 }
1132
1133 if comparison.only_in_a.is_empty() {
1134 output.push(format!(
1135 "- **Only in {}**: *(none)*",
1136 comparison.server_a.nameserver
1137 ));
1138 } else {
1139 output.push(format!(
1140 "- **Only in {}**: {}",
1141 comparison.server_a.nameserver,
1142 comparison
1143 .only_in_a
1144 .iter()
1145 .map(|r| format!("`{}`", r))
1146 .collect::<Vec<_>>()
1147 .join(", ")
1148 ));
1149 }
1150
1151 if comparison.only_in_b.is_empty() {
1152 output.push(format!(
1153 "- **Only in {}**: *(none)*",
1154 comparison.server_b.nameserver
1155 ));
1156 } else {
1157 output.push(format!(
1158 "- **Only in {}**: {}",
1159 comparison.server_b.nameserver,
1160 comparison
1161 .only_in_b
1162 .iter()
1163 .map(|r| format!("`{}`", r))
1164 .collect::<Vec<_>>()
1165 .join(", ")
1166 ));
1167 }
1168
1169 output.join("\n")
1170 }
1171
1172 fn format_subdomains(&self, result: &crate::subdomains::SubdomainResult) -> String {
1173 let mut output = Vec::new();
1174
1175 output.push(format!("## Subdomains: {}", result.domain));
1176 output.push(String::new());
1177 output.push(format!("- **Source**: {}", result.source));
1178 output.push(format!("- **Count**: {}", result.count));
1179 output.push(String::new());
1180
1181 if result.subdomains.is_empty() {
1182 output.push("*No subdomains found*".to_string());
1183 } else {
1184 for subdomain in &result.subdomains {
1185 output.push(format!("- `{}`", subdomain));
1186 }
1187 }
1188
1189 output.join("\n")
1190 }
1191
1192 fn format_diff(&self, diff: &crate::diff::DomainDiff) -> String {
1193 let dash = "\u{2014}";
1194 let mut output = Vec::new();
1195
1196 output.push(format!(
1197 "## Domain Comparison: {} vs {}",
1198 diff.domain_a, diff.domain_b
1199 ));
1200 output.push(String::new());
1201
1202 output.push("### Registration".to_string());
1204 output.push(String::new());
1205 output.push(format!("| Field | {} | {} |", diff.domain_a, diff.domain_b));
1206 output.push("| --- | --- | --- |".to_string());
1207
1208 let reg = &diff.registration;
1209 output.push(format!(
1210 "| Registrar | {} | {} |",
1211 reg.registrar.0.as_deref().unwrap_or(dash),
1212 reg.registrar.1.as_deref().unwrap_or(dash)
1213 ));
1214 output.push(format!(
1215 "| Organization | {} | {} |",
1216 reg.organization.0.as_deref().unwrap_or(dash),
1217 reg.organization.1.as_deref().unwrap_or(dash)
1218 ));
1219 output.push(format!(
1220 "| Created | {} | {} |",
1221 reg.created.0.as_deref().unwrap_or(dash),
1222 reg.created.1.as_deref().unwrap_or(dash)
1223 ));
1224 output.push(format!(
1225 "| Expires | {} | {} |",
1226 reg.expires.0.as_deref().unwrap_or(dash),
1227 reg.expires.1.as_deref().unwrap_or(dash)
1228 ));
1229
1230 output.push(String::new());
1232 output.push("### DNS".to_string());
1233 output.push(String::new());
1234 output.push(format!("| Field | {} | {} |", diff.domain_a, diff.domain_b));
1235 output.push("| --- | --- | --- |".to_string());
1236 let dns = &diff.dns;
1237 output.push(format!(
1238 "| Resolves | {} | {} |",
1239 if dns.resolves.0 { "yes" } else { "no" },
1240 if dns.resolves.1 { "yes" } else { "no" }
1241 ));
1242 let a_recs_a = if dns.a_records.0.is_empty() {
1243 dash.to_string()
1244 } else {
1245 format!("`{}`", dns.a_records.0.join("`, `"))
1246 };
1247 let a_recs_b = if dns.a_records.1.is_empty() {
1248 dash.to_string()
1249 } else {
1250 format!("`{}`", dns.a_records.1.join("`, `"))
1251 };
1252 output.push(format!("| A Records | {} | {} |", a_recs_a, a_recs_b));
1253 let ns_a = if dns.nameservers.0.is_empty() {
1254 dash.to_string()
1255 } else {
1256 format!("`{}`", dns.nameservers.0.join("`, `"))
1257 };
1258 let ns_b = if dns.nameservers.1.is_empty() {
1259 dash.to_string()
1260 } else {
1261 format!("`{}`", dns.nameservers.1.join("`, `"))
1262 };
1263 output.push(format!("| Nameservers | {} | {} |", ns_a, ns_b));
1264
1265 output.push(String::new());
1267 output.push("### SSL".to_string());
1268 output.push(String::new());
1269 output.push(format!("| Field | {} | {} |", diff.domain_a, diff.domain_b));
1270 output.push("| --- | --- | --- |".to_string());
1271 let ssl = &diff.ssl;
1272 output.push(format!(
1273 "| Issuer | {} | {} |",
1274 ssl.issuer.0.as_deref().unwrap_or(dash),
1275 ssl.issuer.1.as_deref().unwrap_or(dash)
1276 ));
1277 output.push(format!(
1278 "| Valid Until | {} | {} |",
1279 ssl.valid_until.0.as_deref().unwrap_or(dash),
1280 ssl.valid_until.1.as_deref().unwrap_or(dash)
1281 ));
1282 output.push(format!(
1283 "| Days Remaining | {} | {} |",
1284 ssl.days_remaining
1285 .0
1286 .map(|d| d.to_string())
1287 .as_deref()
1288 .unwrap_or(dash),
1289 ssl.days_remaining
1290 .1
1291 .map(|d| d.to_string())
1292 .as_deref()
1293 .unwrap_or(dash)
1294 ));
1295 output.push(format!(
1296 "| Valid | {} | {} |",
1297 ssl.is_valid
1298 .0
1299 .map(|v| if v { "yes" } else { "no" })
1300 .unwrap_or(dash),
1301 ssl.is_valid
1302 .1
1303 .map(|v| if v { "yes" } else { "no" })
1304 .unwrap_or(dash)
1305 ));
1306
1307 output.join("\n")
1308 }
1309
1310 fn format_ssl(&self, report: &crate::ssl::SslReport) -> String {
1311 let mut output = Vec::new();
1312
1313 output.push(format!("## SSL Report: {}", report.domain));
1314 output.push(String::new());
1315
1316 output.push(format!(
1317 "- **Valid**: {}",
1318 if report.is_valid { "yes" } else { "no" }
1319 ));
1320 output.push(format!(
1321 "- **Days Until Expiry**: {}",
1322 report.days_until_expiry
1323 ));
1324
1325 if let Some(ref proto) = report.protocol_version {
1326 output.push(format!("- **Protocol**: {}", proto));
1327 }
1328
1329 if !report.san_names.is_empty() {
1330 output.push(format!(
1331 "- **SANs**: {}",
1332 report
1333 .san_names
1334 .iter()
1335 .map(|s| format!("`{}`", s))
1336 .collect::<Vec<_>>()
1337 .join(", ")
1338 ));
1339 }
1340
1341 if !report.chain.is_empty() {
1342 output.push(String::new());
1343 output.push("### Certificate Chain".to_string());
1344 output.push(String::new());
1345 output.push("| # | Subject | Issuer | Valid Until | Key |".to_string());
1346 output.push("| --- | --- | --- | --- | --- |".to_string());
1347 for (i, cert) in report.chain.iter().enumerate() {
1348 let key_info = match (&cert.key_type, cert.key_bits) {
1349 (Some(kt), Some(bits)) => format!("{} ({} bits)", kt, bits),
1350 (Some(kt), None) => kt.clone(),
1351 _ => "N/A".to_string(),
1352 };
1353 output.push(format!(
1354 "| {} | {} | {} | {} | {} |",
1355 i,
1356 cert.subject,
1357 cert.issuer,
1358 cert.valid_until.format("%Y-%m-%d"),
1359 key_info
1360 ));
1361 }
1362 }
1363
1364 output.join("\n")
1365 }
1366
1367 fn format_watch(&self, report: &crate::watchlist::WatchReport) -> String {
1368 let mut output = Vec::new();
1369
1370 output.push("## Domain Watch Report".to_string());
1371 output.push(String::new());
1372 output.push(format!(
1373 "- **Checked**: {}",
1374 report.checked_at.format("%Y-%m-%d %H:%M:%S UTC")
1375 ));
1376 output.push(format!(
1377 "- **Total**: {} domains, {} warnings, {} critical",
1378 report.total, report.warnings, report.critical
1379 ));
1380 output.push(String::new());
1381
1382 if report.results.is_empty() {
1383 output.push("No domains in watchlist.".to_string());
1384 return output.join("\n");
1385 }
1386
1387 output.push("| Status | Domain | SSL Days | Domain Days | HTTP | Issues |".to_string());
1388 output.push("| --- | --- | --- | --- | --- | --- |".to_string());
1389
1390 for r in &report.results {
1391 let icon = if r.issues.is_empty() { "ok" } else { "warn" };
1392 let ssl = r
1393 .ssl_days_remaining
1394 .map(|d| d.to_string())
1395 .unwrap_or_else(|| "N/A".to_string());
1396 let dom = r
1397 .domain_days_remaining
1398 .map(|d| d.to_string())
1399 .unwrap_or_else(|| "N/A".to_string());
1400 let http = r
1401 .http_status
1402 .map(|s| s.to_string())
1403 .unwrap_or_else(|| "N/A".to_string());
1404 let issues = if r.issues.is_empty() {
1405 "-".to_string()
1406 } else {
1407 r.issues.join("; ")
1408 };
1409 output.push(format!(
1410 "| {} | {} | {} | {} | {} | {} |",
1411 icon, r.domain, ssl, dom, http, issues
1412 ));
1413 }
1414
1415 output.join("\n")
1416 }
1417
1418 fn format_domain_info(&self, info: &crate::domain_info::DomainInfo) -> String {
1419 let mut output = Vec::new();
1420
1421 let source_str = match info.source {
1422 crate::domain_info::DomainInfoSource::Both => "both",
1423 crate::domain_info::DomainInfoSource::Rdap => "rdap",
1424 crate::domain_info::DomainInfoSource::Whois => "whois",
1425 crate::domain_info::DomainInfoSource::Available => "available",
1426 };
1427
1428 output.push(format!("## Domain Info: {}", info.domain));
1429 output.push(String::new());
1430 output.push(format!("**Source:** {}", source_str));
1431 output.push(String::new());
1432
1433 output.push("### Registration".to_string());
1435 output.push(String::new());
1436 output.push("| Field | Value |".to_string());
1437 output.push("| --- | --- |".to_string());
1438 output.push(format!(
1439 "| Registrar | {} |",
1440 info.registrar.as_deref().unwrap_or("-")
1441 ));
1442 output.push(format!(
1443 "| Registrant | {} |",
1444 info.registrant.as_deref().unwrap_or("-")
1445 ));
1446 output.push(format!(
1447 "| Organization | {} |",
1448 info.organization.as_deref().unwrap_or("-")
1449 ));
1450 output.push(format!(
1451 "| Created | {} |",
1452 info.creation_date
1453 .map(|d| d.format("%Y-%m-%d").to_string())
1454 .as_deref()
1455 .unwrap_or("-")
1456 ));
1457 output.push(format!(
1458 "| Expires | {} |",
1459 info.expiration_date
1460 .map(|d| d.format("%Y-%m-%d").to_string())
1461 .as_deref()
1462 .unwrap_or("-")
1463 ));
1464 output.push(format!(
1465 "| Updated | {} |",
1466 info.updated_date
1467 .map(|d| d.format("%Y-%m-%d").to_string())
1468 .as_deref()
1469 .unwrap_or("-")
1470 ));
1471 output.push(format!(
1472 "| Nameservers | {} |",
1473 if info.nameservers.is_empty() {
1474 "-".to_string()
1475 } else {
1476 info.nameservers
1477 .iter()
1478 .map(|ns| format!("`{}`", ns))
1479 .collect::<Vec<_>>()
1480 .join(", ")
1481 }
1482 ));
1483 output.push(format!(
1484 "| Status | {} |",
1485 if info.status.is_empty() {
1486 "-".to_string()
1487 } else {
1488 info.status
1489 .iter()
1490 .map(|s| format!("`{}`", s))
1491 .collect::<Vec<_>>()
1492 .join(", ")
1493 }
1494 ));
1495 output.push(format!(
1496 "| DNSSEC | {} |",
1497 info.dnssec.as_deref().unwrap_or("-")
1498 ));
1499
1500 let has_any_contact = info.registrant_email.is_some()
1502 || info.registrant_phone.is_some()
1503 || info.registrant_address.is_some()
1504 || info.registrant_country.is_some()
1505 || info.admin_name.is_some()
1506 || info.admin_organization.is_some()
1507 || info.admin_email.is_some()
1508 || info.admin_phone.is_some()
1509 || info.tech_name.is_some()
1510 || info.tech_organization.is_some()
1511 || info.tech_email.is_some()
1512 || info.tech_phone.is_some();
1513
1514 if has_any_contact {
1515 output.push(String::new());
1516 output.push("### Contacts".to_string());
1517 output.push(String::new());
1518 output.push("| Role | Name | Organization | Email | Phone |".to_string());
1519 output.push("| --- | --- | --- | --- | --- |".to_string());
1520
1521 let has_registrant = info.registrant_email.is_some()
1522 || info.registrant_phone.is_some()
1523 || info.registrant_address.is_some()
1524 || info.registrant_country.is_some();
1525 if has_registrant {
1526 output.push(format!(
1527 "| Registrant | - | - | {} | {} |",
1528 info.registrant_email.as_deref().unwrap_or("-"),
1529 info.registrant_phone.as_deref().unwrap_or("-"),
1530 ));
1531 }
1532
1533 let has_admin = info.admin_name.is_some()
1534 || info.admin_organization.is_some()
1535 || info.admin_email.is_some()
1536 || info.admin_phone.is_some();
1537 if has_admin {
1538 output.push(format!(
1539 "| Admin | {} | {} | {} | {} |",
1540 info.admin_name.as_deref().unwrap_or("-"),
1541 info.admin_organization.as_deref().unwrap_or("-"),
1542 info.admin_email.as_deref().unwrap_or("-"),
1543 info.admin_phone.as_deref().unwrap_or("-"),
1544 ));
1545 }
1546
1547 let has_tech = info.tech_name.is_some()
1548 || info.tech_organization.is_some()
1549 || info.tech_email.is_some()
1550 || info.tech_phone.is_some();
1551 if has_tech {
1552 output.push(format!(
1553 "| Tech | {} | {} | {} | {} |",
1554 info.tech_name.as_deref().unwrap_or("-"),
1555 info.tech_organization.as_deref().unwrap_or("-"),
1556 info.tech_email.as_deref().unwrap_or("-"),
1557 info.tech_phone.as_deref().unwrap_or("-"),
1558 ));
1559 }
1560 }
1561
1562 let has_metadata = info.whois_server.is_some() || info.rdap_url.is_some();
1564 if has_metadata {
1565 output.push(String::new());
1566 output.push("### Protocol Metadata".to_string());
1567 output.push(String::new());
1568 if let Some(ref whois_server) = info.whois_server {
1569 output.push(format!("- **WHOIS Server**: `{}`", whois_server));
1570 }
1571 if let Some(ref rdap_url) = info.rdap_url {
1572 output.push(format!("- **RDAP URL**: `{}`", rdap_url));
1573 }
1574 }
1575
1576 output.join("\n")
1577 }
1578}
1579
1580#[cfg(test)]
1581mod tests {
1582 use super::*;
1583 use crate::dns::RecordType;
1584 use crate::status::StatusResponse;
1585
1586 #[test]
1587 fn test_markdown_format_status() {
1588 let response = StatusResponse::new("example.com".to_string());
1589 let formatter = MarkdownFormatter::new();
1590 let output = formatter.format_status(&response);
1591 assert!(output.contains("## Status: example.com"));
1592 assert!(output.contains("### SSL Certificate"));
1593 assert!(output.contains("### DNS Resolution"));
1594 }
1595
1596 #[test]
1597 fn test_markdown_format_dns_records() {
1598 let records = vec![crate::dns::DnsRecord {
1599 name: "example.com".to_string(),
1600 record_type: RecordType::A,
1601 ttl: 300,
1602 data: crate::dns::RecordData::A {
1603 address: "93.184.216.34".to_string(),
1604 },
1605 }];
1606 let formatter = MarkdownFormatter::new();
1607 let output = formatter.format_dns(&records);
1608 assert!(output.contains("## DNS A Records: example.com"));
1609 assert!(output.contains("| Name | TTL | Type | Data |"));
1610 assert!(output.contains("93.184.216.34"));
1611 }
1612
1613 #[test]
1614 fn test_markdown_format_dns_empty() {
1615 let formatter = MarkdownFormatter::new();
1616 let output = formatter.format_dns(&[]);
1617 assert!(output.contains("No records found"));
1618 }
1619
1620 #[test]
1621 fn test_markdown_format_availability() {
1622 let result = crate::availability::AvailabilityResult {
1623 domain: "test.com".to_string(),
1624 available: true,
1625 confidence: "high".to_string(),
1626 method: "RDAP+WHOIS".to_string(),
1627 details: Some("Domain not found".to_string()),
1628 };
1629 let formatter = MarkdownFormatter::new();
1630 let output = formatter.format_availability(&result);
1631 assert!(output.contains("## Availability: test.com"));
1632 assert!(output.contains("**AVAILABLE**"));
1633 assert!(output.contains("high"));
1634 }
1635}