parse_ansi/lib.rs
1//! Parse ANSI escape codes (colors, underlines, etc.)
2//!
3//! ```rust
4//! extern crate parse_ansi;
5//!
6//! assert_eq!(
7//! parse_ansi::ANSI_REGEX.replace_all(
8//! b"Hello, \x1b[42mworld\x1b[0m!",
9//! b"" as &[u8],
10//! ),
11//! b"Hello, world!" as &[u8],
12//! );
13//! ```
14
15#[macro_use]
16extern crate lazy_static;
17extern crate regex;
18
19#[cfg(test)]
20extern crate itertools;
21
22use regex::bytes::{Matches, Regex};
23
24// Inspired by https://github.com/nodejs/node/blob/641d4a4159aaa96eece8356e03ec6c7248ae3e73/lib/internal/readline.js#L9
25pub const ANSI_RE: &str =
26 r"[\x1b\x9b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]";
27
28lazy_static! {
29 /// A `Regex` that matches ANSI escape codes.
30 ///
31 /// ```rust
32 /// # use parse_ansi::ANSI_REGEX;
33 /// assert_eq!(
34 /// ANSI_REGEX.replace_all(b"foo \x1b[42mbar\x1b[0m baz", b"" as &[u8]),
35 /// b"foo bar baz" as &[u8],
36 /// );
37 pub static ref ANSI_REGEX: Regex = Regex::new(ANSI_RE).unwrap();
38}
39
40/// Parses ANSI escape codes from the given text, returning an `Iterator<Item = Match>`.
41///
42/// ```rust
43/// # use parse_ansi::parse_bytes;
44/// let ansi_text = b"Hello, \x1b[31;4mworld\x1b[0m!";
45/// let parsed: Vec<_> = parse_bytes(ansi_text)
46/// .map(|m| (m.start(), m.end()))
47/// .collect();
48/// assert_eq!(
49/// parsed,
50/// vec![(7, 14), (19, 23)],
51/// );
52/// ```
53pub fn parse_bytes(text: &[u8]) -> Matches {
54 ANSI_REGEX.find_iter(text)
55}
56
57#[cfg(test)]
58mod tests {
59 use super::parse_bytes;
60 use itertools::zip_eq;
61
62 fn test_parse(text: &[u8], expected: &[(usize, usize, &[u8])]) {
63 for (match_, expected_match) in zip_eq(parse_bytes(text), expected.iter()) {
64 assert_eq!(
65 &(match_.start(), match_.end(), match_.as_bytes()),
66 expected_match,
67 );
68 }
69 }
70
71 #[test]
72 fn red_underline() {
73 test_parse(
74 b"before \x1b[31;4mred underline\x1b[0m after",
75 &[(7, 14, b"\x1b[31;4m"), (27, 31, b"\x1b[0m")],
76 );
77 }
78}