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 {
25        body: &'a str,
26    },
27    Modified {
28        diff: &'a similar::TextDiff<'a, 'a, 'a, [u8]>,
29    },
30    Added {
31        body: &'a str,
32    },
33    Deleted {
34        body: &'a str,
35    },
36}
37
38impl TextPreviewTemplate<'_> {
39    fn is_equal(change: &similar::Change<&[u8]>) -> bool {
40        matches!(change.tag(), ChangeTag::Equal)
41    }
42}
43
44#[derive(Template)]
45#[template(path = "text_detail.html")]
46struct TextDetailTemplate<'a> {
47    detail: TextDetailBody<'a>,
48}
49
50enum TextDetailBody<'a> {
51    Diff {
52        lines: &'a similar::TextDiff<'a, 'a, 'a, [u8]>,
53    },
54    Single {
55        label: &'a str,
56        body: &'a str,
57    },
58}
59
60impl TextDetailBody<'_> {
61    fn is_multicolumn(&self) -> bool {
62        matches!(self, TextDetailBody::Diff { .. })
63    }
64}
65
66impl DetailReporter<TextDiff, FileLeaf, HtmlReport> for TextDiffReporter {
67    type Error = TextDiffReportError;
68
69    fn report_unchanged(
70        &self,
71        name: &str,
72        diff: TextDiff,
73        reporter: &HtmlReport,
74    ) -> Result<MayUnsupported<()>, Self::Error> {
75        let body = String::from_utf8_lossy(&diff.expected);
76        let body = body.as_ref();
77        let preview_html = TextPreviewTemplate {
78            body: TextPreviewBody::Unchanged { body },
79        };
80        let detail_html = TextDetailTemplate {
81            detail: TextDetailBody::Single { label: "same", body },
82        };
83        reporter.record_unchanged(name, COMPARES_NAME, preview_html, detail_html)?;
84        Ok(MayUnsupported::Ok(()))
85    }
86
87    fn report_modified(
88        &self,
89        name: &str,
90        diff: TextDiff,
91        reporter: &HtmlReport,
92    ) -> Result<MayUnsupported<()>, Self::Error> {
93        let diff_view = diff.diff();
94        let preview_html = TextPreviewTemplate {
95            body: TextPreviewBody::Modified { diff: &diff_view },
96        };
97        let detail_html = TextDetailTemplate {
98            detail: TextDetailBody::Diff { lines: &diff_view },
99        };
100        reporter.record_modified(name, COMPARES_NAME, preview_html, detail_html)?;
101        Ok(MayUnsupported::Ok(()))
102    }
103
104    fn report_added(
105        &self,
106        name: &str,
107        data: FileLeaf,
108        reporter: &HtmlReport,
109    ) -> Result<MayUnsupported<()>, Self::Error> {
110        if !is_text_file(&data.kind, &data.content) {
111            return Ok(MayUnsupported::Unsupported);
112        }
113        let actual_text = str::from_utf8(&data.content).expect("Invalid content");
114        let preview_html = TextPreviewTemplate {
115            body: TextPreviewBody::Added { body: actual_text },
116        };
117        let detail_html = TextDetailTemplate {
118            detail: TextDetailBody::Single {
119                label: "added",
120                body: actual_text,
121            },
122        };
123        reporter.record_added(name, COMPARES_NAME, preview_html, detail_html)?;
124        Ok(MayUnsupported::Ok(()))
125    }
126
127    fn report_deleted(
128        &self,
129        name: &str,
130        data: FileLeaf,
131        reporter: &HtmlReport,
132    ) -> Result<MayUnsupported<()>, Self::Error> {
133        if !is_text_file(&data.kind, &data.content) {
134            return Ok(MayUnsupported::Unsupported);
135        }
136        let expected_text = str::from_utf8(&data.content).expect("Invalid content");
137        let preview_html = TextPreviewTemplate {
138            body: TextPreviewBody::Deleted { body: expected_text },
139        };
140        let detail_html = TextDetailTemplate {
141            detail: TextDetailBody::Single {
142                label: "deleted",
143                body: expected_text,
144            },
145        };
146        reporter.record_deleted(name, COMPARES_NAME, preview_html, detail_html)?;
147        Ok(MayUnsupported::Ok(()))
148    }
149}