fancyer 0.2.1

Easily print colored text at runtime.
Documentation

//! This crate provides a runtime versio of the `colorize!()`
//! macro found in the `fancy` crate.
//! For more information see it's documentaion.

use std::ops::Range;
use std::iter::once;

/// Colorize and format a string at runtime.
/// 
/// This function interprets a color formatted string. It doesn't print anything,
/// but returns the formatted string.
/// It can be used to parse a colored string at runtime, which allows
/// colored strings to be dynamically created.
/// 
/// # Example
/// 
/// ```rust
/// let msg = colorize("[blue|b]Hello [:u|red]world[:b]!".into());
/// println!("{}", msg);
/// ```
/// 
pub fn colorize(text: String) -> String {

    let mut colored = ColorString::new(text.capacity());

    let mut range = Range { start: 0, end: 0 };
    let mut brackets = 0;

    let third = |num| once('\x1b').chain(text.chars()).skip(num);
    let mut chrs1 = third(0);
    let mut chrs2 = third(1);
    let mut chrs3 = third(2);
    let mut chrsi = text.chars().enumerate().map(|v| v.0 );

    loop {

        let idx = match chrsi.next()  { Some(v) => v, None => break, };
        let last = match chrs1.next() { Some(v) => v, None => break, };
        let curr = match chrs2.next() { Some(v) => v, None => break, };
        let next = match chrs3.next() { Some(v) => v, None => '\x1b', }; 

        if curr == '[' && last != '[' && next != '[' && brackets == 0 { range.start = idx + 1; brackets += 1 }
        else if curr == '[' && last != '[' && next != '[' && brackets != 0 { panic!("At string index {}: Cannot color-format inside a pattern.", idx); }
        
        else if curr == ']' && last != ']' && brackets > 0 {
            brackets -= 1;
            range.end = idx;
            if parse(&text[range.clone()], &mut colored) == false {
                panic!("Invalid mode.");
            };
        }
        
        else if curr == ']' && last != ']' && next != ']' && brackets == 0 { panic!("At string index {}: Unmatched ']' in color format sequence.", idx); }

        else if curr == '[' && next == '[' {  }
        else if curr == ']' && next == ']' {  }

        else if brackets == 0 && curr != '\x1b' { colored.push(curr) };

    }
    
    if brackets > 0 { panic!("Unclosed '[' in color format sequence."); };

    colored.raw("\x1b[0m");
    colored.view()

}

fn parse(text: &str, buffer: &mut ColorString) -> bool {

    let mut view = Range { start: 0, end: text.len() };

    // reset
    if text.chars().nth(0) == Some(':') {
        buffer.raw("\x1b[0m");
        view.start = 1;
    };

    // parse modifiers
    let modifiers = (&text[view]).split('|');
    for modi in modifiers {

        match &modi[..] {

            // styles
            "bold"          | "b"     => buffer.add("1"),
            "dim"           | "faint" => buffer.add("2"),
            "italic"        | "i"     => buffer.add("3"),
            "underline"     | "u"     => buffer.add("4"),
            "inverse"       | "!"     => buffer.add("7"),
            "hidden"                  => buffer.add("8"),
            "strikethrough" | "s"     => buffer.add("9"),

            // "bright" | "br"   => buffer.add("1"),
            
            // foreground colors
            "black"           => buffer.add("30"),
            "red"             => buffer.add("31"),
            "green"           => buffer.add("32"),
            "yellow"          => buffer.add("33"),
            "blue"            => buffer.add("34"),
            "magenta"         => buffer.add("35"),
            "cyan"            => buffer.add("36"),
            "white"           => buffer.add("37"),
            "default" | "def" => buffer.add("39"),
            
            // background colors
            "?black"            => buffer.add("40"),
            "?red"              => buffer.add("41"),
            "?green"            => buffer.add("42"),
            "?yellow"           => buffer.add("43"),
            "?blue"             => buffer.add("44"),
            "?magenta"          => buffer.add("45"),
            "?cyan"             => buffer.add("46"),
            "?white"            => buffer.add("47"),
            "?default" | "?def" => buffer.add("49"),

            "visible" | "vis" => buffer.raw("\x1b[?25l"),
            "invisible" | "invis" => buffer.raw("\x1b[?25h"),

            "blink" => buffer.raw("\x1b[5m"),
            "noblink" => buffer.raw("\x1b[25m"),
            
            seq => {
                
                let isdec = |seq: &str| seq.chars().all(|v| matches!(v, '0'..='9'));
                let ishex = |seq: &str| seq.chars().all(|v| matches!(v, '0'..='9' | 'a'..='f' | 'A'..='B'));

                //`? ansi color id
                if (1..=3).contains(&seq.len()) && isdec(seq) {
                    buffer.add(&format!("38;5;{}", seq));
                }
                
                //? ansi color id background
                else if (2..=4).contains(&seq.len()) && &seq[0..=1] == "?" && isdec(&seq[1..]) {
                    buffer.add(&format!("48;5;{}", seq));
                }
                
                //? ansi color id background
                else if (4..).contains(&seq.len()) && (&seq[0..=1] == "?" && isdec(&seq[1..])) | isdec(&seq[..]) {

                    // todo actually check for the ranges of the color code (0.255)
                    panic!("Invalid ansi color code: {}. Ansi color codes must be in range 0.155.", seq);

                }
                
                //? hex color code
                else if seq.len() == 7 && &seq[0..1] == "#" && ishex(&seq[1..]) {
                    let red = u8::from_str_radix(&seq[1..=2], 16).unwrap();
                    let green = u8::from_str_radix(&seq[3..=4], 16).unwrap();
                    let blue = u8::from_str_radix(&seq[5..=6], 16).unwrap();
                    buffer.add(&format!("38;2;{};{};{}", red, green, blue));
                }
                
                //? hex color code background
                else if seq.len() == 8 && &seq[0..=1] == "?#" && ishex(&seq[2..]) {
                    let red = u8::from_str_radix(&seq[2..=3], 16).unwrap();
                    let green = u8::from_str_radix(&seq[4..=5], 16).unwrap();
                    let blue = u8::from_str_radix(&seq[6..=7], 16).unwrap();
                    buffer.add(&format!("48;2;{};{};{}", red, green, blue));
                }
                
                //? invalid hex color code 
                else if (2..).contains(&seq.len()) && (&seq[0..=1]).contains('#') {
                    panic!("Invalid hex color code: {}. Hex color codes must be a '#' or '?#' followed by exactly 6 hex-digits.", seq);
                }
                
                else if !seq.is_empty() {
                    panic!("Unknown modifier: {:?}", seq);
                }

            },

        }
        
    }

    buffer.next();
    true

}

#[derive(Debug)]
struct ColorString {
    text: String,
    next: String,
}

impl ColorString {

    pub(crate) fn new(cap: usize) -> Self {
        Self { text: String::with_capacity(cap), next: String::with_capacity(16) }
    }

    pub(crate) fn add(&mut self, style: &str) {
        if !self.next.is_empty() { self.next.push(';'); };
        self.next.push_str(&format!("{}", style));
    }

    pub(crate) fn raw(&mut self, style: &str) {
        self.next();
        self.text.push_str(style);
    }

    pub(crate) fn next(&mut self) {
        if !self.next.is_empty() {
            self.text.push_str(&format!("\x1b[{}m", self.next));
            self.next.clear();
        };
    }

    pub(crate) fn push(&mut self, chr: char) {
        self.text.push(chr);
    }

    pub(crate) fn view(self) -> String {
        self.text
    }

}