]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | //! DisplayListFormatter is a module handling the formatting of a |
2 | //! `DisplayList` into a formatted string. | |
3 | //! | |
4 | //! Besides formatting into a string it also uses a `style::Stylesheet` to | |
5 | //! provide additional styling like colors and emphasis to the text. | |
6 | ||
7 | pub mod style; | |
8 | ||
9 | use self::style::{Style, StyleClass, Stylesheet}; | |
10 | use crate::display_list::*; | |
11 | use std::cmp; | |
12 | ||
13 | use crate::stylesheets::no_color::NoColorStylesheet; | |
14 | #[cfg(feature = "ansi_term")] | |
15 | use crate::stylesheets::color::AnsiTermStylesheet; | |
16 | ||
17 | fn repeat_char(c: char, n: usize) -> String { | |
18 | let mut s = String::with_capacity(c.len_utf8()); | |
19 | s.push(c); | |
20 | s.repeat(n) | |
21 | } | |
22 | ||
23 | /// DisplayListFormatter' constructor accepts two arguments: | |
24 | /// | |
25 | /// * `color` allows the formatter to optionally apply colors and emphasis | |
26 | /// using the `ansi_term` crate. | |
27 | /// * `anonymized_line_numbers` will replace line numbers in the left column with the text `LL`. | |
28 | /// | |
29 | /// Example: | |
30 | /// | |
31 | /// ``` | |
32 | /// use annotate_snippets::formatter::DisplayListFormatter; | |
33 | /// use annotate_snippets::display_list::{DisplayList, DisplayLine, DisplaySourceLine}; | |
34 | /// | |
35 | /// let dlf = DisplayListFormatter::new(false, false); // Don't use colors, Don't anonymize line numbers | |
36 | /// | |
37 | /// let dl = DisplayList { | |
38 | /// body: vec![ | |
39 | /// DisplayLine::Source { | |
40 | /// lineno: Some(192), | |
41 | /// inline_marks: vec![], | |
42 | /// line: DisplaySourceLine::Content { | |
43 | /// text: "Example line of text".into(), | |
44 | /// range: (0, 21) | |
45 | /// } | |
46 | /// } | |
47 | /// ] | |
48 | /// }; | |
49 | /// assert_eq!(dlf.format(&dl), "192 | Example line of text"); | |
50 | /// ``` | |
51 | pub struct DisplayListFormatter { | |
52 | stylesheet: Box<dyn Stylesheet>, | |
53 | anonymized_line_numbers: bool, | |
54 | } | |
55 | ||
56 | impl DisplayListFormatter { | |
57 | const ANONYMIZED_LINE_NUM: &'static str = "LL"; | |
58 | ||
59 | /// Constructor for the struct. | |
60 | /// | |
61 | /// The argument `color` selects the stylesheet depending on the user preferences and | |
62 | /// `ansi_term` crate availability. | |
63 | /// | |
64 | /// The argument `anonymized_line_numbers` will replace line numbers in the left column with | |
65 | /// the text `LL`. This can be useful to enable when running UI tests, such as in the Rust | |
66 | /// test suite. | |
67 | pub fn new(color: bool, anonymized_line_numbers: bool) -> Self { | |
68 | if color { | |
69 | Self { | |
70 | #[cfg(feature = "ansi_term")] | |
71 | stylesheet: Box::new(AnsiTermStylesheet {}), | |
72 | #[cfg(not(feature = "ansi_term"))] | |
73 | stylesheet: Box::new(NoColorStylesheet {}), | |
74 | anonymized_line_numbers, | |
75 | } | |
76 | } else { | |
77 | Self { | |
78 | stylesheet: Box::new(NoColorStylesheet {}), | |
79 | anonymized_line_numbers, | |
80 | } | |
81 | } | |
82 | } | |
83 | ||
84 | /// Formats a `DisplayList` into a String. | |
85 | pub fn format(&self, dl: &DisplayList) -> String { | |
86 | let lineno_width = dl.body.iter().fold(0, |max, line| match line { | |
87 | DisplayLine::Source { | |
88 | lineno: Some(lineno), | |
89 | .. | |
90 | } => { | |
91 | if self.anonymized_line_numbers { | |
92 | Self::ANONYMIZED_LINE_NUM.len() | |
93 | } else { | |
94 | cmp::max(lineno.to_string().len(), max) | |
95 | } | |
96 | }, | |
97 | _ => max, | |
98 | }); | |
99 | let inline_marks_width = dl.body.iter().fold(0, |max, line| match line { | |
100 | DisplayLine::Source { inline_marks, .. } => cmp::max(inline_marks.len(), max), | |
101 | _ => max, | |
102 | }); | |
103 | ||
104 | dl.body | |
105 | .iter() | |
106 | .map(|line| self.format_line(line, lineno_width, inline_marks_width)) | |
107 | .collect::<Vec<String>>() | |
108 | .join("\n") | |
109 | } | |
110 | ||
111 | fn format_annotation_type(&self, annotation_type: &DisplayAnnotationType) -> &'static str { | |
112 | match annotation_type { | |
113 | DisplayAnnotationType::Error => "error", | |
114 | DisplayAnnotationType::Warning => "warning", | |
115 | DisplayAnnotationType::Info => "info", | |
116 | DisplayAnnotationType::Note => "note", | |
117 | DisplayAnnotationType::Help => "help", | |
118 | DisplayAnnotationType::None => "", | |
119 | } | |
120 | } | |
121 | ||
122 | fn get_annotation_style(&self, annotation_type: &DisplayAnnotationType) -> Box<dyn Style> { | |
123 | self.stylesheet.get_style(match annotation_type { | |
124 | DisplayAnnotationType::Error => StyleClass::Error, | |
125 | DisplayAnnotationType::Warning => StyleClass::Warning, | |
126 | DisplayAnnotationType::Info => StyleClass::Info, | |
127 | DisplayAnnotationType::Note => StyleClass::Note, | |
128 | DisplayAnnotationType::Help => StyleClass::Help, | |
129 | DisplayAnnotationType::None => StyleClass::None, | |
130 | }) | |
131 | } | |
132 | ||
133 | fn format_label(&self, label: &[DisplayTextFragment]) -> String { | |
134 | let emphasis_style = self.stylesheet.get_style(StyleClass::Emphasis); | |
135 | label | |
136 | .iter() | |
137 | .map(|fragment| match fragment.style { | |
138 | DisplayTextStyle::Regular => fragment.content.clone(), | |
139 | DisplayTextStyle::Emphasis => emphasis_style.paint(&fragment.content), | |
140 | }) | |
141 | .collect::<Vec<String>>() | |
142 | .join("") | |
143 | } | |
144 | ||
145 | fn format_annotation( | |
146 | &self, | |
147 | annotation: &Annotation, | |
148 | continuation: bool, | |
149 | in_source: bool, | |
150 | ) -> String { | |
151 | let color = self.get_annotation_style(&annotation.annotation_type); | |
152 | let formatted_type = if let Some(ref id) = annotation.id { | |
153 | format!( | |
154 | "{}[{}]", | |
155 | self.format_annotation_type(&annotation.annotation_type), | |
156 | id | |
157 | ) | |
158 | } else { | |
159 | self.format_annotation_type(&annotation.annotation_type) | |
160 | .to_string() | |
161 | }; | |
162 | let label = self.format_label(&annotation.label); | |
163 | ||
164 | let label_part = if label.is_empty() { | |
165 | "".to_string() | |
166 | } else if in_source { | |
167 | color.paint(&format!(": {}", self.format_label(&annotation.label))) | |
168 | } else { | |
169 | format!(": {}", self.format_label(&annotation.label)) | |
170 | }; | |
171 | if continuation { | |
172 | let indent = formatted_type.len() + 2; | |
173 | return format!("{}{}", repeat_char(' ', indent), label); | |
174 | } | |
175 | if !formatted_type.is_empty() { | |
176 | format!("{}{}", color.paint(&formatted_type), label_part) | |
177 | } else { | |
178 | label | |
179 | } | |
180 | } | |
181 | ||
182 | fn format_source_line(&self, line: &DisplaySourceLine) -> Option<String> { | |
183 | match line { | |
184 | DisplaySourceLine::Empty => None, | |
185 | DisplaySourceLine::Content { text, .. } => Some(format!(" {}", text)), | |
186 | DisplaySourceLine::Annotation { | |
187 | range, | |
188 | annotation, | |
189 | annotation_type, | |
190 | annotation_part, | |
191 | } => { | |
192 | let indent_char = match annotation_part { | |
193 | DisplayAnnotationPart::Standalone => ' ', | |
194 | DisplayAnnotationPart::LabelContinuation => ' ', | |
195 | DisplayAnnotationPart::Consequitive => ' ', | |
196 | DisplayAnnotationPart::MultilineStart => '_', | |
197 | DisplayAnnotationPart::MultilineEnd => '_', | |
198 | }; | |
199 | let mark = match annotation_type { | |
200 | DisplayAnnotationType::Error => '^', | |
201 | DisplayAnnotationType::Warning => '-', | |
202 | DisplayAnnotationType::Info => '-', | |
203 | DisplayAnnotationType::Note => '-', | |
204 | DisplayAnnotationType::Help => '-', | |
205 | DisplayAnnotationType::None => ' ', | |
206 | }; | |
207 | let color = self.get_annotation_style(annotation_type); | |
208 | let indent_length = match annotation_part { | |
209 | DisplayAnnotationPart::LabelContinuation => range.1, | |
210 | DisplayAnnotationPart::Consequitive => range.1, | |
211 | _ => range.0, | |
212 | }; | |
213 | let indent = color.paint(&repeat_char(indent_char, indent_length + 1)); | |
214 | let marks = color.paint(&repeat_char(mark, range.1 - indent_length)); | |
215 | let annotation = self.format_annotation( | |
216 | annotation, | |
217 | annotation_part == &DisplayAnnotationPart::LabelContinuation, | |
218 | true, | |
219 | ); | |
220 | if annotation.is_empty() { | |
221 | return Some(format!("{}{}", indent, marks)); | |
222 | } | |
223 | Some(format!("{}{} {}", indent, marks, color.paint(&annotation))) | |
224 | } | |
225 | } | |
226 | } | |
227 | ||
228 | fn format_lineno(&self, lineno: Option<usize>, lineno_width: usize) -> String { | |
229 | match lineno { | |
230 | Some(n) => format!("{:>width$}", n, width = lineno_width), | |
231 | None => repeat_char(' ', lineno_width), | |
232 | } | |
233 | } | |
234 | ||
235 | fn format_raw_line(&self, line: &DisplayRawLine, lineno_width: usize) -> String { | |
236 | match line { | |
237 | DisplayRawLine::Origin { | |
238 | path, | |
239 | pos, | |
240 | header_type, | |
241 | } => { | |
242 | let header_sigil = match header_type { | |
243 | DisplayHeaderType::Initial => "-->", | |
244 | DisplayHeaderType::Continuation => ":::", | |
245 | }; | |
246 | let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); | |
247 | ||
248 | if let Some((col, row)) = pos { | |
249 | format!( | |
250 | "{}{} {}:{}:{}", | |
251 | repeat_char(' ', lineno_width), | |
252 | lineno_color.paint(header_sigil), | |
253 | path, | |
254 | col, | |
255 | row | |
256 | ) | |
257 | } else { | |
258 | format!( | |
259 | "{}{} {}", | |
260 | repeat_char(' ', lineno_width), | |
261 | lineno_color.paint(header_sigil), | |
262 | path | |
263 | ) | |
264 | } | |
265 | } | |
266 | DisplayRawLine::Annotation { | |
267 | annotation, | |
268 | source_aligned, | |
269 | continuation, | |
270 | } => { | |
271 | if *source_aligned { | |
272 | if *continuation { | |
273 | format!( | |
274 | "{}{}", | |
275 | repeat_char(' ', lineno_width + 3), | |
276 | self.format_annotation(annotation, *continuation, false) | |
277 | ) | |
278 | } else { | |
279 | let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); | |
280 | format!( | |
281 | "{} {} {}", | |
282 | repeat_char(' ', lineno_width), | |
283 | lineno_color.paint("="), | |
284 | self.format_annotation(annotation, *continuation, false) | |
285 | ) | |
286 | } | |
287 | } else { | |
288 | self.format_annotation(annotation, *continuation, false) | |
289 | } | |
290 | } | |
291 | } | |
292 | } | |
293 | ||
294 | fn format_line( | |
295 | &self, | |
296 | dl: &DisplayLine, | |
297 | lineno_width: usize, | |
298 | inline_marks_width: usize, | |
299 | ) -> String { | |
300 | match dl { | |
301 | DisplayLine::Source { | |
302 | lineno, | |
303 | inline_marks, | |
304 | line, | |
305 | } => { | |
306 | let lineno = if self.anonymized_line_numbers && lineno.is_some() { | |
307 | Self::ANONYMIZED_LINE_NUM.to_string() | |
308 | } else { | |
309 | self.format_lineno(*lineno, lineno_width) | |
310 | }; | |
311 | let marks = self.format_inline_marks(inline_marks, inline_marks_width); | |
312 | let lf = self.format_source_line(line); | |
313 | let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); | |
314 | ||
315 | let mut prefix = lineno_color.paint(&format!("{} |", lineno)); | |
316 | ||
317 | match lf { | |
318 | Some(lf) => { | |
319 | if !marks.is_empty() { | |
320 | prefix.push_str(&format!(" {}", marks)); | |
321 | } | |
322 | format!("{}{}", prefix, lf) | |
323 | } | |
324 | None => { | |
325 | if !marks.trim().is_empty() { | |
326 | prefix.push_str(&format!(" {}", marks)); | |
327 | } | |
328 | prefix | |
329 | } | |
330 | } | |
331 | } | |
332 | DisplayLine::Fold { inline_marks } => { | |
333 | let marks = self.format_inline_marks(inline_marks, inline_marks_width); | |
334 | let indent = lineno_width; | |
335 | if marks.trim().is_empty() { | |
336 | String::from("...") | |
337 | } else { | |
338 | format!("...{}{}", repeat_char(' ', indent), marks) | |
339 | } | |
340 | } | |
341 | DisplayLine::Raw(line) => self.format_raw_line(line, lineno_width), | |
342 | } | |
343 | } | |
344 | ||
345 | fn format_inline_marks( | |
346 | &self, | |
347 | inline_marks: &[DisplayMark], | |
348 | inline_marks_width: usize, | |
349 | ) -> String { | |
350 | format!( | |
351 | "{}{}", | |
352 | " ".repeat(inline_marks_width - inline_marks.len()), | |
353 | inline_marks | |
354 | .iter() | |
355 | .map(|mark| { | |
356 | let sigil = match mark.mark_type { | |
357 | DisplayMarkType::AnnotationThrough => "|", | |
358 | DisplayMarkType::AnnotationStart => "/", | |
359 | }; | |
360 | let color = self.get_annotation_style(&mark.annotation_type); | |
361 | color.paint(sigil) | |
362 | }) | |
363 | .collect::<Vec<String>>() | |
364 | .join(""), | |
365 | ) | |
366 | } | |
367 | } |