1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Convert from ansi stylings to ROFF Control Lines
//! Currently uses [roff](https://docs.rs/roff/0.2.1/roff/) as the engine for generating
//! roff output.

#![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;

/// Static Strings defining ROFF Control Requests
mod control_requests {
    /// Control to Create a Color definition
    pub(crate) const CREATE_COLOR: &str = "defcolor";
    /// Roff control request to set background color (fill color)
    pub(crate) const BACKGROUND: &str = "fcolor";
    /// Roff control request to set foreground color (glyph color)
    pub(crate) const FOREGROUND: &str = "gcolor";
}

/// Generate a [`Roff`] from ANSI escape codes
///
/// ```rust
/// let text = "\u{1b}[44;31mtest\u{1b}[0m";
///
/// let roff_doc = anstyle_roff::to_roff(text);
/// let expected = r#".gcolor red
/// .fcolor blue
/// test
/// "#;
///
/// assert_eq!(roff_doc.to_roff(), expected);
/// ```
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) {
    // Roff (the crate) only supports these inline commands
    //  - Bold
    //  - Italic
    //  - Roman (plain text)
    // If we want more support, or even support combined formats, we will need
    // to push improvements to roff upstream or implement a more thorough roff crate
    // perhaps by spinning off some of this code
    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)
}

/// Check if [`Color`] is an [`AnsiColor::Bright*`][AnsiColor] variant
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)) => {
            // Adding Support for RGB colors, however cansi does not support
            // RGB Colors, so this is not executed. If we switch to a provider
            // That has RGB support we will also get it for Roff
            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)) => {
            // Adding Support for Ansi256 colors, however cansi does not support
            // Ansi256 Colors, so this is not executed. If we switch to a provider
            // That has Xterm support we will also get it for Roff
            add_color_to_roff(doc, control_request, &Some(xterm_to_ansi_or_rgb(*c)));
        }
        None => {
            // TODO: get rid of "default" hardcoded str?
            doc.control(control_request, vec!["default"]);
        }
    }
}

/// Non Lossy Conversion of Xterm color to one that Roff can handle
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)
}

/// Map Color and Bright Variants to Roff Color styles
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");
    }
}