Skip to main content

annotate_snippets/renderer/
mod.rs

1//! The [Renderer] and its settings
2//!
3//! # Example
4//!
5//! ```
6//! # use annotate_snippets::*;
7//! # use annotate_snippets::renderer::*;
8//! # use annotate_snippets::Level;
9//! let report = // ...
10//! # &[Group::with_title(
11//! #     Level::ERROR
12//! #         .primary_title("unresolved import `baz::zed`")
13//! #         .id("E0432")
14//! # )];
15//!
16//! let renderer = Renderer::styled().decor_style(DecorStyle::Unicode);
17//! let output = renderer.render(report);
18//! anstream::println!("{output}");
19//! ```
20
21pub(crate) mod graphics;
22pub(crate) mod no_graphics;
23pub(crate) mod source_map;
24pub(crate) mod stylesheet;
25
26mod margin;
27mod preprocess;
28mod styled_buffer;
29
30use alloc::string::String;
31
32use crate::Report;
33
34pub(crate) use graphics::ElementStyle;
35pub(crate) use graphics::UnderlineParts;
36pub(crate) use graphics::normalize_whitespace;
37pub(crate) use graphics::{LineAnnotation, LineAnnotationType, char_width, num_overlap};
38pub(crate) use stylesheet::Stylesheet;
39
40pub use anstyle::*;
41
42/// See [`Renderer::term_width`]
43pub const DEFAULT_TERM_WIDTH: usize = 140;
44
45const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors");
46const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS {
47    AnsiColor::BrightCyan.on_default()
48} else {
49    AnsiColor::BrightBlue.on_default()
50};
51/// [`Renderer::error`] applied by [`Renderer::styled`]
52pub const DEFAULT_ERROR_STYLE: Style = AnsiColor::BrightRed.on_default().effects(Effects::BOLD);
53/// [`Renderer::warning`] applied by [`Renderer::styled`]
54pub const DEFAULT_WARNING_STYLE: Style = if USE_WINDOWS_COLORS {
55    AnsiColor::BrightYellow.on_default()
56} else {
57    AnsiColor::Yellow.on_default()
58}
59.effects(Effects::BOLD);
60/// [`Renderer::info`] applied by [`Renderer::styled`]
61pub const DEFAULT_INFO_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD);
62/// [`Renderer::note`] applied by [`Renderer::styled`]
63pub const DEFAULT_NOTE_STYLE: Style = AnsiColor::BrightGreen.on_default().effects(Effects::BOLD);
64/// [`Renderer::help`] applied by [`Renderer::styled`]
65pub const DEFAULT_HELP_STYLE: Style = AnsiColor::BrightCyan.on_default().effects(Effects::BOLD);
66/// [`Renderer::line_num`] applied by [`Renderer::styled`]
67pub const DEFAULT_LINE_NUM_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD);
68/// [`Renderer::emphasis`] applied by [`Renderer::styled`]
69pub const DEFAULT_EMPHASIS_STYLE: Style = if USE_WINDOWS_COLORS {
70    AnsiColor::BrightWhite.on_default()
71} else {
72    Style::new()
73}
74.effects(Effects::BOLD);
75/// [`Renderer::none`] applied by [`Renderer::styled`]
76pub const DEFAULT_NONE_STYLE: Style = Style::new();
77/// [`Renderer::context`] applied by [`Renderer::styled`]
78pub const DEFAULT_CONTEXT_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD);
79/// [`Renderer::addition`] applied by [`Renderer::styled`]
80pub const DEFAULT_ADDITION_STYLE: Style = AnsiColor::BrightGreen.on_default();
81/// [`Renderer::removal`] applied by [`Renderer::styled`]
82pub const DEFAULT_REMOVAL_STYLE: Style = AnsiColor::BrightRed.on_default();
83
84/// The [Renderer] for a [`Report`]
85///
86/// The caller is expected to detect any relevant terminal features and configure the renderer,
87/// including
88/// - ANSI Escape code support (always outputted with [`Renderer::styled`])
89/// - Terminal width ([`Renderer::term_width`])
90/// - Unicode support ([`Renderer::decor_style`])
91///
92/// # Example
93///
94/// ```
95/// # use annotate_snippets::*;
96/// # use annotate_snippets::renderer::*;
97/// # use annotate_snippets::Level;
98/// let report = // ...
99/// # &[Group::with_title(
100/// #     Level::ERROR
101/// #         .primary_title("unresolved import `baz::zed`")
102/// #         .id("E0432")
103/// # )];
104///
105/// let renderer = Renderer::styled();
106/// let output = renderer.render(report);
107/// anstream::println!("{output}");
108/// ```
109#[derive(Clone, Debug)]
110pub struct Renderer {
111    anonymized_origin_line_numbers: bool,
112    anonymized_snippet_line_numbers: bool,
113    term_width: usize,
114    decor_style: DecorStyle,
115    stylesheet: Stylesheet,
116    hyperlink: bool,
117    short_message: bool,
118    no_graphics: bool,
119}
120
121impl Renderer {
122    /// No terminal styling
123    pub const fn plain() -> Self {
124        Self {
125            anonymized_origin_line_numbers: false,
126            anonymized_snippet_line_numbers: false,
127            term_width: DEFAULT_TERM_WIDTH,
128            decor_style: DecorStyle::Ascii,
129            stylesheet: Stylesheet::plain(),
130            hyperlink: false,
131            short_message: false,
132            no_graphics: false,
133        }
134    }
135
136    /// Default terminal styling
137    ///
138    /// If ANSI escape codes are not supported, either
139    /// - Call [`Renderer::plain`] instead
140    /// - Strip them after the fact, like with [`anstream`](https://docs.rs/anstream/latest/anstream/)
141    ///
142    /// # Note
143    ///
144    /// When testing styled terminal output, see the [`testing-colors` feature](crate#features)
145    pub const fn styled() -> Self {
146        Self {
147            stylesheet: Stylesheet {
148                error: DEFAULT_ERROR_STYLE,
149                warning: DEFAULT_WARNING_STYLE,
150                info: DEFAULT_INFO_STYLE,
151                note: DEFAULT_NOTE_STYLE,
152                help: DEFAULT_HELP_STYLE,
153                line_num: DEFAULT_LINE_NUM_STYLE,
154                emphasis: DEFAULT_EMPHASIS_STYLE,
155                none: DEFAULT_NONE_STYLE,
156                context: DEFAULT_CONTEXT_STYLE,
157                addition: DEFAULT_ADDITION_STYLE,
158                removal: DEFAULT_REMOVAL_STYLE,
159            },
160            hyperlink: true,
161            ..Self::plain()
162        }
163    }
164
165    /// Render a `Report` without source snippets or graphical decorations
166    ///
167    /// The output is primarily designed for Text-To-Speech and Braille
168    /// displays. It also provides a more detailed alternative to
169    /// [`Renderer::short_message`].
170    ///
171    /// We recommend that applications enable this format when `NO_GRAPHICS=1`
172    /// is set in the environment.
173    ///
174    /// <div class="warning">
175    ///
176    /// This format supports colors and hyperlinks.
177    ///
178    /// [`Renderer::plain`] can be used to disable colors and hyperlinks.
179    ///
180    /// </div>
181    ///
182    /// # Example output
183    ///
184    /// ```text
185    #[doc = include_str!("../../examples/no_graphics.txt")]
186    /// ```
187    pub const fn no_graphics(mut self, no_graphics: bool) -> Self {
188        self.no_graphics = no_graphics;
189        self
190    }
191
192    /// Abbreviate the message
193    pub const fn short_message(mut self, short_message: bool) -> Self {
194        self.short_message = short_message;
195        self
196    }
197
198    /// Set the width to render within
199    ///
200    /// Affects the rendering of [`Snippet`][crate::Snippet]s
201    pub const fn term_width(mut self, term_width: usize) -> Self {
202        self.term_width = term_width;
203        self
204    }
205
206    /// Set the character set used for rendering decor
207    pub const fn decor_style(mut self, decor_style: DecorStyle) -> Self {
208        self.decor_style = decor_style;
209        self
210    }
211
212    /// Anonymize snippet line numbers
213    ///
214    /// When enabled, line numbers are replaced with `LL` which is useful for tests.
215    ///
216    /// # Example
217    ///
218    /// ```text
219    ///   --> $DIR/whitespace-trimming.rs:4:193
220    ///    |
221    /// LL | ...                   let _: () = 42;
222    ///    |                                   ^^ expected (), found integer
223    ///    |
224    /// ```
225    pub const fn anonymized_snippet_line_numbers(mut self, yes: bool) -> Self {
226        self.anonymized_snippet_line_numbers = yes;
227        self
228    }
229
230    /// Deprecated, replaced with [`Self::anonymized_snippet_line_numbers`]
231    #[deprecated(
232        since = "0.12.17",
233        note = "replaced with `Renderer::anonymized_snippet_line_numbers`"
234    )]
235    pub const fn anonymized_line_numbers(mut self, yes: bool) -> Self {
236        self.anonymized_snippet_line_numbers = yes;
237        self
238    }
239
240    /// Anonymize origin line numbers
241    ///
242    /// When enabled, line numbers are replaced with `LL` which is useful for tests.
243    ///
244    /// # Example
245    ///
246    /// ```text
247    ///   --> $DIR/whitespace-trimming.rs:LL:193
248    ///   |
249    /// 4 | ...                   let _: () = 42;
250    ///   |                                   ^^ expected (), found integer
251    ///   |
252    /// ```
253    pub const fn anonymized_origin_line_numbers(mut self, yes: bool) -> Self {
254        self.anonymized_origin_line_numbers = yes;
255        self
256    }
257}
258
259impl Renderer {
260    /// Render a diagnostic [`Report`]
261    pub fn render(&self, groups: Report<'_>) -> String {
262        if self.no_graphics {
263            no_graphics::render_no_graphics(self, groups).unwrap()
264        } else {
265            graphics::render(self, groups)
266        }
267    }
268}
269
270/// Customize [`Renderer::styled`]
271impl Renderer {
272    /// Override the output style for [error][crate::Level::ERROR]
273    pub const fn error(mut self, style: Style) -> Self {
274        self.stylesheet.error = style;
275        self
276    }
277
278    /// Override the output style for [warnings][crate::Level::WARNING]
279    pub const fn warning(mut self, style: Style) -> Self {
280        self.stylesheet.warning = style;
281        self
282    }
283
284    /// Override the output style for [info][crate::Level::INFO]
285    pub const fn info(mut self, style: Style) -> Self {
286        self.stylesheet.info = style;
287        self
288    }
289
290    /// Override the output style for [notes][crate::Level::NOTE]
291    pub const fn note(mut self, style: Style) -> Self {
292        self.stylesheet.note = style;
293        self
294    }
295
296    /// Override the output style for [help][crate::Level::HELP]
297    pub const fn help(mut self, style: Style) -> Self {
298        self.stylesheet.help = style;
299        self
300    }
301
302    /// Override the output style for line numbers in the [`Snippet`][crate::Snippet] gutter
303    pub const fn line_num(mut self, style: Style) -> Self {
304        self.stylesheet.line_num = style;
305        self
306    }
307
308    /// Override the output style for emphasis for the
309    /// [`primary_title`][crate::Level::primary_title]
310    pub const fn emphasis(mut self, style: Style) -> Self {
311        self.stylesheet.emphasis = style;
312        self
313    }
314
315    /// Override the output style for [`AnnotationKind::Context`][crate::AnnotationKind::Context]
316    pub const fn context(mut self, style: Style) -> Self {
317        self.stylesheet.context = style;
318        self
319    }
320
321    /// Override the output style for [`Patch`][crate::Patch] additions
322    pub const fn addition(mut self, style: Style) -> Self {
323        self.stylesheet.addition = style;
324        self
325    }
326
327    /// Override the output style for [`Patch`][crate::Patch] removals
328    pub const fn removal(mut self, style: Style) -> Self {
329        self.stylesheet.removal = style;
330        self
331    }
332
333    /// Override the output style for all other text
334    pub const fn none(mut self, style: Style) -> Self {
335        self.stylesheet.none = style;
336        self
337    }
338
339    /// Override whether hyperlinks should be emitted
340    pub const fn hyperlink(mut self, hyperlink: bool) -> Self {
341        self.hyperlink = hyperlink;
342        self
343    }
344}
345
346/// The character set for rendering for decor
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum DecorStyle {
349    Ascii,
350    Unicode,
351}
352
353impl DecorStyle {
354    fn col_separator(&self) -> char {
355        match self {
356            Self::Ascii => '|',
357            Self::Unicode => '│',
358        }
359    }
360
361    fn note_separator(&self, is_cont: bool) -> &str {
362        match self {
363            Self::Ascii => "= ",
364            Self::Unicode if is_cont => "├ ",
365            Self::Unicode => "╰ ",
366        }
367    }
368
369    fn multi_suggestion_separator(&self) -> &'static str {
370        match self {
371            Self::Ascii => "|",
372            Self::Unicode => "├╴",
373        }
374    }
375
376    fn file_start(&self, is_first: bool, alone: bool) -> &'static str {
377        match self {
378            Self::Ascii => "--> ",
379            Self::Unicode if is_first && alone => " ─▸ ",
380            Self::Unicode if is_first => " ╭▸ ",
381            Self::Unicode => " ├▸ ",
382        }
383    }
384
385    fn secondary_file_start(&self) -> &'static str {
386        match self {
387            Self::Ascii => "::: ",
388            Self::Unicode => " ⸬  ",
389        }
390    }
391
392    fn diff(&self) -> char {
393        match self {
394            Self::Ascii => '~',
395            Self::Unicode => '±',
396        }
397    }
398
399    fn margin(&self) -> &'static str {
400        match self {
401            Self::Ascii => "...",
402            Self::Unicode => "…",
403        }
404    }
405
406    fn underline(&self, is_primary: bool) -> UnderlineParts {
407        //               X0 Y0
408        // label_start > ┯━━━━ < underline
409        //               │ < vertical_text_line
410        //               text
411
412        //    multiline_start_down ⤷ X0 Y0
413        //            top_left > ┌───╿──┘ < top_right_flat
414        //           top_left > ┏│━━━┙ < top_right
415        // multiline_vertical > ┃│
416        //                      ┃│   X1 Y1
417        //                      ┃│   X2 Y2
418        //                      ┃└────╿──┘ < multiline_end_same_line
419        //        bottom_left > ┗━━━━━┥ < bottom_right_with_text
420        //   multiline_horizontal ^   `X` is a good letter
421
422        // multiline_whole_line > ┏ X0 Y0
423        //                        ┃   X1 Y1
424        //                        ┗━━━━┛ < multiline_end_same_line
425
426        // multiline_whole_line > ┏ X0 Y0
427        //                        ┃ X1 Y1
428        //                        ┃  ╿ < multiline_end_up
429        //                        ┗━━┛ < bottom_right
430
431        match (self, is_primary) {
432            (Self::Ascii, true) => UnderlineParts {
433                style: ElementStyle::UnderlinePrimary,
434                underline: '^',
435                label_start: '^',
436                vertical_text_line: '|',
437                multiline_vertical: '|',
438                multiline_horizontal: '_',
439                multiline_whole_line: '/',
440                multiline_start_down: '^',
441                bottom_right: '|',
442                top_left: ' ',
443                top_right_flat: '^',
444                bottom_left: '|',
445                multiline_end_up: '^',
446                multiline_end_same_line: '^',
447                multiline_bottom_right_with_text: '|',
448            },
449            (Self::Ascii, false) => UnderlineParts {
450                style: ElementStyle::UnderlineSecondary,
451                underline: '-',
452                label_start: '-',
453                vertical_text_line: '|',
454                multiline_vertical: '|',
455                multiline_horizontal: '_',
456                multiline_whole_line: '/',
457                multiline_start_down: '-',
458                bottom_right: '|',
459                top_left: ' ',
460                top_right_flat: '-',
461                bottom_left: '|',
462                multiline_end_up: '-',
463                multiline_end_same_line: '-',
464                multiline_bottom_right_with_text: '|',
465            },
466            (Self::Unicode, true) => UnderlineParts {
467                style: ElementStyle::UnderlinePrimary,
468                underline: '━',
469                label_start: '┯',
470                vertical_text_line: '│',
471                multiline_vertical: '┃',
472                multiline_horizontal: '━',
473                multiline_whole_line: '┏',
474                multiline_start_down: '╿',
475                bottom_right: '┙',
476                top_left: '┏',
477                top_right_flat: '┛',
478                bottom_left: '┗',
479                multiline_end_up: '╿',
480                multiline_end_same_line: '┛',
481                multiline_bottom_right_with_text: '┥',
482            },
483            (Self::Unicode, false) => UnderlineParts {
484                style: ElementStyle::UnderlineSecondary,
485                underline: '─',
486                label_start: '┬',
487                vertical_text_line: '│',
488                multiline_vertical: '│',
489                multiline_horizontal: '─',
490                multiline_whole_line: '┌',
491                multiline_start_down: '│',
492                bottom_right: '┘',
493                top_left: '┌',
494                top_right_flat: '┘',
495                bottom_left: '└',
496                multiline_end_up: '│',
497                multiline_end_same_line: '┘',
498                multiline_bottom_right_with_text: '┤',
499            },
500        }
501    }
502}