colored_str/
lib.rs

1// Copyright (C) 2023 Sebastien Guerri
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16#![warn(missing_docs)]
17
18//! Coloring terminal by parsing string content
19//! 
20//! This crate is an extension to the [colored](https://crates.io/crates/colored) crate
21//! that enables terminal coloring. It provides a trait and a macro to parse a given string
22//! that incorporates style flags.
23//! 
24//! ## Usage
25//! 
26//! This crate is [on crates.io](https://crates.io/crates/colored-str) and can be used
27//! by adding `colored-str` to your dependencies in your project's `Cargo.toml`.
28//! 
29//! ```toml
30//! [dependencies]
31//! colored-str = "0.1.8"
32//! ```
33//! 
34//! ## How to use
35//! 
36//! Styles must be written within `<...>` opening flag and `</>` closing flag.
37//! 
38//! Style variations must be written within `<+...>` opening flag and `<->` closing flag.
39//! 
40//! See below examples.
41//! 
42//! ## Limitations
43//! 
44//! **Blocks cannot be overlapped**  
45//! Such code `<red> ... <blue> ... </> ... </>` will not work properly.  
46//! This is true as well for variations : `<red><+blue> ... <+bold> ... <-><-></>` will not work properly.  
47//! 
48//! **A style cannot be removed**  
49//! With `<red+bold> ... here I want to keep red only => impossible </>`.  
50//! The workaround is as follows: `<red> <+bold> ... <-> here I have red only </>`
51//! 
52//! ## Examples
53//! 
54//! ```
55//! use colored_str::coloredln;
56//! 
57//! coloredln!("<red>this is red</>");
58//! coloredln!("<#FF0000>this is also red</>");
59//! coloredln!("<blue+red>this is red again</>");
60//! coloredln!("<red+on_blue>this is red on blue</>");
61//! coloredln!("<red+on_#0000FF>this is also red on blue</>");
62//! coloredln!("<bold>this is bold</>");
63//! coloredln!("<red>there is a first line\nthen a second</>");
64//! ```
65//! 
66//! You can add variables as per [`println!`]
67//! 
68//! ```
69//! use colored_str::coloredln;
70//! 
71//! let message = "this is red";
72//! coloredln!("<red>{message}</>");
73//! coloredln!("<red>{}</>", message);
74//! ```
75//! 
76//! You can add styles adjustments in a block
77//! 
78//! ```
79//! use colored_str::coloredln;
80//! 
81//! coloredln!("<red>this is red <+bold>this is red and bold<-> then red again </>");
82//! coloredln!("<red>this is red <+bold+on_blue>this is red on blue and bold<-> then red again </>");
83//! ```
84//! 
85//! You can also use it as a trait
86//! 
87//! ```
88//! use colored_str::Colored;
89//! let s: String = "<red>this is red</>".colored().to_string();
90//! println!("{}", s);
91//! ```
92//! 
93//! ## List of styles
94//! 
95//! ### Colors
96//! 
97//! - `black`
98//! - `red`
99//! - `green`
100//! - `yellow`
101//! - `blue`
102//! - `magenta`
103//! - `purple`
104//! - `cyan`
105//! - `white`
106//! 
107//! All can be used as backgound using `on_` prefix.
108//! 
109//! ### Light/Bright Colors
110//! 
111//! - `lblack`
112//! - `lred`
113//! - `lgreen`
114//! - `lyellow`
115//! - `lblue`
116//! - `lmagenta`
117//! - `lpurple`
118//! - `lcyan`
119//! - `lwhite`
120//! 
121//! All can be used as backgound using `on_` prefix.
122//! 
123//! ### Decorations
124//! 
125//! - `bold`
126//! - `underline`
127//! - `italic`
128//! - `dimmed`
129//! - `reverse`
130//! - `reversed`
131//! - `blink`
132//! - `hidden`
133//! - `strikethrough`
134//! 
135//! ### True colors
136//! 
137//! - `#RRGGBB`
138//! - `on_#RRGGBB`
139//! 
140
141use std::borrow::Cow;
142
143use regex::Regex;
144use regex::Replacer;
145use regex::Captures;
146use regex::CaptureMatches;
147use lazy_static::lazy_static;
148
149use colored::*;
150
151/// Regex to check truecolor foreground format
152fn is_truecolor(text: &str) -> bool
153{
154    lazy_static! {
155        static ref RE: Regex = Regex::new(r"^#[ABCDEF\d]{6}$").unwrap();
156    }
157    RE.is_match(text)
158}
159
160/// Regex to check truecolor background format
161fn is_on_truecolor(text: &str) -> bool
162{
163    lazy_static! {
164        static ref RE: Regex = Regex::new(r"^on_#[ABCDEF\d]{6}$").unwrap();
165    }
166    RE.is_match(text)
167}
168
169/// Regex to retrieve all styled blocks
170fn get_styles<R>(text: &str, rep: R) -> Cow<str>
171    where R: Replacer
172{
173    lazy_static! {
174        static ref RE: Regex = Regex::new(r"<([\w\d#+]+?)>((.|\n)*?)</>").unwrap();
175        // static ref RE: Regex = Regex::new(r"<(.+?)>((.|\n)*?)</>").unwrap();
176    }
177    RE.replace_all(text, rep)
178}
179
180/// Regex to retrieve all style modification subblocks
181fn iter_substyles(text: &str) -> CaptureMatches
182{
183    lazy_static! {
184        static ref RE: Regex = Regex::new(r"<\+([\w\d#+]+?)>((.|\n)*?)<->").unwrap();
185        // static ref RE: Regex = Regex::new(r"<\+(.+?)>((.|\n)*?)<->").unwrap();
186    }
187    RE.captures_iter(text)
188}
189
190/// If style is truecolor foreground, apply style
191fn test_truecolor(style: &'_ str, content: &ColoredString) -> Option<ColoredString>
192{
193    if !is_truecolor(style) {
194        return None
195    }
196        
197    let red = &style[1..3];
198    let green = &style[3..5];
199    let blue = &style[5..7];
200    let r = u8::from_str_radix(red, 16).expect("invalid color");
201    let g = u8::from_str_radix(green, 16).expect("invalid color");
202    let b = u8::from_str_radix(blue, 16).expect("invalid color");
203    Some(content.clone().truecolor(r, g, b))
204}
205
206/// If style is backcolor foreground, apply style
207fn test_on_truecolor(style: &'_ str, content: &ColoredString) -> Option<ColoredString>
208{
209    if !is_on_truecolor(style) {
210        return None
211    }
212     
213    let red = &style[4..6];
214    let green = &style[6..8];
215    let blue = &style[8..10];
216    let r = u8::from_str_radix(red, 16).expect("invalid color");
217    let g = u8::from_str_radix(green, 16).expect("invalid color");
218    let b = u8::from_str_radix(blue, 16).expect("invalid color");
219    Some(content.clone().on_truecolor(r, g, b))
220}
221
222/// Returns the function to apply in case generic ColoredString function does not apply
223fn test_other(style: &'_ str) -> impl Fn(ColoredString) -> ColoredString + '_
224{
225    move |content: ColoredString| {
226        if let Some(result) = test_truecolor(style, &content) {
227            result
228        } else if let Some(result) = test_on_truecolor(style, &content) {
229            result
230        } else {
231            let colored = format!("<{}>{}</>", style, content);
232            ColoredString::from(colored.as_ref())    
233        }
234    }
235}
236
237/// Returns the function to apply in case generic ColoredString function exists
238fn test_style<'a>(style: &'a str) -> Box<dyn Fn(ColoredString) -> ColoredString + 'a>
239{
240    match style.to_lowercase().as_str() {
241
242        "black" => Box::new(&ColoredString::black),
243        "red" => Box::new(&ColoredString::red),
244        "green" => Box::new(&ColoredString::green),
245        "yellow" => Box::new(&ColoredString::yellow),
246        "blue" => Box::new(&ColoredString::blue),
247        "magenta" => Box::new(&ColoredString::magenta),
248        "purple" => Box::new(&ColoredString::purple),
249        "cyan" => Box::new(&ColoredString::cyan),
250        "white" => Box::new(&ColoredString::white),
251        "lblack" => Box::new(&ColoredString::bright_black),
252        "lred" => Box::new(&ColoredString::bright_red),
253        "lgreen" => Box::new(&ColoredString::bright_green),
254        "lyellow" => Box::new(&ColoredString::bright_yellow),
255        "lblue" => Box::new(&ColoredString::bright_blue),
256        "lmagenta" => Box::new(&ColoredString::bright_magenta),
257        "lpurple" => Box::new(&ColoredString::bright_purple),
258        "lcyan" => Box::new(&ColoredString::bright_cyan),
259        "lwhite" => Box::new(&ColoredString::bright_white),
260
261        "on_black" => Box::new(&ColoredString::on_black),
262        "on_red" => Box::new(&ColoredString::on_red),
263        "on_green" => Box::new(&ColoredString::on_green),
264        "on_yellow" => Box::new(&ColoredString::on_yellow),
265        "on_blue" => Box::new(&ColoredString::on_blue),
266        "on_magenta" => Box::new(&ColoredString::on_magenta),
267        "on_purple" => Box::new(&ColoredString::on_purple),
268        "on_cyan" => Box::new(&ColoredString::on_cyan),
269        "on_white" => Box::new(&ColoredString::on_white),
270        "on_lblack" => Box::new(&ColoredString::on_bright_black),
271        "on_lred" => Box::new(&ColoredString::on_bright_red),
272        "on_lgreen" => Box::new(&ColoredString::on_bright_green),
273        "on_lyellow" => Box::new(&ColoredString::on_bright_yellow),
274        "on_lblue" => Box::new(&ColoredString::on_bright_blue),
275        "on_lmagenta" => Box::new(&ColoredString::on_bright_magenta),
276        "on_lpurple" => Box::new(&ColoredString::on_bright_purple),
277        "on_lcyan" => Box::new(&ColoredString::on_bright_cyan),
278        "on_lwhite" => Box::new(&ColoredString::on_bright_white),
279
280        "bold" => Box::new(&ColoredString::bold),
281        "underline" => Box::new(&ColoredString::underline),
282        "italic" => Box::new(&ColoredString::italic),
283        "dimmed" => Box::new(&ColoredString::dimmed),
284        "reverse" => Box::new(&ColoredString::reverse),
285        "reversed" => Box::new(&ColoredString::reversed),
286        "blink" => Box::new(&ColoredString::blink),
287        "hidden" => Box::new(&ColoredString::hidden),
288        "strikethrough" => Box::new(&ColoredString::strikethrough),
289
290        _ => Box::new(test_other(style))
291    }
292}
293
294/// Overwrite the style of first entry with the style of second entry
295fn update_with_style(text: ColoredString, colored: &ColoredString) -> ColoredString
296{
297    let mut result = text;
298    if let Some(fgcolor) = colored.fgcolor() {
299        result = result.color(fgcolor);
300    }
301    if let Some(bgcolor) = colored.bgcolor() {
302        result = result.on_color(bgcolor);
303    }
304    if colored.style().contains(Styles::Bold) { result = result.bold(); }
305    if colored.style().contains(Styles::Underline) { result = result.underline(); }
306    if colored.style().contains(Styles::Italic) { result = result.italic(); }
307    if colored.style().contains(Styles::Dimmed) { result = result.dimmed(); }
308    if colored.style().contains(Styles::Reversed) { result = result.reverse(); }
309    if colored.style().contains(Styles::Blink) { result = result.blink(); }
310    if colored.style().contains(Styles::Hidden) { result = result.hidden(); }
311    if colored.style().contains(Styles::Strikethrough) { result = result.strikethrough(); }
312    result
313}
314
315/// Add a given ColoredString style to an unstyled text
316fn set_style_from(text: &str, colored: &ColoredString) -> ColoredString
317{
318    let mut result = ColoredString::from(text);
319    result = update_with_style(result, colored);
320    result
321}
322
323/// Add a given ColoredString style to an already styled text (overwrite content)
324fn add_style_from(colored_from: &ColoredString, colored_to: &ColoredString) -> ColoredString
325{
326    let mut result = set_style_from(colored_to, colored_from);
327    result = update_with_style(result, colored_to);
328    result
329}
330
331
332
333
334
335
336
337
338
339
340
341
342/// Creates a new [`ColoredString`][1] by parsing given text.
343///
344/// It will parse the given text, searching for `<...> * </>` blocks and `<+...> * <->`
345/// subblocks, to add corresponding styles to the text. It then returns a new
346/// instance of [`ColoredString`][1].
347///
348/// [1]: <https://docs.rs/colored/latest/colored/struct.ColoredString.html>
349/// 
350/// # Examples
351///
352/// Basic usage:
353///
354/// ```
355/// use colored_str::colored;
356/// 
357/// println!("{}", colored("<red>this is red text</red>"));
358/// ```
359/// 
360/// See [crate] for other examples
361pub fn colored(text: &str) -> ColoredString
362{
363    let updated = get_styles(text, |caps: &Captures| {
364
365        // Create main styled item based on capture
366        let combined = caps[1].split('+');
367        let mut item = ColoredString::from(&caps[2]);
368        for style in combined {
369            item = test_style(style.trim())(item);
370        }
371
372        let mut items: Vec<ColoredString> = vec![];
373        let mut id_start = 0;
374        let mut id_end = 0;
375
376        // Add substyles
377        for caps in iter_substyles(&item) {
378
379            let range = caps.get(0).unwrap().range();
380            id_end = range.end;
381
382            // Add styled item before the capture if text is not empty
383            if id_start != range.start {
384                let text = &item[id_start..range.start];
385                items.push(set_style_from(text, &item));
386                id_start = id_end;
387            }
388
389            // Add substyled item if not empty
390            if !&caps[2].is_empty() {
391                let combined = caps[1].split('+');
392                let mut subitem = ColoredString::from(&caps[2]);
393                for style in combined {
394                    subitem = test_style(style.trim())(subitem);
395                }
396                subitem = add_style_from(&item, &subitem);
397                items.push(subitem.clone());
398            }
399        }
400
401        // Add styled item after all captures (end of the string)
402        if id_end != item.len() {
403            let text = &item[id_end..item.len()];
404            items.push(set_style_from(text, &item));
405        }
406
407        // Join all items
408        let mut res = "".to_owned();
409        for i in items {
410            res = format!("{}{}", res, &i);
411        }
412        res
413    });
414
415    ColoredString::from(updated.as_ref())
416}
417
418/// The trait that enables a string to be colorized
419pub trait Colored
420{
421    /// Creates a new [`ColoredString`][1] by parsing given text.
422    /// 
423    /// [1]: <https://docs.rs/colored/latest/colored/struct.ColoredString.html>
424    fn colored(self) -> ColoredString;
425}
426
427impl<'a> Colored for &'a str
428{
429    fn colored(self) -> ColoredString
430    {
431        colored(self)
432    }
433}
434
435/// Creates a new [`String`] by parsing given text.
436///
437/// With nothing given returns an empty [`String`].  
438/// Otherwise format given parameters using `format!` macro then apply [`colored()`].
439///
440/// # Examples
441///
442/// Basic usage:
443///
444/// ```
445/// use colored_str::cformat;
446/// 
447/// println!("{}", cformat!("<red>this is red text</red>"));
448/// println!("{}", cformat!("<red>this is {} text</red>", "red"));
449/// ```
450/// 
451/// See [crate] for other examples
452#[macro_export]
453macro_rules! cformat {
454    () => {
455        String::from("")
456    };
457    ($top:tt) => ({
458        let msg = format!($top);
459        $crate::colored(&msg).to_string()
460    });
461    ($top:tt, $($arg:tt)*) => ({
462        let msg = format!($top, $($arg)*);
463        $crate::colored(&msg).to_string()
464    });
465}
466
467/// Print colored text to standard output.
468///
469/// With nothing given does nothing.  
470/// Otherwise format given parameters using `format!` macro, apply [`colored()`], then [`print!`] to standard output.
471///
472/// # Examples
473///
474/// Basic usage:
475///
476/// ```
477/// use colored_str::colored;
478/// 
479/// colored!("<red>this is red text</red>");
480/// colored!("<red>this is {} text</red>", "red");
481/// ```
482/// 
483/// See [crate] for other examples
484#[macro_export]
485macro_rules! colored {
486    () => {
487        print!()
488    };
489    ($top:tt) => {
490        let msg = format!($top);
491        print!("{}", $crate::colored(&msg));
492    };
493    ($top:tt, $($arg:tt)*) => {
494        let msg = format!($top, $($arg)*);
495        print!("{}", $crate::colored(&msg));
496    };
497}
498
499
500/// Print colored text to standard output with newline at the end.
501///
502/// With nothing given does nothing.  
503/// Otherwise format given parameters using `format!` macro, apply [`colored()`], then [`println!`] to standard output.
504///
505/// # Examples
506///
507/// Basic usage:
508///
509/// ```
510/// use colored_str::coloredln;
511/// 
512/// coloredln!("<red>this is red text</red>");
513/// coloredln!("<red>this is {} text</red>", "red");
514/// ```
515/// 
516/// See [crate] for other examples
517#[macro_export]
518macro_rules! coloredln {
519    () => {
520        println!()
521    };
522    ($top:tt) => {
523        let msg = format!($top);
524        println!("{}", $crate::colored(&msg));
525    };
526    ($top:tt, $($arg:tt)*) => {
527        let msg = format!($top, $($arg)*);
528        println!("{}", $crate::colored(&msg));
529    };
530}