codesnake/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3#![warn(missing_docs)]
4//! Pretty printer for non-overlapping code spans.
5//!
6//! This crate aids you in creating output like the following,
7//! both for the terminal (ANSI) as well as for the web (HTML):
8//!
9//! <!-- colors taken from https://en.wikipedia.org/wiki/Solarized -->
10//! <style>
11//! pre span.red   { color: #dc322f; }
12//! pre span.green { color: #859900; }
13//! pre span.blue  { color: #268bd2; }
14//! pre span.yellow{ color: #b58900; }
15//! </style>
16//! <pre style="background-color:#002b36; color:#93a1a1; line-height:1.0; font-size:large;">
17//!   ╭─<span class=red>[fac.lisp]</span>
18//!   │
19//! 1 │   (defun <span class=green>factorial</span> (n) <span class=blue>(if (zerop n) 1</span>
20//!   ┆          <span class=green>────┬────</span>     <span class=blue>▲</span>
21//!   ┆          <span class=green>    │    </span>     <span class=blue>│</span>
22//!   ┆              <span class=green>╰─────────────────────────</span> this function ...
23//!   ┆ <span class=blue>╭──────────────────────╯</span>
24//! 2 │ <span class=blue>│</span> <span class=blue>        (* n (factorial (1- n))))</span>)<span class=yellow></span>
25//!   ┆ <span class=blue>│</span>                                 <span class=blue>▲</span> <span class=yellow>┬</span>
26//!   ┆ <span class=blue>│</span>                                 <span class=blue>│</span> <span class=yellow>│</span>
27//!   ┆ <span class=blue>╰─────────────────────────────────┴───</span> ... is defined by this
28//!   ┆                                     <span class=yellow>│</span>
29//!   ┆                                     <span class=yellow>╰─</span> (and here is EOF)
30//! ──╯
31//! </pre>
32//!
33//! This example has been created with `cargo run --example example -- --html`.
34//! To see its console output, run `cargo run --example example`.
35//!
36//! # Usage
37//!
38//! Suppose that we have a source file and a list of byte ranges that we want to annotate.
39//! For example:
40//!
41//! ~~~
42//! let src = r#"if true { 42 } else { "42" }"#;
43//! let labels = [
44//!     (8..14, "this is of type Nat"),
45//!     (20..28, "this is of type String"),
46//! ];
47//! ~~~
48//!
49//! First, we have to create a [`LineIndex`].
50//! This splits the source into lines, so that further functions can
51//! quickly find in which line a byte is situated.
52//!
53//! ~~~
54//! use codesnake::LineIndex;
55//! # let src = r#"if true { 42 } else { "42" }"#;
56//! let idx = LineIndex::new(src);
57//! ~~~
58//!
59//! Next, we create a code [`Block`] from our index and the [`Label`]s:
60//!
61//! ~~~
62//! use codesnake::{Block, Label};
63//! # use codesnake::LineIndex;
64//! # let src = r#"if true { 42 } else { "42" }"#;
65//! # let idx = LineIndex::new(src);
66//! # let labels = [(8..14, "this is of type Nat")];
67//! let block = Block::new(&idx, labels.map(|(range, text)| Label::new(range).with_text(text))).unwrap();
68//! ~~~
69//!
70//! This will fail if your labels refer to bytes outside the range of your source.
71//!
72//! Finally, we can print our code block:
73//!
74//! ~~~
75//! use codesnake::CodeWidth;
76//! # use codesnake::{Block, Label};
77//! # use codesnake::LineIndex;
78//! # let src = r#"if true { 42 } else { "42" }"#;
79//! # let idx = LineIndex::new(src);
80//! # let labels = [(8..14, "this is of type Nat")];
81//! # let block = Block::new(&idx, labels.map(|(range, text)| Label::new(range).with_text(text))).unwrap();
82//! let block = block.map_code(|c| CodeWidth::new(c, c.len()));
83//! // yield "  ╭─[main.rs]"
84//! println!("{}{}", block.prologue(), "[main.rs]");
85//! print!("{block}");
86//! // yield "──╯"
87//! println!("{}", block.epilogue());
88//! ~~~
89//!
90//! # Colors
91//!
92//! To color the output on a terminal, you can use a crate like [`yansi`](https://docs.rs/yansi).
93//! This allows you to color the snakes of a label as follows:
94//!
95//! ~~~
96//! use codesnake::Label;
97//! use yansi::Paint;
98//! # let (range, text) = (8..14, "this is of type Nat");
99//! let label = Label::new(range).with_text(text).with_style(|s| s.red().to_string());
100//! ~~~
101//!
102//! For HTML, you can use something like:
103//!
104//! ~~~
105//! use codesnake::Label;
106//! # let (range, text) = (8..14, "this is of type Nat");
107//! let label = Label::new(range).with_text(text).with_style(|s| {
108//!     format!("<span style=\"color:red\">{s}</span>")
109//! });
110//! ~~~
111
112extern crate alloc;
113
114use alloc::string::{String, ToString};
115use alloc::{boxed::Box, format, vec::Vec};
116use core::fmt::{self, Display, Formatter};
117use core::ops::Range;
118
119/// Associate byte offsets with line numbers.
120///
121/// If `idx = LineIndex::new(s)` and `idx.0[n] = (offset, line)`, then
122/// the `n`-th line of `s` starts at `offset` in `s` and equals `line`.
123pub struct LineIndex<'a>(Vec<(usize, &'a str)>);
124
125impl<'a> LineIndex<'a> {
126    /// Create a new index.
127    #[must_use]
128    pub fn new(s: &'a str) -> Self {
129        // indices of '\n' characters
130        let newlines: Vec<_> = s
131            .char_indices()
132            .filter_map(|(i, c)| (c == '\n').then_some(i))
133            .collect();
134        // indices of line starts and ends
135        let starts = core::iter::once(0).chain(newlines.iter().map(|i| *i + 1));
136        let ends = newlines.iter().copied().chain(core::iter::once(s.len()));
137
138        let lines = starts.zip(ends).map(|(start, end)| (start, &s[start..end]));
139        Self(lines.collect())
140    }
141
142    fn get(&self, offset: usize) -> Option<IndexEntry> {
143        use core::cmp::Ordering;
144        let line_no = self.0.binary_search_by(|(line_start, line)| {
145            if *line_start > offset {
146                Ordering::Greater
147            } else if line_start + line.len() < offset {
148                Ordering::Less
149            } else {
150                Ordering::Equal
151            }
152        });
153        let line_no = line_no.ok()?;
154        let (line_start, line) = self.0[line_no];
155        Some(IndexEntry {
156            line_no,
157            line,
158            bytes: offset - line_start,
159        })
160    }
161}
162
163#[derive(Debug)]
164struct IndexEntry<'a> {
165    line: &'a str,
166    line_no: usize,
167    bytes: usize,
168}
169
170/// Code label with text and style.
171pub struct Label<C, T> {
172    code: C,
173    text: Option<T>,
174    style: Box<Style>,
175}
176
177impl<T> Label<Range<usize>, T> {
178    /// Create a new label.
179    ///
180    /// If the range start equals the range end,
181    /// an arrow is drawn at the range start.
182    /// This can be useful to indicate errors that occur at the end of the input.
183    #[must_use]
184    pub fn new(code: Range<usize>) -> Self {
185        Self {
186            code,
187            text: None,
188            style: Box::new(|s| s),
189        }
190    }
191}
192
193impl<C, T> Label<C, T> {
194    /// Provide text for the label.
195    pub fn with_text(self, text: T) -> Self {
196        Self {
197            code: self.code,
198            text: Some(text),
199            style: self.style,
200        }
201    }
202
203    /// Use a custom style for drawing the label's snake.
204    #[must_use]
205    pub fn with_style(self, style: impl Fn(String) -> String + 'static) -> Self {
206        Self {
207            code: self.code,
208            text: self.text,
209            style: Box::new(style),
210        }
211    }
212}
213
214/// Piece of code together with its display width.
215pub struct CodeWidth<C> {
216    code: C,
217    width: usize,
218}
219
220impl<C> CodeWidth<C> {
221    /// Create a new piece of code with associated display width.
222    pub fn new(code: C, width: usize) -> Self {
223        CodeWidth { code, width }
224    }
225
226    /// Width to the left and right of the center (excluding the center itself).
227    fn left_right(&self) -> (usize, usize) {
228        let left = self.width / 2;
229        let right = self.width.saturating_sub(left + 1);
230        (left, right)
231    }
232}
233
234impl<C: Display> Display for CodeWidth<C> {
235    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
236        self.code.fmt(f)
237    }
238}
239
240type Style = dyn Fn(String) -> String;
241
242/// Sequence of lines, containing code `C` and (label) text `T`.
243pub struct Block<C, T>(Vec<(usize, Parts<C, T>)>);
244
245type TextStyle<T> = (Option<T>, Box<Style>);
246
247/// Line parts, containing code `C` and (label) text `T`.
248struct Parts<C, T> {
249    incoming: Option<(C, Option<T>)>,
250    inside: Vec<(C, Option<TextStyle<T>>)>,
251    outgoing: Option<(C, Box<Style>)>,
252}
253
254impl<C, T> Default for Parts<C, T> {
255    fn default() -> Self {
256        Self {
257            incoming: None,
258            inside: Vec::new(),
259            outgoing: None,
260        }
261    }
262}
263
264impl<'a, T> Block<&'a str, T> {
265    /// Create a new block.
266    ///
267    /// Given a sequence of labels, find all input lines that are touched by the labels.
268    ///
269    /// The label ranges `r` must fulfill the following conditions:
270    ///
271    /// * `r.start <= r.end`.
272    /// * If the length of the string that was used to construct `idx` is `len`, then
273    ///   `r.start <= len` and `r.end <= len`.
274    /// * For any two subsequent labels with ranges `r1` and `r2`,
275    ///   `r1.start < r2.start` and `r1.end <= r2.start`.
276    ///
277    /// If any of these conditions is not fulfilled, this function returns `None`.
278    pub fn new<I>(idx: &'a LineIndex, labels: I) -> Option<Self>
279    where
280        I: IntoIterator<Item = Label<Range<usize>, T>>,
281    {
282        let mut prev_range: Option<Range<_>> = None;
283        let mut lines = Vec::new();
284        for label in labels {
285            if label.code.start > label.code.end {
286                return None;
287            }
288            if let Some(prev) = prev_range.replace(label.code.clone()) {
289                if label.code.start <= prev.start || label.code.start < prev.end {
290                    return None;
291                }
292            }
293            let start = idx.get(label.code.start)?;
294            let end = idx.get(label.code.end)?;
295            debug_assert!(start.line_no <= end.line_no);
296
297            let mut parts = match lines.pop() {
298                Some((line_no, _line, parts)) if line_no == start.line_no => parts,
299                Some(line) => {
300                    lines.push(line);
301                    Parts::default()
302                }
303                None => Parts::default(),
304            };
305
306            if start.line_no == end.line_no {
307                let label = (label.text, label.style);
308                parts.inside.push((start.bytes..end.bytes, Some(label)));
309                lines.push((start.line_no, start.line, parts));
310            } else {
311                parts.outgoing = Some((start.bytes..start.line.len(), label.style));
312                lines.push((start.line_no, start.line, parts));
313                for line_no in start.line_no + 1..end.line_no {
314                    let line = idx.0[line_no].1;
315                    let parts = Parts {
316                        inside: Vec::from([(0..line.len(), None)]),
317                        ..Default::default()
318                    };
319                    lines.push((line_no, line, parts));
320                }
321                let parts = Parts {
322                    incoming: Some((0..end.bytes, label.text)),
323                    ..Default::default()
324                };
325                lines.push((end.line_no, end.line, parts));
326            }
327        }
328
329        let block = lines
330            .into_iter()
331            .map(|(line_no, line, parts)| (line_no, parts.segment(line)));
332        Some(Block(block.collect()))
333    }
334}
335
336/// Line that precedes a block.
337pub struct Prologue(usize);
338/// Line that succeeds a block.
339pub struct Epilogue(usize);
340
341impl<C, T> Block<C, T> {
342    /// Apply function to code.
343    #[must_use]
344    pub fn map_code<C1>(self, mut f: impl FnMut(C) -> C1) -> Block<C1, T> {
345        let lines = self.0.into_iter();
346        let lines = lines.map(|(no, parts)| (no, parts.map_code(&mut f)));
347        Block(lines.collect())
348    }
349
350    fn some_incoming(&self) -> bool {
351        self.0.iter().any(|(.., parts)| parts.incoming.is_some())
352    }
353
354    fn line_no_width(&self) -> usize {
355        let max = self.0.last().unwrap().0 + 1;
356        // number of digits; taken from https://stackoverflow.com/a/69302957
357        core::iter::successors(Some(max), |&n| (n >= 10).then_some(n / 10)).count()
358    }
359
360    /// Return the line that precedes the block.
361    #[must_use]
362    pub fn prologue(&self) -> Prologue {
363        Prologue(self.line_no_width())
364    }
365
366    /// Return the line that succeeds the block.
367    #[must_use]
368    pub fn epilogue(&self) -> Epilogue {
369        Epilogue(self.line_no_width())
370    }
371}
372
373impl Display for Prologue {
374    /// " ... ╭─"
375    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
376        write!(
377            f,
378            "{} {}{}",
379            " ".repeat(self.0),
380            Snake::UpRight,
381            Snake::Horizontal
382        )
383    }
384}
385
386impl Display for Epilogue {
387    /// "─...─╯"
388    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
389        write!(f, "{}", Snake::line_up(self.0 + 1))
390    }
391}
392
393impl<C: Display, T: Display> Display for Block<CodeWidth<C>, T> {
394    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
395        let line_no_width = self.line_no_width();
396        // " ...  │"
397        writeln!(f, "{} {}", " ".repeat(line_no_width), Snake::Vertical)?;
398        // " ...  ┆"
399        let dots =
400            |f: &mut Formatter| write!(f, "{} {}", " ".repeat(line_no_width), Snake::VerticalDots);
401        let incoming_space =
402            |f: &mut Formatter| write!(f, "{}", if self.some_incoming() { "  " } else { "" });
403
404        let mut incoming_style: Option<&Style> = None;
405
406        for (line_no, parts) in &self.0 {
407            write!(f, "{:line_no_width$} │", line_no + 1)?;
408            if let Some(style) = incoming_style {
409                write!(f, " {}", style(Snake::Vertical.to_string()))?;
410            } else {
411                incoming_space(f)?;
412            }
413
414            write!(f, " ")?;
415            parts.fmt_code(incoming_style, f)?;
416            writeln!(f)?;
417
418            dots(f)?;
419            write!(f, " ")?;
420            if let Some(style) = incoming_style {
421                style(Snake::Vertical.to_string()).fmt(f)?;
422                parts.fmt_incoming(style, Snake::ArrowUp, f)?;
423            } else {
424                incoming_space(f)?;
425            }
426            parts.fmt_arrows(f)?;
427            writeln!(f)?;
428
429            if let Some((code, text)) = &parts.incoming {
430                let style = incoming_style.take().unwrap();
431
432                dots(f)?;
433                write!(f, " ")?;
434                style(Snake::Vertical.to_string()).fmt(f)?;
435                parts.fmt_incoming(style, Snake::Vertical, f)?;
436                parts.fmt_inside_vert(0, f)?;
437                writeln!(f)?;
438
439                dots(f)?;
440                write!(f, " ")?;
441                if let Some(text) = text {
442                    let snake =
443                        Snake::down_line_up_line(code.width, parts.width() + 1 - code.width);
444                    write!(f, "{} {}", style(snake), text)?;
445                } else {
446                    let snake = Snake::down_line_up(code.width);
447                    write!(f, "{}", style(snake))?;
448                    parts.fmt_inside_vert(0, f)?;
449                }
450                writeln!(f)?;
451            }
452
453            let incoming_width = width(&parts.incoming);
454            let mut before = 0;
455            let prefix = |f: &mut _| {
456                dots(f)?;
457                incoming_space(f)?;
458                write!(f, " ")?;
459                " ".repeat(incoming_width).fmt(f)?;
460                Ok(())
461            };
462            for (i, (code, text_style)) in parts.inside.iter().enumerate() {
463                if let Some((Some(text), style)) = text_style {
464                    prefix(f)?;
465                    parts.fmt_inside_vert(i, f)?;
466                    writeln!(f)?;
467
468                    prefix(f)?;
469
470                    // print something like "╰─...─ text"
471                    let (left, right) = code.left_right();
472                    let after = width(&parts.inside) - before - code.width + width(&parts.outgoing);
473                    let snake = Snake::down_line(right + after + 1);
474                    write!(f, "{}{} {}", " ".repeat(before + left), style(snake), text)?;
475                    writeln!(f)?;
476                }
477                before += code.width;
478            }
479
480            if let Some((_, style)) = &parts.outgoing {
481                dots(f)?;
482                write!(f, " ")?;
483                style(Snake::up_line_up(incoming_width + width(&parts.inside) + 1)).fmt(f)?;
484                writeln!(f)?;
485
486                incoming_style = Some(style);
487            }
488        }
489        Ok(())
490    }
491}
492
493impl<C: Display, T> Parts<C, T> {
494    fn fmt_code(&self, mut incoming: Option<&Style>, f: &mut Formatter) -> fmt::Result {
495        if let Some((code, _text)) = &self.incoming {
496            let style = incoming.take().unwrap();
497            write!(f, "{}", style(code.to_string()))?;
498        }
499
500        for (code, label) in &self.inside {
501            match (label, incoming) {
502                (Some((_text, style)), _) => write!(f, "{}", style(code.to_string()))?,
503                (None, Some(style)) => write!(f, "{}", style(code.to_string()))?,
504                (None, None) => write!(f, "{code}")?,
505            }
506        }
507        if let Some((code, style)) = &self.outgoing {
508            write!(f, "{}", style(code.to_string()))?;
509        }
510        Ok(())
511    }
512}
513
514impl<T> Parts<Range<usize>, T> {
515    fn segment(self, line: &str) -> Parts<&str, T> {
516        let len = line.len();
517        let start = self.incoming.as_ref().map_or(0, |(code, _)| code.end);
518        let end = self.outgoing.as_ref().map_or(len, |(code, _)| code.start);
519        let last = self.inside.last().map_or(start, |(code, _)| code.end);
520
521        let mut pos = start;
522        let unlabelled = |start, end| (start < end).then(|| (&line[start..end], None));
523        let inside = self.inside.into_iter().flat_map(|(code, label)| {
524            let unlabelled = unlabelled(pos, code.start);
525            let labelled = (&line[code.start..code.end], label);
526            pos = code.end;
527            unlabelled.into_iter().chain([labelled])
528        });
529        Parts {
530            incoming: self.incoming.map(|(code, text)| (&line[..code.end], text)),
531            inside: inside.chain(unlabelled(last, end)).collect(),
532            outgoing: self.outgoing.map(|(code, sty)| (&line[code.start..], sty)),
533        }
534    }
535}
536
537impl<C, T> Parts<C, T> {
538    #[must_use]
539    fn map_code<C1>(self, mut f: impl FnMut(C) -> C1) -> Parts<C1, T> {
540        let inside = self.inside.into_iter();
541        Parts {
542            incoming: self.incoming.map(|(code, text)| (f(code), text)),
543            inside: inside.map(|(code, label)| (f(code), label)).collect(),
544            outgoing: self.outgoing.map(|(code, style)| (f(code), style)),
545        }
546    }
547}
548
549fn width<'a, C: 'a, T: 'a>(i: impl IntoIterator<Item = &'a (CodeWidth<C>, T)>) -> usize {
550    i.into_iter().map(|(code, _)| code.width).sum()
551}
552
553impl<C, T> Parts<CodeWidth<C>, T> {
554    /// Position of the end of the rightmost label.
555    fn width(&self) -> usize {
556        width(&self.inside) + width(&self.incoming) + width(&self.outgoing)
557    }
558
559    fn fmt_incoming(&self, style: &Style, snake: Snake, f: &mut Formatter) -> fmt::Result {
560        if let Some((code, _text)) = &self.incoming {
561            write!(f, "{}{}", " ".repeat(code.width), style(snake.to_string()))?;
562        }
563        Ok(())
564    }
565
566    fn fmt_inside<S1, S2>(&self, from: usize, s1: S1, s2: S2, f: &mut Formatter) -> fmt::Result
567    where
568        // display line for label without text
569        S1: Fn(usize) -> String,
570        // display line for label with text
571        S2: Fn(usize, usize) -> String,
572    {
573        let before = width(&self.inside[..from]);
574        write!(f, "{}", " ".repeat(before))?;
575
576        for (code, label) in &self.inside[from..] {
577            match label {
578                Some((Some(_text), style)) => {
579                    let (left, right) = code.left_right();
580                    style(s2(left, right)).fmt(f)?
581                }
582                Some((None, style)) => style(s1(code.width)).fmt(f)?,
583                None => " ".repeat(code.width).fmt(f)?,
584            }
585        }
586        Ok(())
587    }
588
589    /// Print something like "... ─┬─ ... ─┬─ ... ▲".
590    fn fmt_arrows(&self, f: &mut Formatter) -> fmt::Result {
591        self.fmt_inside(0, Snake::line, Snake::line_down_line, f)?;
592
593        if let Some((_code, style)) = &self.outgoing {
594            style(Snake::ArrowUp.to_string()).fmt(f)?;
595        }
596        Ok(())
597    }
598
599    /// Print something like "... │ ...  │"
600    fn fmt_inside_vert(&self, from: usize, f: &mut Formatter) -> fmt::Result {
601        let s1 = |w| " ".repeat(w).to_string();
602        let s2 = |l, r| format!("{}{}{}", " ".repeat(l), Snake::Vertical, " ".repeat(r));
603        self.fmt_inside(from, s1, s2, f)?;
604
605        if let Some((_code, style)) = &self.outgoing {
606            write!(f, "{}", style(Snake::Vertical.to_string()))?;
607        }
608        Ok(())
609    }
610}
611
612/// Parts used to draw code spans and lines.
613#[derive(Copy, Clone)]
614enum Snake {
615    /// "─"
616    Horizontal,
617    /// "│"
618    Vertical,
619    /// "┆"
620    VerticalDots,
621    /// "╭"
622    UpRight,
623    /// "╯"
624    RightUp,
625    /// "╰"
626    DownRight,
627    /// "▲"
628    ArrowUp,
629    /// "┴"
630    HorizontalUp,
631    /// "┬"
632    HorizontalDown,
633}
634
635impl Snake {
636    /// ─...─
637    fn line(len: usize) -> String {
638        "─".repeat(len)
639    }
640
641    /// ╰─...─
642    fn down_line(len: usize) -> String {
643        format!("{}{}", Snake::DownRight, Snake::line(len))
644    }
645
646    /// ╰─...─┴─...─
647    fn down_line_up_line(l: usize, r: usize) -> String {
648        format!(
649            "{}{}{}{}",
650            Self::DownRight,
651            Self::line(l),
652            Self::HorizontalUp,
653            Self::line(r)
654        )
655    }
656
657    /// "╰─...─╯"
658    fn down_line_up(len: usize) -> String {
659        format!("{}{}{}", Self::DownRight, Self::line(len), Self::RightUp)
660    }
661
662    /// "╭─...─╯"
663    fn up_line_up(len: usize) -> String {
664        format!("{}{}{}", Self::UpRight, Self::line(len), Self::RightUp)
665    }
666
667    /// "─...─╯"
668    fn line_up(len: usize) -> String {
669        format!("{}{}", Self::line(len), Self::RightUp)
670    }
671
672    /// ─...─┬─...─
673    fn line_down_line(l: usize, r: usize) -> String {
674        format!("{}{}{}", Self::line(l), Self::HorizontalDown, Self::line(r))
675    }
676}
677
678impl Display for Snake {
679    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
680        match self {
681            Self::Horizontal => "─",
682            Self::Vertical => "│",
683            Self::VerticalDots => "┆",
684            Self::UpRight => "╭",
685            Self::RightUp => "╯",
686            Self::DownRight => "╰",
687            Self::ArrowUp => "▲",
688            Self::HorizontalUp => "┴",
689            Self::HorizontalDown => "┬",
690        }
691        .fmt(f)
692    }
693}