#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(missing_docs)]
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]
mod styled_str;
use anstyle::{Ansi256Color, AnsiColor, Color, RgbColor, Style};
use anstyle_lossy::palette::Palette;
use roff::{bold, italic, Roff};
use styled_str::StyledStr;
mod control_requests {
pub(crate) const CREATE_COLOR: &str = "defcolor";
pub(crate) const BACKGROUND: &str = "fcolor";
pub(crate) const FOREGROUND: &str = "gcolor";
}
pub fn to_roff(styled_text: &str) -> Roff {
let mut doc = Roff::new();
for styled in styled_str::styled_stream(styled_text) {
set_color(
(&styled.style.get_fg_color(), &styled.style.get_bg_color()),
&mut doc,
);
set_effects_and_text(&styled, &mut doc);
}
doc
}
fn set_effects_and_text(styled: &StyledStr<'_>, doc: &mut Roff) {
let effects = styled.style.get_effects();
if effects.contains(anstyle::Effects::BOLD) | has_bright_fg(&styled.style) {
doc.text(vec![bold(styled.text)]);
} else if effects.contains(anstyle::Effects::ITALIC) {
doc.text(vec![italic(styled.text)]);
} else {
doc.text(vec![roff::roman(styled.text)]);
}
}
fn has_bright_fg(style: &Style) -> bool {
style
.get_fg_color()
.as_ref()
.map(is_bright)
.unwrap_or(false)
}
fn is_bright(fg_color: &Color) -> bool {
if let Color::Ansi(color) = fg_color {
matches!(
color,
AnsiColor::BrightRed
| AnsiColor::BrightBlue
| AnsiColor::BrightBlack
| AnsiColor::BrightCyan
| AnsiColor::BrightGreen
| AnsiColor::BrightWhite
| AnsiColor::BrightYellow
| AnsiColor::BrightMagenta
)
} else {
false
}
}
type ColorSet<'a> = (&'a Option<Color>, &'a Option<Color>);
fn set_color(colors: ColorSet<'_>, doc: &mut Roff) {
add_color_to_roff(doc, control_requests::FOREGROUND, colors.0);
add_color_to_roff(doc, control_requests::BACKGROUND, colors.1);
}
fn add_color_to_roff(doc: &mut Roff, control_request: &str, color: &Option<Color>) {
match color {
Some(Color::Rgb(c)) => {
let name = rgb_name(c);
doc.control(
control_requests::CREATE_COLOR,
vec![name.as_str(), "rgb", to_hex(c).as_str()],
)
.control(control_request, vec![name.as_str()]);
}
Some(Color::Ansi(c)) => {
doc.control(control_request, vec![ansi_color_to_roff(c)]);
}
Some(Color::Ansi256(c)) => {
add_color_to_roff(doc, control_request, &Some(xterm_to_ansi_or_rgb(*c)));
}
None => {
doc.control(control_request, vec!["default"]);
}
}
}
fn xterm_to_ansi_or_rgb(color: Ansi256Color) -> Color {
match color.into_ansi() {
Some(ansi_color) => Color::Ansi(ansi_color),
None => Color::Rgb(anstyle_lossy::xterm_to_rgb(color, Palette::default())),
}
}
fn rgb_name(c: &RgbColor) -> String {
format!("hex_{}", to_hex(c).as_str())
}
fn to_hex(rgb: &RgbColor) -> String {
let val: usize = ((rgb.0 as usize) << 16) + ((rgb.1 as usize) << 8) + (rgb.2 as usize);
format!("#{:06x}", val)
}
fn ansi_color_to_roff(color: &AnsiColor) -> &'static str {
match color {
AnsiColor::Black | AnsiColor::BrightBlack => "black",
AnsiColor::Red | AnsiColor::BrightRed => "red",
AnsiColor::Green | AnsiColor::BrightGreen => "green",
AnsiColor::Yellow | AnsiColor::BrightYellow => "yellow",
AnsiColor::Blue | AnsiColor::BrightBlue => "blue",
AnsiColor::Magenta | AnsiColor::BrightMagenta => "magenta",
AnsiColor::Cyan | AnsiColor::BrightCyan => "cyan",
AnsiColor::White | AnsiColor::BrightWhite => "white",
}
}
#[cfg(test)]
mod tests {
use super::*;
use anstyle::RgbColor;
#[test]
fn test_to_hex() {
assert_eq!(to_hex(&RgbColor(0, 0, 0)).as_str(), "#000000");
assert_eq!(to_hex(&RgbColor(255, 0, 0)).as_str(), "#ff0000");
assert_eq!(to_hex(&RgbColor(0, 255, 0)).as_str(), "#00ff00");
assert_eq!(to_hex(&RgbColor(0, 0, 255)).as_str(), "#0000ff");
}
}