use std::fmt::Write;
use crate::errors::LexError;
use crate::lexer::{tokenize, EmphasisType, TagType, Token};
#[derive(Debug, PartialEq, Clone)]
pub enum Ground {
Foreground,
Background,
}
#[derive(Default, Clone, Debug)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub double_underline: bool,
pub strikethrough: bool,
pub blink: bool,
pub overline: bool,
pub invisible: bool,
pub reverse: bool,
pub rapid_blink: bool,
pub reset: bool,
pub prefix: Option<String>,
}
#[derive(Debug, PartialEq, Clone)]
pub enum NamedColor {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Color {
Named(NamedColor),
Ansi256(u8),
Rgb(u8, u8, u8),
}
impl Style {
pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
let mut res = Self {
..Default::default()
};
for tok in tokenize(markup.into())? {
match tok {
Token::Text(_) => continue,
Token::Tag(tag) => match tag {
TagType::ResetAll | TagType::ResetOne(_) => res.reset = true,
TagType::Emphasis(emphasis) => match emphasis {
EmphasisType::Dim => res.dim = true,
EmphasisType::Blink => res.blink = true,
EmphasisType::Bold => res.bold = true,
EmphasisType::Italic => res.italic = true,
EmphasisType::Strikethrough => res.strikethrough = true,
EmphasisType::Underline => res.underline = true,
EmphasisType::DoubleUnderline => res.double_underline = true,
EmphasisType::Overline => res.overline = true,
EmphasisType::Invisible => res.invisible = true,
EmphasisType::Reverse => res.reverse = true,
EmphasisType::RapidBlink => res.rapid_blink = true,
},
TagType::Color { color, ground } => match ground {
Ground::Background => res.bg = Some(color),
Ground::Foreground => res.fg = Some(color),
},
TagType::Prefix(_) => continue,
},
}
}
Ok(res)
}
}
impl NamedColor {
pub(crate) fn from_str(input: &str) -> Option<Self> {
match input {
"black" => Some(Self::Black),
"red" => Some(Self::Red),
"green" => Some(Self::Green),
"yellow" => Some(Self::Yellow),
"blue" => Some(Self::Blue),
"magenta" => Some(Self::Magenta),
"cyan" => Some(Self::Cyan),
"white" => Some(Self::White),
"bright-black" => Some(Self::BrightBlack),
"bright-red" => Some(Self::BrightRed),
"bright-green" => Some(Self::BrightGreen),
"bright-yellow" => Some(Self::BrightYellow),
"bright-blue" => Some(Self::BrightBlue),
"bright-magenta" => Some(Self::BrightMagenta),
"bright-cyan" => Some(Self::BrightCyan),
"bright-white" => Some(Self::BrightWhite),
_ => None,
}
}
}
fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
let mut seq = String::from("\x1b[");
for (i, n) in vec.iter().enumerate() {
if i != 0 {
seq.push(';');
}
write!(seq, "{n}").unwrap();
}
seq.push('m');
seq
}
fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
let addend: u8 = match param {
Ground::Background => 10,
Ground::Foreground => 0,
};
match color {
Color::Named(named) => {
ansi.push(match named {
NamedColor::Black => 30 + addend,
NamedColor::Red => 31 + addend,
NamedColor::Green => 32 + addend,
NamedColor::Yellow => 33 + addend,
NamedColor::Blue => 34 + addend,
NamedColor::Magenta => 35 + addend,
NamedColor::Cyan => 36 + addend,
NamedColor::White => 37 + addend,
NamedColor::BrightBlack => 90 + addend,
NamedColor::BrightRed => 91 + addend,
NamedColor::BrightGreen => 92 + addend,
NamedColor::BrightYellow => 93 + addend,
NamedColor::BrightBlue => 94 + addend,
NamedColor::BrightMagenta => 95 + addend,
NamedColor::BrightCyan => 96 + addend,
NamedColor::BrightWhite => 97 + addend,
});
}
Color::Ansi256(v) => {
ansi.extend_from_slice(&[38 + addend, 5, *v]);
}
Color::Rgb(r, g, b) => {
ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
}
}
}
const fn named_sgr(color: &NamedColor) -> u8 {
match color {
NamedColor::Black => 30,
NamedColor::Red => 31,
NamedColor::Green => 32,
NamedColor::Yellow => 33,
NamedColor::Blue => 34,
NamedColor::Magenta => 35,
NamedColor::Cyan => 36,
NamedColor::White => 37,
NamedColor::BrightBlack => 90,
NamedColor::BrightRed => 91,
NamedColor::BrightGreen => 92,
NamedColor::BrightYellow => 93,
NamedColor::BrightBlue => 94,
NamedColor::BrightMagenta => 95,
NamedColor::BrightCyan => 96,
NamedColor::BrightWhite => 97,
}
}
pub fn color_to_ansi(color: &Color, ground: Ground) -> String {
let add: u8 = match ground {
Ground::Background => 10,
Ground::Foreground => 0,
};
match color {
Color::Named(n) => format!("\x1b[{}m", named_sgr(n) + add),
Color::Ansi256(v) => format!("\x1b[{};5;{}m", 38 + add, v),
Color::Rgb(r, g, b) => format!("\x1b[{};2;{};{};{}m", 38 + add, r, g, b),
}
}
pub fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
let code: u8 = match emphasis {
EmphasisType::Bold => 1,
EmphasisType::Dim => 2,
EmphasisType::Italic => 3,
EmphasisType::Underline => 4,
EmphasisType::DoubleUnderline => 21,
EmphasisType::Blink => 5,
EmphasisType::RapidBlink => 6,
EmphasisType::Reverse => 7,
EmphasisType::Invisible => 8,
EmphasisType::Strikethrough => 9,
EmphasisType::Overline => 53,
};
format!("\x1b[{}m", code)
}
pub fn style_to_ansi(style: &Style) -> String {
let mut ansi: Vec<u8> = Vec::new();
if style.reset {
return String::from("\x1b[0m");
}
for (enabled, code) in [
(style.bold, 1),
(style.dim, 2),
(style.italic, 3),
(style.underline, 4),
(style.double_underline, 21),
(style.blink, 5),
(style.rapid_blink, 6),
(style.reverse, 7),
(style.invisible, 8),
(style.strikethrough, 9),
(style.overline, 53),
] {
if enabled {
ansi.push(code);
}
}
if let Some(fg) = &style.fg {
encode_color_sgr(&mut ansi, Ground::Foreground, fg);
}
if let Some(bg) = &style.bg {
encode_color_sgr(&mut ansi, Ground::Background, bg);
}
if ansi.is_empty() {
return String::new();
}
vec_to_ansi_seq(ansi)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::EmphasisType;
#[test]
fn test_named_color_from_str_known_colors() {
assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
}
#[test]
fn test_named_color_from_str_unknown_returns_none() {
assert_eq!(NamedColor::from_str("purple"), None);
}
#[test]
fn test_named_color_from_str_case_sensitive() {
assert_eq!(NamedColor::from_str("Red"), None);
assert_eq!(NamedColor::from_str("RED"), None);
}
#[test]
fn test_named_color_from_str_empty_returns_none() {
assert_eq!(NamedColor::from_str(""), None);
}
#[test]
fn test_vec_to_ansi_seq_single_param() {
let result = vec_to_ansi_seq(vec![1]);
assert_eq!(result, "\x1b[1m");
}
#[test]
fn test_vec_to_ansi_seq_multiple_params() {
let result = vec_to_ansi_seq(vec![1, 31]);
assert_eq!(result, "\x1b[1;31m");
}
#[test]
fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
let result = vec_to_ansi_seq(vec![]);
assert_eq!(result, "\x1b[m");
}
#[test]
fn test_color_to_ansi_named_foreground() {
let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
assert_eq!(result, "\x1b[31m");
}
#[test]
fn test_color_to_ansi_named_background() {
let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
assert_eq!(result, "\x1b[41m");
}
#[test]
fn test_color_to_ansi_ansi256_foreground() {
let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
assert_eq!(result, "\x1b[38;5;200m");
}
#[test]
fn test_color_to_ansi_ansi256_background() {
let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
assert_eq!(result, "\x1b[48;5;100m");
}
#[test]
fn test_color_to_ansi_rgb_foreground() {
let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
assert_eq!(result, "\x1b[38;2;255;128;0m");
}
#[test]
fn test_color_to_ansi_rgb_background() {
let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
assert_eq!(result, "\x1b[48;2;0;0;255m");
}
#[test]
fn test_color_to_ansi_rgb_zero_values() {
let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
assert_eq!(result, "\x1b[38;2;0;0;0m");
}
#[test]
fn test_emphasis_to_ansi_bold() {
assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
}
#[test]
fn test_emphasis_to_ansi_dim() {
assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
}
#[test]
fn test_emphasis_to_ansi_italic() {
assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
}
#[test]
fn test_emphasis_to_ansi_underline() {
assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
}
#[test]
fn test_emphasis_to_ansi_blink() {
assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
}
#[test]
fn test_emphasis_to_ansi_strikethrough() {
assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
}
#[test]
fn test_style_to_ansi_empty_style_returns_empty_string() {
let style = Style {
fg: None,
bg: None,
bold: false,
dim: false,
italic: false,
underline: false,
strikethrough: false,
blink: false,
..Default::default()
};
assert_eq!(style_to_ansi(&style), "");
}
#[test]
fn test_style_to_ansi_bold_only() {
let style = Style {
fg: None,
bg: None,
bold: true,
dim: false,
italic: false,
underline: false,
strikethrough: false,
blink: false,
..Default::default()
};
assert_eq!(style_to_ansi(&style), "\x1b[1m");
}
#[test]
fn test_style_to_ansi_bold_with_foreground_color() {
let style = Style {
fg: Some(Color::Named(NamedColor::Green)),
bg: None,
bold: true,
dim: false,
italic: false,
underline: false,
strikethrough: false,
blink: false,
..Default::default()
};
assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
}
#[test]
fn test_style_to_ansi_fg_and_bg() {
let style = Style {
fg: Some(Color::Named(NamedColor::White)),
bg: Some(Color::Named(NamedColor::Blue)),
bold: false,
dim: false,
italic: false,
underline: false,
strikethrough: false,
blink: false,
..Default::default()
};
assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
}
#[test]
fn test_style_to_ansi_all_emphasis_flags() {
let style = Style {
fg: None,
bg: None,
bold: true,
dim: true,
italic: true,
underline: true,
strikethrough: true,
blink: true,
..Default::default()
};
assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
}
}