hl2_lib/
highlighter.rs

1use std::io::{BufRead, BufReader, BufWriter, Read, Write};
2
3use crate::highlight_scheme::HighlightScheme;
4
5/// A struct responsible for reading from and writing to a stream,
6/// doing some processing on the text flowing between them to highlight
7/// it according to the given schemes.
8pub struct Highlighter {
9    /// Schemes to use for formatting the stream data.
10    schemes: Vec<HighlightScheme>,
11    /// a BufReader for the input stream. Can be on any type that implements Read
12    /// but is usually stdin or a file.
13    in_stream_reader: BufReader<Box<dyn Read>>,
14    /// a BufWriter for the output stream. Can be on any type that implements Write
15    /// but is usually stdout or a file.
16    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    /// read in a single line from the stream and chop any trailing newlines
27    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    /// apply scheme formatting on a single line
35    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    /// write a line to the output stream.
43    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    /// process the input stream line-by-line until EOF is reached or an error
49    /// occurs.
50    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        // flush the stream just in case
55        self.out_stream_writer.flush()?;
56        return Ok(());
57    }
58}
59
60impl Drop for Highlighter {
61    fn drop(&mut self) {
62        // when we drop our highlighter we want to be sure we actually wrote
63        // everything we finished processing to the output stream.
64        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}