Skip to main content

sourceannot/
lib.rs

1#![warn(
2    rust_2018_idioms,
3    trivial_casts,
4    trivial_numeric_casts,
5    unreachable_pub,
6    unused_qualifications
7)]
8#![forbid(unsafe_code)]
9#![no_std]
10
11//! A library to render snippets of source code with annotations.
12//!
13//! This crate is meant to be used as a building block for compiler diagnostics
14//! (error reporting, warnings, lints, etc.).
15//!
16//! This crate is `#![no_std]`, but it depends on `alloc`.
17//!
18//! # Spans and positions
19//!
20//! Annotation spans are [`Range<usize>`](core::ops::Range) indices into the
21//! snippet's *source unit* sequence (see [`Snippet`]). The exact unit depends
22//! on how the snippet was built:
23//!
24//! - [`Snippet::with_utf8()`] uses **byte offsets** into the original `&str`.
25//! - [`Snippet::with_utf8_bytes()`] uses **byte offsets** into the original `&[u8]`.
26//! - [`Snippet::with_latin1()`] uses **byte offsets** into the original `&[u8]`.
27//! - [`Snippet::with_utf16_words()`] uses **16-bit word offsets** into the
28//!   original `u16` sequence.
29//! - [`Snippet::with_chars()`] uses **[`char`] indices** into the original
30//!   character sequence.
31//!
32//! These indices are *not* indices into the rendered output: some characters
33//! will be replaced with some representation (for example, tabs are replaced
34//! with spaces, some control characters are replaced, and invalid UTF-8 can
35//! be represented as `�` or `<XX>`). The library keeps the mapping so that
36//! spans still line up with what is shown.
37//!
38//! # Output flexibility
39//!
40//! Rendering is backend-agnostic: the library emits a stream of UTF-8 fragments
41//! tagged with metadata, and an [`Output`] implementation decides what to do
42//! with them.
43//!
44//! This lets you render to plain text (e.g. a [`String`](alloc::string::String)
45//! or [`PlainOutput`]), or integrate with your own styling system (terminal colors,
46//! HTML, etc.).
47//!
48//! # Cargo features
49//!
50//! - `std` (enabled by default): enables features that depend on [`std`],
51//!   currently [`PlainOutput`] for writing rendered annotations to any
52//!   [`std::io::Write`].
53//!
54//! When the `std` feature is disabled, this crate is `#![no_std]` but still
55//! depends on [`alloc`].
56//!
57//! # Example
58//!
59//! ```
60//! // Some source code
61//! let source = indoc::indoc! {r#"
62//!     fn main() {
63//!         println!("Hello, world!");
64//!     }
65//! "#};
66//!
67//! // Create the snippet
68//! let snippet = sourceannot::Snippet::with_utf8(
69//!     1,
70//!     source,
71//!     4,
72//!     sourceannot::ControlCharStyle::Codepoint,
73//!     true,
74//! );
75//!
76//! // Styles are generic over the type of the metadata that accompanies each
77//! // chunk of rendered text. In this example, we will use the following enum:
78//! #[derive(Copy, Clone, Debug, PartialEq, Eq)]
79//! enum Color {
80//!     Default,
81//!     Red,
82//!     Green,
83//!     Blue,
84//! }
85//! // If do not you need this per-chunk metadata, you can use `()` instead.
86//!
87//! // Define the styles
88//! // Use Unicode box drawing characters
89//! let main_style = sourceannot::MainStyle {
90//!     margin: Some(sourceannot::MarginStyle {
91//!         line_char: '│',
92//!         discontinuity_chars: [' ', ' ', '·'],
93//!         meta: Color::Blue,
94//!     }),
95//!     horizontal_char: '─',
96//!     vertical_char: '│',
97//!     top_vertical_char: '╭',
98//!     top_corner_char: '╭',
99//!     bottom_corner_char: '╰',
100//!     spaces_meta: Color::Default,
101//!     text_normal_meta: Color::Default,
102//!     text_alt_meta: Color::Default,
103//! };
104//!
105//! // You can use a different style for each annotation, but in
106//! // this example we will use the same style for all of them.
107//! let annot_style = sourceannot::AnnotStyle {
108//!     caret: '^',
109//!     text_normal_meta: Color::Red,
110//!     text_alt_meta: Color::Red,
111//!     line_meta: Color::Red,
112//! };
113//!
114//! // Create the annotations
115//! let mut annotations = sourceannot::Annotations::new(&snippet, &main_style);
116//!
117//! annotations.add_annotation(
118//!     0..44,
119//!     &annot_style,
120//!     vec![("this is the `main` function".into(), Color::Red)],
121//! );
122//! annotations.add_annotation(
123//!     16..24,
124//!     &annot_style,
125//!     vec![("this is a macro invocation".into(), Color::Red)],
126//! );
127//!
128//! // Render the snippet with annotations. `PlainOutput` can write to any
129//! // `std::io::Write` ignoring colors. But you could use your favorite terminal
130//! // coloring library with a wrapper that implements the `Output` trait.
131//! let max_line_no_width = annotations.max_line_no_width();
132//! annotations
133//!     .render(
134//!         max_line_no_width,
135//!         0,
136//!         0,
137//!         sourceannot::PlainOutput(std::io::stderr().lock()),
138//!     )
139//!     .expect("failed to write to stderr");
140//!
141//! // You can also render to a string, which also ignores colors.
142//! let mut rendered = String::new();
143//! annotations.render(max_line_no_width, 0, 0, &mut rendered);
144//!
145//! # assert_eq!(
146//! #     rendered,
147//! #     indoc::indoc! {r#"
148//! #         1 │ ╭ fn main() {
149//! #         2 │ │     println!("Hello, world!");
150//! #           │ │     ^^^^^^^^ this is a macro invocation
151//! #         3 │ │ }
152//! #           │ ╰─^ this is the `main` function
153//! #     "#},
154//! # );
155//! ```
156//!
157//! The output will look like this:
158//!
159//! ```text
160//! 1 │ ╭ fn main() {
161//! 2 │ │     println!("Hello, world!");
162//!   │ │     ^^^^^^^^ this is a macro invocation
163//! 3 │ │ }
164//!   │ ╰─^ this is the `main` function
165//! ```
166//!
167//! With an invalid UTF-8 source:
168//!
169//! ```
170//! // Some source code
171//! let source = indoc::indoc! {b"
172//!     fn main() {
173//!         println!(\"Hello, \xFFworld!\");
174//!     }
175//! "};
176//!
177//! // Create the snippet
178//! let snippet = sourceannot::Snippet::with_utf8_bytes(
179//!     1,
180//!     source,
181//!     4,
182//!     sourceannot::ControlCharStyle::Codepoint,
183//!     true,
184//!     sourceannot::InvalidSeqStyle::Hexadecimal,
185//!     true,
186//! );
187//!
188//! // Assume styles from the previous example...
189//! # #[derive(Copy, Clone, Debug, PartialEq, Eq)]
190//! # enum Color {
191//! #     Default,
192//! #     Red,
193//! #     Green,
194//! #     Blue,
195//! # }
196//! # let main_style = sourceannot::MainStyle {
197//! #     margin: Some(sourceannot::MarginStyle {
198//! #         line_char: '│',
199//! #         discontinuity_chars: [' ', ' ', '·'],
200//! #         meta: Color::Blue,
201//! #     }),
202//! #     horizontal_char: '─',
203//! #     vertical_char: '│',
204//! #     top_vertical_char: '╭',
205//! #     top_corner_char: '╭',
206//! #     bottom_corner_char: '╰',
207//! #     spaces_meta: Color::Default,
208//! #     text_normal_meta: Color::Default,
209//! #     text_alt_meta: Color::Default,
210//! # };
211//! # let annot_style = sourceannot::AnnotStyle {
212//! #     caret: '^',
213//! #     text_normal_meta: Color::Red,
214//! #     text_alt_meta: Color::Red,
215//! #     line_meta: Color::Red,
216//! # };
217//!
218//! let mut annotations = sourceannot::Annotations::new(&snippet, &main_style);
219//! annotations.add_annotation(
220//!     0..45,
221//!     &annot_style,
222//!     vec![("this is the `main` function".into(), Color::Red)],
223//! );
224//!
225//! // Add a span that points to the invalid UTF-8 byte.
226//! annotations.add_annotation(
227//!     33..34,
228//!     &annot_style,
229//!     vec![("this an invalid UTF-8 sequence".into(), Color::Red)],
230//! );
231//!
232//! let max_line_no_width = annotations.max_line_no_width();
233//! annotations
234//!     .render(
235//!         max_line_no_width,
236//!         0,
237//!         0,
238//!         sourceannot::PlainOutput(std::io::stderr().lock()),
239//!     )
240//!     .expect("failed to write to stderr");
241//!
242//! # let mut rendered = String::new();
243//! # annotations.render(max_line_no_width, 0, 0, &mut rendered);
244//! # assert_eq!(
245//! #     rendered,
246//! #     indoc::indoc! {r#"
247//! #         1 │ ╭ fn main() {
248//! #         2 │ │     println!("Hello, <FF>world!");
249//! #           │ │                      ^^^^ this an invalid UTF-8 sequence
250//! #         3 │ │ }
251//! #           │ ╰─^ this is the `main` function
252//! #     "#},
253//! # );
254//! ```
255//!
256//! The output will look like this:
257//!
258//! ```text
259//! 1 │ ╭ fn main() {
260//! 2 │ │     println!("Hello, <FF>world!");
261//!   │ │                      ^^^^ this an invalid UTF-8 sequence
262//! 3 │ │ }
263//!   │ ╰─^ this is the `main` function
264//! ```
265
266extern crate alloc;
267#[cfg(feature = "std")]
268extern crate std;
269
270mod annots;
271mod snippet;
272
273pub use annots::Annotations;
274pub use snippet::{
275    ControlCharStyle, InvalidSeqStyle, Snippet, SnippetBuilder, char_should_be_replaced,
276};
277
278/// Trait that consumes a rendered annotated snippet.
279///
280/// Rendering produces a stream of text fragments , each tagged with some
281/// metadata `M` that describes how that fragment should be presented (for
282/// example, a color/style).
283///
284/// You can implement this trait to plug in your preferred output backend:
285/// plain text, terminal coloring, HTML, etc.
286///
287/// `M` is an implementor-defined metadata type. You can use `()` if you do not
288/// need it.
289///
290/// # Example
291///
292/// A simple `Output` implementation that captures rendered fragments alongside
293/// their metadata:
294///
295/// ```
296/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
297/// enum Style {
298///     Normal,
299///     Emph,
300/// }
301///
302/// // Note that `Annotations::render()` takes the `Output` implementor by value,
303/// // so we need to wrap the mutable reference.
304/// struct Capture<'a>(pub &'a mut Vec<(String, Style)>);
305///
306/// impl sourceannot::Output<Style> for Capture<'_> {
307///     type Error = std::convert::Infallible;
308///
309///     fn put_str(&mut self, text: &str, meta: &Style) -> Result<(), Self::Error> {
310///         self.0.push((text.to_string(), *meta));
311///         Ok(())
312///     }
313/// }
314/// ```
315pub trait Output<M> {
316    /// Error type produced by this output backend.
317    ///
318    /// For example, it can be [`std::io::Error`] when writing to an I/O
319    /// stream, or [`std::convert::Infallible`] when the output cannot fail.
320    type Error;
321
322    /// Writes a UTF-8 text fragment with associated metadata.
323    fn put_str(&mut self, text: &str, meta: &M) -> Result<(), Self::Error>;
324
325    /// Writes a single character with associated metadata.
326    fn put_char(&mut self, ch: char, meta: &M) -> Result<(), Self::Error> {
327        self.put_str(ch.encode_utf8(&mut [0; 4]), meta)
328    }
329
330    /// Writes formatted text with associated metadata.
331    fn put_fmt(&mut self, args: core::fmt::Arguments<'_>, meta: &M) -> Result<(), Self::Error> {
332        struct Adapter<'a, M, O: ?Sized + Output<M>> {
333            output: &'a mut O,
334            meta: &'a M,
335            error: Option<O::Error>,
336        }
337
338        impl<'a, M, O: ?Sized + Output<M>> core::fmt::Write for Adapter<'a, M, O> {
339            fn write_str(&mut self, s: &str) -> core::fmt::Result {
340                self.output.put_str(s, self.meta).map_err(|e| {
341                    self.error = Some(e);
342                    core::fmt::Error
343                })
344            }
345        }
346
347        let mut writer = Adapter {
348            output: self,
349            meta,
350            error: None,
351        };
352        core::fmt::write(&mut writer, args)
353            .map_err(|_| {
354                writer
355                    .error
356                    .unwrap_or_else(|| {
357                        panic!("a formatting trait implementation returned an error when the underlying stream did not")
358                    })
359            })
360    }
361}
362
363/// Writing to a [`String`](alloc::string::String) ignores metadata.
364impl<M> Output<M> for &mut alloc::string::String {
365    type Error = core::convert::Infallible;
366
367    fn put_str(&mut self, text: &str, _meta: &M) -> Result<(), Self::Error> {
368        self.push_str(text);
369        Ok(())
370    }
371
372    fn put_char(&mut self, ch: char, _meta: &M) -> Result<(), Self::Error> {
373        self.push(ch);
374        Ok(())
375    }
376
377    fn put_fmt(&mut self, args: core::fmt::Arguments<'_>, _meta: &M) -> Result<(), Self::Error> {
378        core::fmt::write(self, args).unwrap();
379        Ok(())
380    }
381}
382
383/// An [`Output`] implementor that writes to any [`std::io::Write`] ignoring
384/// metadata.
385#[cfg(feature = "std")]
386pub struct PlainOutput<W: std::io::Write>(pub W);
387
388#[cfg(feature = "std")]
389impl<W: std::io::Write, M> Output<M> for PlainOutput<W> {
390    type Error = std::io::Error;
391
392    fn put_str(&mut self, text: &str, _meta: &M) -> Result<(), Self::Error> {
393        self.0.write_all(text.as_bytes())
394    }
395
396    fn put_char(&mut self, ch: char, _meta: &M) -> Result<(), Self::Error> {
397        let mut buf = [0; 4];
398        let s = ch.encode_utf8(&mut buf);
399        self.0.write_all(s.as_bytes())
400    }
401
402    fn put_fmt(&mut self, args: core::fmt::Arguments<'_>, _meta: &M) -> Result<(), Self::Error> {
403        self.0.write_fmt(args)
404    }
405}
406
407/// The general style of an annotated snippet.
408///
409/// This controls how the snippet and its annotations are drawn (margin,
410/// connector lines, corners) and which metadata is attached to each text
411/// fragment.
412///
413/// `M` is an output-backend-defined metadata type (often a "color/style"). It
414/// is passed through to [`Output`].
415#[derive(Copy, Clone, Debug, PartialEq, Eq)]
416pub struct MainStyle<M> {
417    /// The style of the margin.
418    ///
419    /// If `None`, there will not be any margin at all.
420    pub margin: Option<MarginStyle<M>>,
421
422    /// Character used to draw the horizontal connectors of multi-line annotations.
423    pub horizontal_char: char,
424
425    /// Character used to draw the vertical connector of multi-line annotations.
426    pub vertical_char: char,
427
428    /// Character used to draw the top corner of multi-line annotations that
429    /// start at the first column.
430    pub top_vertical_char: char,
431
432    /// Character used to draw the top corner of multi-line annotations.
433    pub top_corner_char: char,
434
435    /// Character used to draw the bottom corner of multi-line annotations.
436    pub bottom_corner_char: char,
437
438    /// Metadata that accompanies spaces.
439    ///
440    /// This is used for padding and separator spaces inserted by the renderer.
441    pub spaces_meta: M,
442
443    /// Metadata that accompanies unannotated text.
444    pub text_normal_meta: M,
445
446    /// Metadata that accompanies unannotated alternate text.
447    ///
448    /// "Alternate text" refers to replacement text emitted when the renderer
449    /// makes normally-invisible or potentially-confusing source elements
450    /// explicit (for example, certain control characters or invalid UTF-8
451    /// sequences, depending on snippet settings).
452    pub text_alt_meta: M,
453}
454
455/// The style of the margin of an annotated snippet.
456///
457/// The margin is the left-hand area that typically contains line numbers and a
458/// vertical separator.
459#[derive(Copy, Clone, Debug, PartialEq, Eq)]
460pub struct MarginStyle<M> {
461    /// Character used to draw the vertical separator of the margin.
462    pub line_char: char,
463
464    /// Characters used to draw discontinuities in the margin when intermediate
465    /// source lines are omitted.
466    pub discontinuity_chars: [char; 3],
467
468    /// Metadata that accompanies margin characters.
469    ///
470    /// This applies to line numbers as well as the margin separator glyphs.
471    pub meta: M,
472}
473
474/// The style of a particular annotation.
475///
476/// This controls the glyphs and metadata used to render a specific annotation
477/// span (carets, connector lines, and label text).
478#[derive(Copy, Clone, Debug, PartialEq, Eq)]
479pub struct AnnotStyle<M> {
480    /// Caret character used to point to the annotated text.
481    pub caret: char,
482
483    /// Metadata that accompanies annotated text.
484    pub text_normal_meta: M,
485
486    /// Metadata that accompanies annotated alternate text.
487    pub text_alt_meta: M,
488
489    /// Metadata that accompanies annotation drawings.
490    ///
491    /// This applies to carets and connector lines.
492    pub line_meta: M,
493}