diff2html/printers/
line_by_line.rs

1use std::cmp::min;
2
3use handlebars::Handlebars;
4use v_htmlescape::escape;
5
6use super::utils::{self, rematch, Difference};
7use crate::{config::Diff2HtmlConfig, parse};
8
9static GENERIC_COLUMN_LINE_NUMBER: &'static str =
10    include_str!("../templates/generic-column-line-number.hbs");
11static GENERIC_EMPTY_DIFF: &'static str = include_str!("../templates/generic-empty-diff.hbs");
12static GENERIC_FILE_PATH: &'static str = include_str!("../templates/generic-file-path.hbs");
13static GENERIC_LINE: &'static str = include_str!("../templates/generic-line.hbs");
14static GENERIC_WRAPPER: &'static str = include_str!("../templates/generic-wrapper.hbs");
15static LINE_BY_LINE_FILE_DIFF: &'static str =
16    include_str!("../templates/line-by-line-file-diff.hbs");
17static LINE_BY_LINE_NUMBERS: &'static str = include_str!("../templates/line-by-line-numbers.hbs");
18static ICON_FILE: &'static str = include_str!("../templates/icon-file.hbs");
19
20pub struct LineByLinePrinter {
21    config: Diff2HtmlConfig,
22    handlebars: Handlebars,
23    line_matcher: rematch::Rematcher<parse::Line>,
24    diff_matcher: rematch::Rematcher<Difference>,
25}
26
27impl LineByLinePrinter {
28    pub fn new(config: Diff2HtmlConfig) -> LineByLinePrinter {
29        let mut handlebars = Handlebars::new();
30        handlebars
31            .register_template_string("generic-column-line-number", GENERIC_COLUMN_LINE_NUMBER)
32            .unwrap();
33        handlebars
34            .register_template_string("generic-empty-diff", GENERIC_EMPTY_DIFF)
35            .unwrap();
36        handlebars
37            .register_template_string("generic-file-path", GENERIC_FILE_PATH)
38            .unwrap();
39        handlebars
40            .register_template_string("generic-line", GENERIC_LINE)
41            .unwrap();
42        handlebars
43            .register_template_string("generic-wrapper", GENERIC_WRAPPER)
44            .unwrap();
45        handlebars
46            .register_template_string("line-by-line-numbers", LINE_BY_LINE_NUMBERS)
47            .unwrap();
48        handlebars
49            .register_template_string("line-by-line-file-diff", LINE_BY_LINE_FILE_DIFF)
50            .unwrap();
51
52        LineByLinePrinter {
53            config: config,
54            handlebars: handlebars,
55            line_matcher: utils::get_line_matcher(),
56            diff_matcher: utils::get_difference_matcher(),
57        }
58    }
59
60    pub fn render(&self, files: &Vec<parse::File>) -> String {
61        let output = files
62            .iter()
63            .map(|file| {
64                let diffs = if file.blocks.len() > 0 {
65                    self.generate_file_html(file)
66                } else {
67                    utils::generate_empty_diff(&self.handlebars, "d2h-code-side-line")
68                };
69                self.generate_file_diff_html(file, diffs)
70            })
71            .collect::<Vec<String>>()
72            .join("\n");
73
74        self.handlebars
75            .render(
76                "generic-wrapper",
77                &json!({
78                    "content": output,
79                }),
80            )
81            .unwrap()
82    }
83
84    fn generate_file_diff_html(&self, file: &parse::File, diffs: String) -> String {
85        let file_path = self
86            .handlebars
87            .render(
88                "generic-file-path",
89                &json!({
90                    "fileDiffName": utils::get_diff_name(file),
91                    "fileIcon": ICON_FILE,
92                    "fileTag": utils::get_line_type_tag(file).to_owned(),
93                }),
94            )
95            .unwrap();
96        self.handlebars
97            .render(
98                "line-by-line-file-diff",
99                &json!({
100                    "file": file.to_owned(),
101                    "fileHtmlId": utils::get_html_id(file),
102                    "diffs": diffs,
103                    "filePath": file_path,
104                }),
105            )
106            .unwrap()
107    }
108
109    fn generate_file_html(&self, file: &parse::File) -> String {
110        file.blocks
111            .iter()
112            .map(|block| {
113                let mut lines = utils::make_column_line_number_html(
114                    &self.handlebars,
115                    block.header.as_ref().unwrap(),
116                    "d2h-code-linenumber",
117                    "d2h-code-line",
118                );
119
120                let mut old_lines = Vec::new();
121                let mut new_lines = Vec::new();
122
123                for i in 0..block.lines.len() {
124                    let line = &block.lines[i];
125                    let escaped_line = escape(&line.content).to_string();
126
127                    if line.line_type != Some(parse::LineType::Inserts)
128                        && (new_lines.len() > 0
129                            || (line.line_type != Some(parse::LineType::Deletes)
130                                && old_lines.len() > 0))
131                    {
132                        self.process_change_block(file, &mut lines, &mut old_lines, &mut new_lines);
133                    }
134
135                    if line.line_type == Some(parse::LineType::Context) {
136                        lines += &self.generate_line_html(
137                            file.is_combined,
138                            line.line_type.as_ref().unwrap(),
139                            line.old_number,
140                            line.new_number,
141                            escaped_line,
142                            None,
143                        );
144                    } else if line.line_type == Some(parse::LineType::Inserts)
145                        && old_lines.len() == 0
146                    {
147                        lines += &self.generate_line_html(
148                            file.is_combined,
149                            line.line_type.as_ref().unwrap(),
150                            line.old_number,
151                            line.new_number,
152                            escaped_line,
153                            None,
154                        );
155                    } else if line.line_type == Some(parse::LineType::Deletes) {
156                        old_lines.push(line.to_owned());
157                    } else if line.line_type == Some(parse::LineType::Inserts)
158                        && old_lines.len() > 0
159                    {
160                        new_lines.push(line.to_owned());
161                    } else {
162                        eprintln!("Unknown state in html line-by-line-generator.");
163                        self.process_change_block(file, &mut lines, &mut old_lines, &mut new_lines);
164                    }
165                }
166
167                self.process_change_block(file, &mut lines, &mut old_lines, &mut new_lines);
168
169                lines
170            })
171            .collect::<Vec<String>>()
172            .join("\n")
173    }
174
175    fn process_change_block(
176        &self,
177        file: &parse::File,
178        lines: &mut String,
179        old_lines: &mut Vec<parse::Line>,
180        new_lines: &mut Vec<parse::Line>,
181    ) {
182        let comparisons = old_lines.len() * new_lines.len();
183        let max_comparisons = 2500;
184        let do_matching = comparisons < max_comparisons && (self.config.matching != "none");
185
186        let old_lines2 = old_lines.to_owned();
187        let new_lines2 = new_lines.to_owned();
188        let (matches, insert_type, delete_type) = {
189            if do_matching {
190                (
191                    self.line_matcher.matches(&old_lines2, &new_lines2),
192                    parse::LineType::InsertChanges,
193                    parse::LineType::DeleteChanges,
194                )
195            } else {
196                (
197                    vec![vec![old_lines2.as_ref(), new_lines2.as_ref()]],
198                    parse::LineType::Inserts,
199                    parse::LineType::Deletes,
200                )
201            }
202        };
203
204        matches.iter().for_each(|item| {
205            *old_lines = item[0].to_vec();
206            *new_lines = item[1].to_vec();
207
208            let mut processed_old_lines = String::new();
209            let mut processed_new_lines = String::new();
210
211            let common = min(old_lines.len(), new_lines.len());
212
213            let mut j = 0;
214            let mut old_line;
215            let mut new_line;
216            while j < common {
217                old_line = Some(&old_lines[j]);
218                new_line = Some(&new_lines[j]);
219
220                // TODO: hmmm
221                //self.is_combined = file.is_combined;
222
223                let diff = utils::diff_highlight(
224                    &self.config,
225                    Some(&self.diff_matcher),
226                    &old_line.as_ref().unwrap().content,
227                    &new_line.as_ref().unwrap().content,
228                );
229
230                processed_old_lines += &self.generate_line_html(
231                    file.is_combined,
232                    &delete_type,
233                    old_line.as_ref().and_then(|v| v.old_number),
234                    old_line.as_ref().and_then(|v| v.new_number),
235                    diff.first.line,
236                    Some(diff.first.prefix),
237                );
238                processed_new_lines += &self.generate_line_html(
239                    file.is_combined,
240                    &insert_type,
241                    new_line.as_ref().and_then(|v| v.old_number),
242                    new_line.as_ref().and_then(|v| v.new_number),
243                    diff.second.line,
244                    Some(diff.second.prefix),
245                );
246
247                j += 1;
248            }
249
250            *lines += &processed_old_lines as &str;
251            *lines += &processed_new_lines as &str;
252
253            *lines +=
254                &self.process_lines(file.is_combined, &old_lines[common..], &new_lines[common..]);
255        });
256
257        *old_lines = Vec::new();
258        *new_lines = Vec::new();
259    }
260
261    fn generate_line_html(
262        &self,
263        is_combined: bool,
264        line_type: &parse::LineType,
265        old_number: Option<usize>,
266        new_number: Option<usize>,
267        content: String,
268        possible_prefix: Option<&str>,
269    ) -> String {
270        let line_number = self
271            .handlebars
272            .render(
273                "line-by-line-numbers",
274                &json!({
275                    "oldNumber": old_number,
276                    "newNumber": new_number,
277                }),
278            )
279            .unwrap();
280
281        let (prefix, line_without_prefix) = match possible_prefix {
282            Some(prefix) => (prefix, content),
283            _ => {
284                let line_with_prefix = utils::separate_prefix(is_combined, &content);
285                (line_with_prefix.prefix, line_with_prefix.line.to_owned())
286            }
287        };
288
289        self.handlebars
290            .render(
291                "generic-line",
292                &json!({
293                    "type": utils::get_line_type_class(line_type).to_owned(),
294                    "lineClass": "d2h-code-linenumber".to_owned(),
295                    "contentClass": "d2h-code-line".to_owned(),
296                    "prefix": prefix.to_owned(),
297                    "content": line_without_prefix,
298                    "lineNumber": line_number,
299                }),
300            )
301            .unwrap()
302    }
303
304    fn process_lines(
305        &self,
306        is_combined: bool,
307        old_lines: &[parse::Line],
308        new_lines: &[parse::Line],
309    ) -> String {
310        let mut lines = String::new();
311
312        for i in 0..old_lines.len() {
313            let old_line = &old_lines[i];
314            let old_escaped_line = escape(&old_line.content);
315            lines += &self.generate_line_html(
316                is_combined,
317                old_line.line_type.as_ref().unwrap(),
318                old_line.old_number,
319                old_line.new_number,
320                old_escaped_line.to_string(),
321                None,
322            );
323        }
324
325        for j in 0..new_lines.len() {
326            let new_line = &new_lines[j];
327            let new_escaped_line = escape(&new_line.content);
328            lines += &self.generate_line_html(
329                is_combined,
330                new_line.line_type.as_ref().unwrap(),
331                new_line.old_number,
332                new_line.new_number,
333                new_escaped_line.to_string(),
334                None,
335            );
336        }
337
338        lines
339    }
340}