Skip to main content

styled_str/
lib.rs

1//! Tools for parsing and managing ANSI-styled strings.
2//!
3//! This library allows to:
4//!
5//! - [Parse ANSI-styled strings](#parsing-ansi-escapes).
6//! - Create styled strings from [human-readable format](#rich-format), including in compile time.
7//! - Compare styled strings with [rich diff info](#comparing-styled-strings).
8//! - Manipulate styled strings, e.g. split them into lines, split off parts etc.
9//!
10//! One of guiding use cases for the library is hassle-free snapshot testing for styled strings,
11//! without the need to compare literal strings with ANSI escapes (which is brittle and not human-readable),
12//! and outputting more informative diff info than a simple `assert_eq!` would.
13//!
14//! For the example of real-world usage, see the [`term-transcript`](https://docs.rs/term-transcript/) crate.
15//!
16//! # Styled strings
17//!
18//! The core types exposed by this crate are [`StyledStr`] and [`StyledString`]. Both are a container for text + styling
19//! signaled via [ANSI escape codes]. `StyledStr` is borrowed (analog to `&str`), while `StyledString`
20//! is owned (analog to `String`). Styling is represented as a sequence of spans ([`SpanStr`] type)
21//! that cover the text in its entirety. Styles reuse the model from [`anstyle`]; i.e., a style
22//! is just a [`Style`](anstyle::Style).
23//!
24//! # Rich syntax
25//!
26//! One way to create `Styled` strings is parsing [`rich`]-inspired syntax, either in compile time
27//! via the [`styled!`] macro, or in runtime via [`FromStr`](core::str::FromStr).
28//! Conversely, a `Styled` string can be converted to the rich format via its [`Display`](core::fmt::Display)
29//! implementation.
30//!
31//! The format is as follows:
32//!
33//! - Styling directives are enclosed in double brackets: `[[` + `]]`.
34//! - A directive is a list of *tokens* separated by whitespace and/or commas `,` or semicolons `;`.
35//! - A token represents an effect, e.g. `bold` or `underline`, a foreground [color](#color-tokens) (e.g., `red`
36//!   or `#fed`), or a background color (`on` + a color token, e.g. `on blue`).
37//! - By default, a directive completely overrides the previously applied directive. Hence, there is no need
38//!   for special closing directives.
39//! - A directive can be made to inherit from the previously applied style by preceding all tokens with a `*`.
40//!   This also allows to *subtract* effects by specifying them with a `-` or `!` in front, like `-bold` or `!italic`.
41//!   Similarly, `-color` / `!color` (or `-fg` / `!fg`) switches off the foreground color, and
42//!   `-on`, `!on`, `-bg`, or `!bg` switches off the background color.
43//! - A directive may be empty: `[[]]`. As a special case, `[[/]]` (i.e., a single `/` token) is equivalent
44//!   to an empty directive.
45//!
46//! ## Effects
47//!
48//! The following effects are supported:
49//!
50//! | Effect | Aliases |
51//! |:-------|:--------|
52//! | `bold` | `b` |
53//! | `italic` | `it`, `i` |
54//! | `underline`| `ul`, `u` |
55//! | `strikethrough` | `strike`, `s` |
56//! | `dimmed` | `dim` |
57//! | `invert` | `inverted`, `inv` |
58//! | `blink` | |
59//! | `concealed` | `conceal`, `hide`, `hidden` |
60//!
61//! ## Color tokens
62//!
63//! A color may be represented as follows:
64//!
65//! - One of the 8 base terminal colors (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`)
66//! - One of the 8 bright terminal colors signaled via `!` suffix or `bright-` prefix (e.g., `blue!` or `bright-blue`).
67//! - One of [256 indexed ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) specified
68//!   as `color$idx` or `color($idx)`, e.g. `color23` or `color(254)`.
69//! - A 24-bit RGB color written in CSS-like hex format, e.g. `#fa4` or `#c0ffee`.
70//!
71//! ## Syntax examples
72//!
73//! ```
74//! use styled_str::{styled, StyledStr};
75//!
76//! const STYLED: StyledStr = styled!("[[green! on white, bold]]Hello,[[/]] world[[it]]!");
77//! assert_eq!(STYLED.text(), "Hello, world!");
78//! assert_eq!(STYLED.spans().len(), 3);
79//! ```
80//!
81//! Here, the first style applies to `Hello,`, then it is completely reset for ` world`,
82//! and finally, the italic effect (and only it) is applied to `!`.
83//!
84//! ```
85//! # use styled_str::{styled, StyledStr};
86//! const STYLED: StyledStr = styled!("[[bold green!]]Hello[[* -bold]], world[[* invert]]!");
87//! ```
88//!
89//! Here, there are again 3 styled spans, but the second and third ones inherit the preceding style.
90//! The second span removes the bold effect, and the third one inverts foreground and background colors.
91//!
92//! # Other string functionality
93//!
94//! ## Parsing ANSI escapes
95//!
96//! [`StyledString::from_ansi()`] and [`StyledString::from_ansi_bytes()`] allow parsing a styled string
97//! from ANSI escapes (e.g., captured from a terminal).
98//!
99//! ```
100//! # use styled_str::{styled, StyledString};
101//! let str = StyledString::from_ansi(
102//!     "\u{1b}[32mHello,\u{1b}[m world\u{1b}[1m!\u{1b}[m",
103//! )?;
104//! assert_eq!(str.text(), "Hello, world!");
105//! assert_eq!(str.to_string(), "[[green]]Hello,[[/]] world[[bold]]!");
106//! # anyhow::Ok(())
107//! ```
108//!
109//! ## Comparing styled strings
110//!
111//! [`StyledStr::diff()`] allows comparing two styled strings both in terms of text and styles.
112//! [`TextDiff`] and [`StyleDiff`] provide more fine-grained control over comparison logic.
113//! These types can be [`Display`](core::fmt::Display)ed / [`Debug`](core::fmt::Debug)ged
114//! in order to provide rich human-readable info about differences (e.g., in the test code).
115//!
116//! ```
117//! # use styled_str::{styled, Diff};
118//! # use assert_matches::assert_matches;
119//! let left = styled!("Hello, [[bold dim white on #fa4]]world!");
120//! let right = styled!("Hello, [[bold]]world[[/]]!");
121//! let diff = left.diff(right).unwrap_err();
122//! assert_matches!(&diff, Diff::Style(_));
123//!
124//! // Will output detailed human-readable info about the diff.
125//! println!("{diff}");
126//! ```
127//!
128//! # Limitations
129//!
130//! - ANSI escape sequences other than [SGR] ones are either dropped (in case of [CSI] sequences),
131//!   or lead to [an error](AnsiError).
132//!
133//! # Alternatives and similar tools
134//!
135//! - This crate builds on the [`anstyle`] library, using its styling data model. `anstyle` together
136//!   with [`anstream`](https://docs.rs/anstream/) provides tools to create / output ANSI-styled strings in runtime.
137//!   It doesn't cover creating strings in compile time, parsing ANSI-styled strings, or comparing styled strings.
138//! - [`color_print`](https://docs.rs/color-print/) provides proc macros to create / output ANSI-styled strings
139//!   using `rich`-like syntax.
140//! - [`parse-style`](https://docs.rs/parse-style/) allows parsing `rich`-like style specs.
141//!
142//! # Crate features
143//!
144//! ## `std`
145//!
146//! *(On by default)*
147//!
148//! Enables std-specific functionality, such as [`Error`](std::error::Error) trait implementations.
149//! Note that disabling this feature for now doesn't make this crate no-std compatible.
150//!
151//! [ANSI escape codes]: https://en.wikipedia.org/wiki/ANSI_escape_code
152//! [`rich`]: https://rich.readthedocs.io/en/stable/index.html
153//! [SGR]: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR
154//! [CSI]: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
155
156// Documentation settings
157#![doc(html_root_url = "https://docs.rs/styled-str/0.5.0")]
158// Conditional compilation
159#![cfg_attr(not(feature = "std"), no_std)]
160extern crate core;
161
162pub use crate::{
163    ansi_parser::AnsiError,
164    errors::{HexColorError, ParseError, ParseErrorKind},
165    rich_parser::{RichStyle, parse_hex_color, rgb_color_to_hex},
166    style_diff::StyleDiff,
167    types::{
168        Diff, Lines, SpanStr, StackStyled, StyledStr, StyledString, StyledStringBuilder, TextDiff,
169    },
170};
171
172#[macro_use]
173mod utils;
174mod ansi_parser;
175mod errors;
176mod rich_parser;
177mod style_diff;
178#[cfg(test)]
179mod tests;
180mod types;
181
182mod alloc {
183    #[cfg(not(feature = "std"))]
184    extern crate alloc as std;
185
186    pub(crate) use std::{borrow::Cow, format, string::String, vec::Vec};
187}
188
189/// Parses [rich syntax](crate#rich-syntax) into a [`StyledStr`] in compile time.
190///
191/// # Examples
192///
193/// ```
194/// use styled_str::{styled, StyledStr};
195///
196/// const STYLED: StyledStr = styled!(
197///     "[[bold red on white]]ERROR:[[/]] [[it]]Something[[/]] \
198///      [[strike]]bad[[/]] happened"
199/// );
200/// assert_eq!(STYLED.text(), "ERROR: Something bad happened");
201/// assert_eq!(STYLED.span(0).unwrap().text, "ERROR:");
202/// ```
203#[macro_export]
204macro_rules! styled {
205    ($raw:expr) => {{
206        const __CAPACITIES: (usize, usize) = $crate::StyledStr::capacities($raw);
207        const { $crate::StackStyled::<{ __CAPACITIES.0 }, { __CAPACITIES.1 }>::new($raw) }.as_ref()
208    }};
209}
210
211#[cfg(doctest)]
212doc_comment::doctest!("../README.md");