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 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}