codesnake-0.2.1/.cargo_vcs_info.json0000644000000001360000000000100127700ustar { "git": { "sha1": "f0fa8cf81232daacadee678d710565ea77c029d8" }, "path_in_vcs": "" }codesnake-0.2.1/.github/workflows/msrv.yml000064400000000000000000000003461046102023000166720ustar 00000000000000name: Build with MSRV (minimal supported Rust version) on: workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@1.62 - run: cargo check codesnake-0.2.1/.github/workflows/rust.yml000064400000000000000000000004741046102023000167020ustar 00000000000000name: Rust on: push: branches: [ "main" ] pull_request: branches: [ "main" ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose codesnake-0.2.1/.gitignore000064400000000000000000000000231046102023000135430ustar 00000000000000/target Cargo.lock codesnake-0.2.1/Cargo.lock0000644000000011130000000000100107370ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "codesnake" version = "0.2.1" dependencies = [ "unicode-width", "yansi", ] [[package]] name = "unicode-width" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" codesnake-0.2.1/Cargo.toml0000644000000024200000000000100107640ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.62" name = "codesnake" version = "0.2.1" authors = ["Michael FΓ€rber "] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Pretty printer for non-overlapping code spans" readme = "README.md" keywords = [ "error", "diagnostics", "report", "compiler", "programming-language", ] categories = [ "command-line-utilities", "compilers", "value-formatting", ] license = "MIT" repository = "https://github.com/01mf02/codesnake" [lib] name = "codesnake" path = "src/lib.rs" [[example]] name = "example" path = "examples/example.rs" [[test]] name = "tests" path = "tests/tests.rs" [dev-dependencies.unicode-width] version = "0.1.13" [dev-dependencies.yansi] version = "1.0.1" codesnake-0.2.1/Cargo.toml.orig000064400000000000000000000007521046102023000144530ustar 00000000000000[package] name = "codesnake" description = "Pretty printer for non-overlapping code spans" version = "0.2.1" authors = ["Michael FΓ€rber "] repository = "https://github.com/01mf02/codesnake" edition = "2021" rust-version = "1.62" license = "MIT" keywords = ["error", "diagnostics", "report", "compiler", "programming-language"] categories = ["command-line-utilities", "compilers", "value-formatting"] [dev-dependencies] unicode-width = "0.1.13" yansi = "1.0.1" codesnake-0.2.1/LICENSE000064400000000000000000000020601046102023000125630ustar 00000000000000MIT License Copyright (c) 2024 Michael FΓ€rber Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. codesnake-0.2.1/README.md000064400000000000000000000040271046102023000130420ustar 00000000000000# codesnake codesnake is a Rust crate that shows code blocks and labels parts of it. Its name comes from the snake-like appearance of the lines that connect code spans with their corresponding labels. 🐍 An example of its output: ![Example output](example.svg) ## Features * Uses `&str` as code input and byte positions for spans * Multiple spans within a code block * Spans may range over multiple lines * Zero dependencies While this crate does not support colored output out of the box, it is very easy to integrate it with other crates like `yansi` to produce colored output for the terminal (ANSI) and web (HTML). ## Related crates * [`ariadne`](https://crates.io/crates/ariadne): I used ariadne happily myself for quite some time, but it had [broken semantic versioning](https://github.com/zesterer/ariadne/issues/116) for at least one month now, which led to build failures in a project of mine. Furthermore, the heart of this crate is a function more than 700 lines long, of which the author himself states that it [is complex, has bugs and needs rewriting][write.rs]. codesnake can be considered to be a rewrite of ariadne, focusing on its core features. * [`codespan-reporting`](https://crates.io/crates/codespan-reporting): This seems to be the spiritual predecessor of ariadne, but it looks unfortunately unmaintained, with the last release from 2021. * [`miette`](https://crates.io/crates/miette): This seems to be a pretty popular library nowadays for code span reporting. However, to me it looks intimidatingly complex and provides much more functionality than I need. Furthermore, it has a higher MSRV and more dependencies than I like. But, the real deal breaker for me is that according to its description, it is "for us mere mortals who aren't compiler hackers". Given that I see myself as some kind of compiler hacker, I do not feel as part of its target group (although I'm probably mortal). :) [write.rs]: https://github.com/zesterer/ariadne/blob/876a093653bdbe7b69f4e77cd122fed5caa37a27/src/write.rs#L10 codesnake-0.2.1/example.svg000064400000000000000000000051741046102023000137430ustar 00000000000000 ╭─[fac.lisp] β”‚ 1 β”‚ (defun factorial (n) (if (zerop n) 1 ┆ ────┬──── β–² ┆ β”‚ β”‚ ┆ ╰───────────────────────── this function ... ┆ ╭──────────────────────╯ 2 β”‚ β”‚ (* n (factorial (1- n))))) ┆ β”‚ β–² ┬ ┆ β”‚ β”‚ β”‚ ┆ ╰─────────────────────────────────┴─── ... is defined by this ┆ β”‚ ┆ ╰─ (and here is EOF) ──╯ codesnake-0.2.1/examples/example.rs000064400000000000000000000033711046102023000154030ustar 00000000000000// To generate SVG from this example: // // cargo run --example example | ansisvg --colorscheme "Builtin Solarized Dark" --fontname "Source Code Pro" --fontsize 18 > example.svg use codesnake::{Block, CodeWidth, Label, LineIndex}; use core::fmt::Display; use yansi::{Color, Paint}; const SRC: &str = r#"(defun factorial (n) (if (zerop n) 1 (* n (factorial (1- n)))))"#; fn style(html: bool, d: &impl Display, color: Color) -> String { if html { let mut color = format!("{color:?}"); color.make_ascii_lowercase(); format!("{d}",) } else { d.fg(color).to_string() } } fn main() { /* to find the byte positions in this example: for ci in SRC.char_indices() { println!("{ci:?}"); } */ let html = std::env::args().skip(1).any(|arg| arg == "--html"); let idx = LineIndex::new(SRC); let color = |color| move |s| style(html, &s, color); let labels = [ Label::new(1..6).with_style(color(Color::Red)), Label::new(7..16) .with_text("this function ...") .with_style(color(Color::Green)), Label::new(21..70) .with_text("... is defined by this") .with_style(color(Color::Blue)), Label::new(71..71) .with_text("(and here is EOF)") .with_style(color(Color::Yellow)), ]; let block = Block::new(&idx, labels).unwrap().map_code(|s| { let s = s.replace('\t', " "); let w = unicode_width::UnicodeWidthStr::width(&*s); CodeWidth::new(s, core::cmp::max(w, 1)) }); println!( "{}{}", block.prologue(), style(html, &"[fac.lisp]", Color::Red) ); print!("{block}"); println!("{}", block.epilogue()); } codesnake-0.2.1/src/lib.rs000064400000000000000000000547031046102023000134740ustar 00000000000000#![no_std] #![forbid(unsafe_code)] #![warn(missing_docs)] //! Pretty printer for non-overlapping code spans. //! //! This crate aids you in creating output like the following, //! both for the terminal (ANSI) as well as for the web (HTML): //! //! //! //!
//!   ╭─[fac.lisp]
//!   β”‚
//! 1 β”‚   (defun factorial (n) (if (zerop n) 1
//!   ┆          ────┬────     β–²
//!   ┆              β”‚         β”‚
//!   ┆              ╰───────────────────────── this function ...
//!   ┆ ╭──────────────────────╯
//! 2 β”‚ β”‚         (* n (factorial (1- n)))))
//!   ┆ β”‚                                 β–² ┬
//!   ┆ β”‚                                 β”‚ β”‚
//!   ┆ ╰─────────────────────────────────┴─── ... is defined by this
//!   ┆                                     β”‚
//!   ┆                                     ╰─ (and here is EOF)
//! ──╯
//! 
//! //! This example has been created with `cargo run --example example -- --html`. //! To see its console output, run `cargo run --example example`. //! //! # Usage //! //! Suppose that we have a source file and a list of byte ranges that we want to annotate. //! For example: //! //! ~~~ //! let src = r#"if true { 42 } else { "42" }"#; //! let labels = [ //! (8..14, "this is of type Nat"), //! (20..28, "this is of type String"), //! ]; //! ~~~ //! //! First, we have to create a [`LineIndex`]. //! This splits the source into lines, so that further functions can //! quickly find in which line a byte is situated. //! //! ~~~ //! use codesnake::LineIndex; //! # let src = r#"if true { 42 } else { "42" }"#; //! let idx = LineIndex::new(src); //! ~~~ //! //! Next, we create a code [`Block`] from our index and the [`Label`]s: //! //! ~~~ //! use codesnake::{Block, Label}; //! # use codesnake::LineIndex; //! # let src = r#"if true { 42 } else { "42" }"#; //! # let idx = LineIndex::new(src); //! # let labels = [(8..14, "this is of type Nat")]; //! let block = Block::new(&idx, labels.map(|(range, text)| Label::new(range).with_text(text))).unwrap(); //! ~~~ //! //! This will fail if your labels refer to bytes outside the range of your source. //! //! Finally, we can print our code block: //! //! ~~~ //! use codesnake::CodeWidth; //! # use codesnake::{Block, Label}; //! # use codesnake::LineIndex; //! # let src = r#"if true { 42 } else { "42" }"#; //! # let idx = LineIndex::new(src); //! # let labels = [(8..14, "this is of type Nat")]; //! # let block = Block::new(&idx, labels.map(|(range, text)| Label::new(range).with_text(text))).unwrap(); //! let block = block.map_code(|c| CodeWidth::new(c, c.len())); //! // yield " ╭─[main.rs]" //! println!("{}{}", block.prologue(), "[main.rs]"); //! print!("{block}"); //! // yield "──╯" //! println!("{}", block.epilogue()); //! ~~~ //! //! # Colors //! //! To color the output on a terminal, you can use a crate like [`yansi`](https://docs.rs/yansi). //! This allows you to color the snakes of a label as follows: //! //! ~~~ //! use codesnake::Label; //! use yansi::Paint; //! # let (range, text) = (8..14, "this is of type Nat"); //! let label = Label::new(range).with_text(text).with_style(|s| s.red().to_string()); //! ~~~ //! //! For HTML, you can use something like: //! //! ~~~ //! use codesnake::Label; //! # let (range, text) = (8..14, "this is of type Nat"); //! let label = Label::new(range).with_text(text).with_style(|s| { //! format!("{s}") //! }); //! ~~~ extern crate alloc; use alloc::string::{String, ToString}; use alloc::{boxed::Box, format, vec::Vec}; use core::fmt::{self, Display, Formatter}; use core::ops::Range; /// Associate byte offsets with line numbers. /// /// If `idx = LineIndex::new(s)` and `idx.0[n] = (offset, line)`, then /// the `n`-th line of `s` starts at `offset` in `s` and equals `line`. pub struct LineIndex<'a>(Vec<(usize, &'a str)>); impl<'a> LineIndex<'a> { /// Create a new index. #[must_use] pub fn new(s: &'a str) -> Self { // indices of '\n' characters let newlines: Vec<_> = s .char_indices() .filter_map(|(i, c)| (c == '\n').then_some(i)) .collect(); // indices of line starts and ends let starts = core::iter::once(0).chain(newlines.iter().map(|i| *i + 1)); let ends = newlines.iter().copied().chain(core::iter::once(s.len())); let lines = starts.zip(ends).map(|(start, end)| (start, &s[start..end])); Self(lines.collect()) } fn get(&self, offset: usize) -> Option { use core::cmp::Ordering; let line_no = self.0.binary_search_by(|(line_start, line)| { if *line_start > offset { Ordering::Greater } else if line_start + line.len() < offset { Ordering::Less } else { Ordering::Equal } }); let line_no = line_no.ok()?; let (line_start, line) = self.0[line_no]; Some(IndexEntry { line_no, line, bytes: offset - line_start, }) } } #[derive(Debug)] struct IndexEntry<'a> { line: &'a str, line_no: usize, bytes: usize, } /// Code label with text and style. pub struct Label { code: C, text: Option, style: Box