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}