Skip to main content

semdiff_differ_text/
report_html.rs

1use crate::{TextDiff, TextDiffReporter, is_text_file};
2use askama::Template;
3use semdiff_core::fs::FileLeaf;
4use semdiff_core::{DetailReporter, MayUnsupported};
5use semdiff_output::html::{HtmlReport, HtmlReportError};
6use similar::ChangeTag;
7use thiserror::Error;
8
9const COMPARES_NAME: &str = "text";
10
11#[derive(Debug, Error)]
12pub enum TextDiffReportError {
13    #[error("html report error: {0}")]
14    HtmlReport(#[from] HtmlReportError),
15}
16
17#[derive(Template)]
18#[template(path = "text_preview.html")]
19struct TextPreviewTemplate<'a> {
20    body: TextPreviewBody<'a>,
21}
22
23enum TextPreviewBody<'a> {
24    Unchanged { body: &'a str },
25    Modified { diff: &'a similar::TextDiff<'a, 'a, [u8]> },
26    Added { body: &'a str },
27    Deleted { body: &'a str },
28}
29
30impl TextPreviewTemplate<'_> {
31    fn is_equal(change: &similar::Change<&[u8]>) -> bool {
32        matches!(change.tag(), ChangeTag::Equal)
33    }
34}
35
36#[derive(Template)]
37#[template(path = "text_detail.html")]
38struct TextDetailTemplate<'a> {
39    detail: TextDetailBody<'a>,
40}
41
42enum TextDetailBody<'a> {
43    Diff { lines: &'a similar::TextDiff<'a, 'a, [u8]> },
44    Single { label: &'a str, body: &'a str },
45}
46
47impl TextDetailBody<'_> {
48    fn is_multicolumn(&self) -> bool {
49        matches!(self, TextDetailBody::Diff { .. })
50    }
51}
52
53impl DetailReporter<TextDiff, FileLeaf, HtmlReport> for TextDiffReporter {
54    type Error = TextDiffReportError;
55
56    fn report_unchanged(
57        &self,
58        name: &str,
59        diff: &TextDiff,
60        reporter: &HtmlReport,
61    ) -> Result<MayUnsupported<()>, Self::Error> {
62        let body = String::from_utf8_lossy(&diff.expected);
63        let body = body.as_ref();
64        let preview_html = TextPreviewTemplate {
65            body: TextPreviewBody::Unchanged { body },
66        };
67        let detail_html = TextDetailTemplate {
68            detail: TextDetailBody::Single { label: "same", body },
69        };
70        reporter.record_unchanged(name, COMPARES_NAME, preview_html, detail_html)?;
71        Ok(MayUnsupported::Ok(()))
72    }
73
74    fn report_modified(
75        &self,
76        name: &str,
77        diff: &TextDiff,
78        reporter: &HtmlReport,
79    ) -> Result<MayUnsupported<()>, Self::Error> {
80        let diff_view = diff.diff();
81        let preview_html = TextPreviewTemplate {
82            body: TextPreviewBody::Modified { diff: &diff_view },
83        };
84        let detail_html = TextDetailTemplate {
85            detail: TextDetailBody::Diff { lines: &diff_view },
86        };
87        reporter.record_modified(name, COMPARES_NAME, preview_html, detail_html)?;
88        Ok(MayUnsupported::Ok(()))
89    }
90
91    fn report_added(
92        &self,
93        name: &str,
94        data: &FileLeaf,
95        reporter: &HtmlReport,
96    ) -> Result<MayUnsupported<()>, Self::Error> {
97        if !is_text_file(&data.kind, &data.content) {
98            return Ok(MayUnsupported::Unsupported);
99        }
100        let actual_text = str::from_utf8(&data.content).expect("Invalid content");
101        let preview_html = TextPreviewTemplate {
102            body: TextPreviewBody::Added { body: actual_text },
103        };
104        let detail_html = TextDetailTemplate {
105            detail: TextDetailBody::Single {
106                label: "added",
107                body: actual_text,
108            },
109        };
110        reporter.record_added(name, COMPARES_NAME, preview_html, detail_html)?;
111        Ok(MayUnsupported::Ok(()))
112    }
113
114    fn report_deleted(
115        &self,
116        name: &str,
117        data: &FileLeaf,
118        reporter: &HtmlReport,
119    ) -> Result<MayUnsupported<()>, Self::Error> {
120        if !is_text_file(&data.kind, &data.content) {
121            return Ok(MayUnsupported::Unsupported);
122        }
123        let expected_text = str::from_utf8(&data.content).expect("Invalid content");
124        let preview_html = TextPreviewTemplate {
125            body: TextPreviewBody::Deleted { body: expected_text },
126        };
127        let detail_html = TextDetailTemplate {
128            detail: TextDetailBody::Single {
129                label: "deleted",
130                body: expected_text,
131            },
132        };
133        reporter.record_deleted(name, COMPARES_NAME, preview_html, detail_html)?;
134        Ok(MayUnsupported::Ok(()))
135    }
136}