Skip to main content

annotate_snippets/
level.rs

1//! [`Level`] constants for easy importing
2
3use alloc::borrow::Cow;
4
5use anstyle::Style;
6
7use crate::renderer::stylesheet::Stylesheet;
8use crate::snippet::{ERROR_TXT, HELP_TXT, INFO_TXT, NOTE_TXT, WARNING_TXT};
9use crate::{Message, OptionCow, Title};
10
11/// Default `error:` [`Level`]
12pub const ERROR: Level<'_> = Level {
13    name: None,
14    level: LevelInner::Error,
15};
16
17/// Default `warning:` [`Level`]
18pub const WARNING: Level<'_> = Level {
19    name: None,
20    level: LevelInner::Warning,
21};
22
23/// Default `info:` [`Level`]
24pub const INFO: Level<'_> = Level {
25    name: None,
26    level: LevelInner::Info,
27};
28
29/// Default `note:` [`Level`]
30pub const NOTE: Level<'_> = Level {
31    name: None,
32    level: LevelInner::Note,
33};
34
35/// Default `help:` [`Level`]
36pub const HELP: Level<'_> = Level {
37    name: None,
38    level: LevelInner::Help,
39};
40
41/// Severity level for [`Title`]s and [`Message`]s
42///
43/// # Example
44///
45/// ```rust
46/// # use annotate_snippets::*;
47/// let report = &[
48///     Level::ERROR.primary_title("mismatched types").id("E0308")
49///         .element(Level::NOTE.message("expected reference")),
50///     Group::with_title(
51///         Level::HELP.secondary_title("function defined here")
52///     ),
53/// ];
54/// ```
55#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
56pub struct Level<'a> {
57    pub(crate) name: Option<Option<Cow<'a, str>>>,
58    pub(crate) level: LevelInner,
59}
60
61/// # Constructors
62impl<'a> Level<'a> {
63    pub const ERROR: Level<'a> = ERROR;
64    pub const WARNING: Level<'a> = WARNING;
65    pub const INFO: Level<'a> = INFO;
66    pub const NOTE: Level<'a> = NOTE;
67    pub const HELP: Level<'a> = HELP;
68}
69
70impl<'a> Level<'a> {
71    /// For the primary, or root cause, [`Group`][crate::Group] (the first) in a [`Report`][crate::Report]
72    ///
73    /// See [`Group::with_title`][crate::Group::with_title]
74    ///
75    /// <div class="warning">
76    ///
77    /// Text passed to this function is considered "untrusted input", as such
78    /// all text is passed through a normalization function. Styled text is
79    /// not allowed to be passed to this function.
80    ///
81    /// </div>
82    pub fn primary_title(self, text: impl Into<Cow<'a, str>>) -> Title<'a> {
83        Title {
84            level: self,
85            id: None,
86            text: text.into(),
87            allows_styling: false,
88            is_fixable: false,
89        }
90    }
91
92    /// For any secondary, or context, [`Group`][crate::Group]s (subsequent) in a [`Report`][crate::Report]
93    ///
94    /// See [`Group::with_title`][crate::Group::with_title]
95    ///
96    /// <div class="warning">
97    ///
98    /// Text passed to this function is allowed to be styled, as such all
99    /// text is considered "trusted input" and has no normalizations applied to
100    /// it. [`normalize_untrusted_str`](crate::normalize_untrusted_str) can be
101    /// used to normalize untrusted text before it is passed to this function.
102    ///
103    /// </div>
104    pub fn secondary_title(self, text: impl Into<Cow<'a, str>>) -> Title<'a> {
105        Title {
106            level: self,
107            id: None,
108            text: text.into(),
109            allows_styling: true,
110            is_fixable: false,
111        }
112    }
113
114    /// A text [`Element`][crate::Element] in a [`Group`][crate::Group]
115    ///
116    /// <div class="warning">
117    ///
118    /// Text passed to this function is allowed to be styled, as such all
119    /// text is considered "trusted input" and has no normalizations applied to
120    /// it. [`normalize_untrusted_str`](crate::normalize_untrusted_str) can be
121    /// used to normalize untrusted text before it is passed to this function.
122    ///
123    /// </div>
124    pub fn message(self, text: impl Into<Cow<'a, str>>) -> Message<'a> {
125        Message {
126            level: self,
127            text: text.into(),
128        }
129    }
130
131    pub(crate) fn as_str(&'a self) -> &'a str {
132        match (&self.name, self.level) {
133            (Some(Some(name)), _) => name.as_ref(),
134            (Some(None), _) => "",
135            (None, LevelInner::Error) => ERROR_TXT,
136            (None, LevelInner::Warning) => WARNING_TXT,
137            (None, LevelInner::Info) => INFO_TXT,
138            (None, LevelInner::Note) => NOTE_TXT,
139            (None, LevelInner::Help) => HELP_TXT,
140        }
141    }
142
143    pub(crate) fn style(&self, stylesheet: &Stylesheet) -> Style {
144        self.level.style(stylesheet)
145    }
146}
147
148/// # Customize the `Level`
149impl<'a> Level<'a> {
150    /// Replace the name describing this [`Level`]
151    ///
152    /// <div class="warning">
153    ///
154    /// Text passed to this function is considered "untrusted input", as such
155    /// all text is passed through a normalization function. Pre-styled text is
156    /// not allowed to be passed to this function.
157    ///
158    /// </div>
159    ///
160    /// # Example
161    ///
162    /// ```rust
163    /// # #[allow(clippy::needless_doctest_main)]
164    #[doc = include_str!("../examples/custom_level.rs")]
165    /// ```
166    #[doc = include_str!("../examples/custom_level.svg")]
167    pub fn with_name(self, name: impl Into<OptionCow<'a>>) -> Level<'a> {
168        Level {
169            name: Some(name.into().0),
170            level: self.level,
171        }
172    }
173
174    /// Do not show the [`Level`]s name
175    ///
176    /// Useful for:
177    /// - Another layer of the application will include the level (e.g. when rendering errors)
178    /// - [`Message`]s that are part of a previous [`Group`][crate::Group] [`Element`][crate::Element]s
179    ///
180    /// # Example
181    ///
182    /// ```rust
183    /// # use annotate_snippets::{Group, Snippet, AnnotationKind, Level};
184    ///let source = r#"fn main() {
185    ///     let b: &[u8] = include_str!("file.txt");    //~ ERROR mismatched types
186    ///     let s: &str = include_bytes!("file.txt");   //~ ERROR mismatched types
187    /// }"#;
188    /// let report = &[
189    ///     Level::ERROR.primary_title("mismatched types").id("E0308")
190    ///         .element(
191    ///             Snippet::source(source)
192    ///                 .path("$DIR/mismatched-types.rs")
193    ///                 .annotation(
194    ///                     AnnotationKind::Primary
195    ///                         .span(105..131)
196    ///                         .label("expected `&str`, found `&[u8; 0]`"),
197    ///                 )
198    ///                 .annotation(
199    ///                     AnnotationKind::Context
200    ///                         .span(98..102)
201    ///                         .label("expected due to this"),
202    ///                 ),
203    ///         )
204    ///         .element(
205    ///             Level::NOTE
206    ///                 .no_name()
207    ///                 .message("expected reference `&str`\nfound reference `&'static [u8; 0]`"),
208    ///         ),
209    /// ];
210    /// ```
211    pub fn no_name(self) -> Level<'a> {
212        self.with_name(None::<&str>)
213    }
214}
215
216#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
217pub(crate) enum LevelInner {
218    Error,
219    Warning,
220    Info,
221    Note,
222    Help,
223}
224
225impl LevelInner {
226    pub(crate) fn style(self, stylesheet: &Stylesheet) -> Style {
227        match self {
228            LevelInner::Error => stylesheet.error,
229            LevelInner::Warning => stylesheet.warning,
230            LevelInner::Info => stylesheet.info,
231            LevelInner::Note => stylesheet.note,
232            LevelInner::Help => stylesheet.help,
233        }
234    }
235}