Skip to main content

farben/
lib.rs

1//! # Introduction
2//! Farben (as in "color" in german) is a zero-dependency terminal coloring library.
3//! Farben uses a markup-language-like syntax to your string and outputs them colored. For example:
4//!
5//! # Example
6//! ```
7//! use farben::*;
8//!
9//! let colored = color("[red]I'm red!");
10//! assert_eq!(colored, "\x1b[31mI'm red!\x1b[0m");
11//! ```
12pub(crate) mod ansi;
13pub mod errors;
14pub(crate) mod lexer;
15pub(crate) mod parser;
16
17/// Parses and renders a farben markup string, appending a final SGR reset.
18///
19/// # Errors
20///
21/// Returns a `LexError` if the input contains an unclosed tag, an unknown tag name,
22/// or a malformed color value.
23pub fn try_color(input: impl Into<String>) -> Result<String, errors::LexError> {
24    let input = input.into();
25    let mut res = parser::render(lexer::tokenize(input)?);
26    res.push_str("\x1b[0m");
27    Ok(res)
28}
29
30/// Parses and renders a farben markup string, appending a final SGR reset.
31///
32/// # Panics
33///
34/// Panics if the input is not valid farben markup. Use `try_color` to handle errors explicitly.
35pub fn color(input: impl Into<String>) -> String {
36    let input = input.into();
37    try_color(input).expect("Failed to colorize")
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    // --- try_color ---
45
46    #[test]
47    fn test_try_color_named_color() {
48        let result = try_color("[red]I'm red!");
49        assert!(result.is_ok());
50        assert_eq!(result.unwrap(), "\x1b[31mI'm red!\x1b[0m");
51    }
52
53    #[test]
54    fn test_try_color_appends_trailing_reset() {
55        let result = try_color("[blue]text");
56        assert!(result.is_ok());
57        assert!(result.unwrap().ends_with("\x1b[0m"));
58    }
59
60    #[test]
61    fn test_try_color_plain_text_no_tags() {
62        let result = try_color("no markup here");
63        assert!(result.is_ok());
64        assert_eq!(result.unwrap(), "no markup here\x1b[0m");
65    }
66
67    #[test]
68    fn test_try_color_empty_string() {
69        let result = try_color("");
70        assert!(result.is_ok());
71        assert_eq!(result.unwrap(), "\x1b[0m");
72    }
73
74    #[test]
75    fn test_try_color_invalid_tag_returns_error() {
76        let result = try_color("[error]text");
77        assert!(result.is_err());
78    }
79
80    #[test]
81    fn test_try_color_unclosed_tag_returns_error() {
82        let result = try_color("[red");
83        assert!(result.is_err());
84    }
85
86    #[test]
87    fn test_try_color_rgb_color() {
88        let result = try_color("[rgb(255,0,0)]red");
89        assert!(result.is_ok());
90        assert_eq!(result.unwrap(), "\x1b[38;2;255;0;0mred\x1b[0m");
91    }
92
93    #[test]
94    fn test_try_color_bold_and_named_color() {
95        let result = try_color("[bold red]hi");
96        assert!(result.is_ok());
97        assert_eq!(result.unwrap(), "\x1b[1m\x1b[31mhi\x1b[0m");
98    }
99
100    #[test]
101    fn test_try_color_escaped_bracket() {
102        let result = try_color("\\[not a tag]");
103        assert!(result.is_ok());
104        assert!(result.unwrap().starts_with('['));
105    }
106
107    #[test]
108    fn test_try_color_inline_reset() {
109        let result = try_color("[red]before[/]after");
110        assert!(result.is_ok());
111        let s = result.unwrap();
112        assert!(s.contains("\x1b[0m"));
113    }
114
115    // --- color ---
116
117    #[test]
118    fn test_color_valid_input_returns_string() {
119        let result = color("[green]ok");
120        assert_eq!(result, "\x1b[32mok\x1b[0m");
121    }
122
123    #[test]
124    #[should_panic]
125    fn test_color_invalid_input_panics() {
126        color("[notacolor]text");
127    }
128}
129
130// Skipped (side effects): none: both public functions are pure string transformations.