1use std::io::{BufRead, BufReader, BufWriter, Read, Write};
2
3use crate::highlight_scheme::HighlightScheme;
4
5pub struct Highlighter {
9 schemes: Vec<HighlightScheme>,
11 in_stream_reader: BufReader<Box<dyn Read>>,
14 out_stream_writer: BufWriter<Box<dyn Write>>,
17}
18
19impl Highlighter {
20 pub fn new(schemes: Vec<HighlightScheme>, in_stream: Box<dyn Read>, out_stream: Box<dyn Write>) -> Self {
21 let in_stream_reader = BufReader::new(in_stream);
22 let out_stream_writer = BufWriter::new(out_stream);
23 return Highlighter { schemes, in_stream_reader, out_stream_writer };
24 }
25
26 fn read_line(&mut self) -> Result<Option<String>, std::io::Error> {
28 let mut line = String::new();
29 let bytes_read = self.in_stream_reader.read_line(&mut line)?;
30 if bytes_read == 0 { return Ok(None) }
31 return Ok(Some(line.trim_end_matches('\n').to_string()));
32 }
33
34 fn process_line(&self, mut line: String) -> String {
36 for scheme in self.schemes.iter() {
37 line = scheme.format_line(&line);
38 }
39 return line;
40 }
41
42 fn write_line(&mut self, line: String) -> Result<(), std::io::Error> {
44 write!(self.out_stream_writer, "{}\n", &line)?;
45 return Ok(());
46 }
47
48 pub fn process_to_eof(&mut self) -> Result<(), std::io::Error> {
51 while let Some(line_in) = self.read_line()? {
52 self.write_line(self.process_line(line_in))?;
53 }
54 self.out_stream_writer.flush()?;
56 return Ok(());
57 }
58}
59
60impl Drop for Highlighter {
61 fn drop(&mut self) {
62 self.out_stream_writer.flush().expect("Could not flush output stream whilst cleaning up.");
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use colored::{Colorize, Styles};
71 use tempfile::tempfile;
72
73 use super::*;
74 use std::io::{empty, read_to_string, Seek};
75
76 fn process_buffer(schemes: Vec<HighlightScheme>, in_buf: &str) -> String {
77 let h = Highlighter::new(schemes, Box::new(empty()), Box::new(empty()));
78 return in_buf
79 .lines()
80 .map(|line| h.process_line(line.to_string()))
81 .fold(String::new(), |acc, x| acc + "\n" + &x).trim_start().to_string();
82 }
83
84 fn assert_proccessed_strings_or_report(expected_output: String, output: String) {
85 if expected_output == output {
86 return;
87 }
88
89 println!("encountered inequality between expected output and actual output:");
90 println!("RAW DATA:");
91 println!("expected output: {:?}", expected_output);
92 println!("actual output: {:?}", output);
93 println!("\nCOLOURISED DATA:");
94 println!("expected output: {}", expected_output);
95 println!("actual output: {}", output);
96 panic!("expected output did not equal actual output. see logs for details.");
97 }
98
99 #[test]
100 fn process_line_no_schemes_test() {
101 let schemes = vec![];
102 let input = "Hello!";
103 let expected_output = "Hello!";
104
105 let output = process_buffer(schemes, input);
106
107 assert_eq!(expected_output, output);
108 }
109
110 #[test]
111 fn process_line_single_highlight_test() {
112 let schemes = vec![
113 HighlightScheme::new(r"\[ERROR\].*", "red", Styles::Clear).expect("could not setup highlight scheme"),
114 ];
115
116 let input = "[ERROR] you need to realign the positronic matrix buffers";
117 let expected_output = "[ERROR] you need to realign the positronic matrix buffers".red().to_string();
118
119 let output = process_buffer(schemes, input);
120
121 assert_proccessed_strings_or_report(expected_output, output);
122 }
123
124 #[test]
125 fn process_line_no_matches_test() {
126 let schemes = vec![
127 HighlightScheme::new(r"\[ERROR\].*", "red", Styles::Clear).expect("could not setup highlight scheme"),
128 ];
129
130 let input = "some message that doesnt fit the match";
131 let expected_output = "some message that doesnt fit the match".to_string();
132
133 let output = process_buffer(schemes, input);
134
135 assert_proccessed_strings_or_report(expected_output, output);
136 }
137
138 #[test]
139 fn process_line_multi_line_multi_match() {
140 let schemes = vec![
141 HighlightScheme::new(r"\[ERROR\].*", "red", Styles::Clear).expect("could not setup highlight scheme"),
142 HighlightScheme::new(r"\[WARN\].*", "yellow", Styles::Clear).expect("could not setup highlight scheme"),
143 ];
144
145 let input = "[ERROR] you need to realign the positronic matrix buffers\n[WARN] the fields array is slightly out of alignment.";
146 let expected_output = format!("{}\n{}", "[ERROR] you need to realign the positronic matrix buffers".red().to_string(), &"[WARN] the fields array is slightly out of alignment.".yellow());
147
148 let output = process_buffer(schemes, input);
149
150 assert_proccessed_strings_or_report(expected_output, output);
151 }
152
153 #[test]
154 fn streams_singleline_test() {
155 let mut input_file = tempfile().unwrap();
156 let output_file = tempfile().unwrap();
157 let mut output_file_2 = output_file.try_clone().unwrap();
158
159 let input = "this is my input file.";
160
161 let expected_output = "this is my \u{1b}[32minput\u{1b}[0m \u{1b}[36mfile\u{1b}[0m.\n";
162
163 write!(input_file, "{}", input).expect("could not write test data to tmp input file.");
164 input_file.flush().expect("could not flush test data to input file");
165 input_file.rewind().expect("could not rewind input file stream");
166
167 let schemes = vec![
168 HighlightScheme::new(r"input", "green", Styles::Clear).expect("could not setup highlight scheme"),
169 HighlightScheme::new(r"file", "cyan", Styles::Clear).expect("could not setup highlight scheme"),
170 ];
171
172 let mut h = Highlighter::new(schemes, Box::new(input_file), Box::new(output_file));
173
174 h.process_to_eof().expect("error during highlighter processing");
175
176 drop(h);
177 output_file_2.rewind().expect("could not rewind to beginning of stream.");
178 let reader = BufReader::new(output_file_2);
179 let output = read_to_string(reader).expect("could not read from output tmpfile");
180 assert_proccessed_strings_or_report(expected_output.to_string(), output);
181 }
182
183 #[test]
184 fn streams_multiline_test() {
185 let mut input_file = tempfile().unwrap();
186 let output_file = tempfile().unwrap();
187 let mut output_file_2 = output_file.try_clone().unwrap();
188
189 let input = "\
190this is my input file.
191this is a second line.
192this is the final line.
193";
194
195 let expected_output = "\
196this is my input file.
197this is a \u{1b}[32msecond\u{1b}[0m line.
198this is the \u{1b}[36mfinal\u{1b}[0m line.
199";
200
201 write!(input_file, "{}", input).expect("could not write test data to tmp input file.");
202 input_file.flush().expect("could not flush test data to input file");
203 input_file.rewind().expect("could not rewind input file stream");
204
205 let schemes = vec![
206 HighlightScheme::new(r"\[ERROR\].*", "red", Styles::Clear).expect("could not setup highlight scheme"),
207 HighlightScheme::new(r"\[WARN\].*", "yellow", Styles::Clear).expect("could not setup highlight scheme"),
208 HighlightScheme::new(r"second", "green", Styles::Clear).expect("could not setup highlight scheme"),
209 HighlightScheme::new(r"final", "cyan", Styles::Clear).expect("could not setup highlight scheme"),
210 ];
211
212 let mut h = Highlighter::new(schemes, Box::new(input_file), Box::new(output_file));
213
214 h.process_to_eof().expect("error during highlighter processing");
215
216 drop(h);
217 output_file_2.rewind().expect("could not rewind to beginning of stream.");
218 let reader = BufReader::new(output_file_2);
219 let output = read_to_string(reader).expect("could not read from output tmpfile");
220 assert_proccessed_strings_or_report(expected_output.to_string(), output);
221 }
222}