anstyle_roff/
lib.rs

1//! Convert from ansi stylings to ROFF Control Lines
2//! Currently uses [roff](https://docs.rs/roff/0.2.1/roff/) as the engine for generating
3//! roff output.
4
5#![cfg_attr(docsrs, feature(doc_auto_cfg))]
6#![warn(missing_docs)]
7#![warn(clippy::print_stderr)]
8#![warn(clippy::print_stdout)]
9
10mod styled_str;
11use anstyle::{Ansi256Color, AnsiColor, Color, RgbColor, Style};
12use anstyle_lossy::palette::Palette;
13use roff::{bold, italic, Roff};
14use styled_str::StyledStr;
15
16/// Static Strings defining ROFF Control Requests
17mod control_requests {
18    /// Control to Create a Color definition
19    pub(crate) const CREATE_COLOR: &str = "defcolor";
20    /// Roff control request to set background color (fill color)
21    pub(crate) const BACKGROUND: &str = "fcolor";
22    /// Roff control request to set foreground color (glyph color)
23    pub(crate) const FOREGROUND: &str = "gcolor";
24}
25
26/// Generate a [`Roff`] from ANSI escape codes
27///
28/// ```rust
29/// let text = "\u{1b}[44;31mtest\u{1b}[0m";
30///
31/// let roff_doc = anstyle_roff::to_roff(text);
32/// let expected = r#".gcolor red
33/// .fcolor blue
34/// test
35/// "#;
36///
37/// assert_eq!(roff_doc.to_roff(), expected);
38/// ```
39pub fn to_roff(styled_text: &str) -> Roff {
40    let mut doc = Roff::new();
41    let mut previous_fg_color = None;
42    let mut previous_bg_color = None;
43    for styled in styled_str::styled_stream(styled_text) {
44        if previous_fg_color != styled.style.get_fg_color() {
45            add_color_to_roff(
46                &mut doc,
47                control_requests::FOREGROUND,
48                &styled.style.get_fg_color(),
49            );
50            previous_fg_color = styled.style.get_fg_color();
51        }
52        if previous_bg_color != styled.style.get_bg_color() {
53            add_color_to_roff(
54                &mut doc,
55                control_requests::BACKGROUND,
56                &styled.style.get_bg_color(),
57            );
58            previous_bg_color = styled.style.get_bg_color();
59        }
60        set_effects_and_text(&styled, &mut doc);
61    }
62    doc
63}
64
65fn set_effects_and_text(styled: &StyledStr<'_>, doc: &mut Roff) {
66    // Roff (the crate) only supports these inline commands
67    //  - Bold
68    //  - Italic
69    //  - Roman (plain text)
70    // If we want more support, or even support combined formats, we will need
71    // to push improvements to roff upstream or implement a more thorough roff crate
72    // perhaps by spinning off some of this code
73    let effects = styled.style.get_effects();
74    if effects.contains(anstyle::Effects::BOLD) | has_bright_fg(&styled.style) {
75        doc.text([bold(styled.text)]);
76    } else if effects.contains(anstyle::Effects::ITALIC) {
77        doc.text([italic(styled.text)]);
78    } else {
79        doc.text([roff::roman(styled.text)]);
80    }
81}
82
83fn has_bright_fg(style: &Style) -> bool {
84    style
85        .get_fg_color()
86        .as_ref()
87        .map(is_bright)
88        .unwrap_or(false)
89}
90
91/// Check if [`Color`] is an [`AnsiColor::Bright*`][AnsiColor] variant
92fn is_bright(fg_color: &Color) -> bool {
93    if let Color::Ansi(color) = fg_color {
94        matches!(
95            color,
96            AnsiColor::BrightRed
97                | AnsiColor::BrightBlue
98                | AnsiColor::BrightBlack
99                | AnsiColor::BrightCyan
100                | AnsiColor::BrightGreen
101                | AnsiColor::BrightWhite
102                | AnsiColor::BrightYellow
103                | AnsiColor::BrightMagenta
104        )
105    } else {
106        false
107    }
108}
109
110fn add_color_to_roff(doc: &mut Roff, control_request: &str, color: &Option<Color>) {
111    match color {
112        Some(Color::Rgb(c)) => {
113            // Adding Support for RGB colors, however cansi does not support
114            // RGB Colors, so this is not executed. If we switch to a provider
115            // That has RGB support we will also get it for Roff
116            let name = rgb_name(c);
117            doc.control(
118                control_requests::CREATE_COLOR,
119                [name.as_str(), "rgb", to_hex(c).as_str()],
120            )
121            .control(control_request, [name.as_str()]);
122        }
123
124        Some(Color::Ansi(c)) => {
125            doc.control(control_request, [ansi_color_to_roff(c)]);
126        }
127        Some(Color::Ansi256(c)) => {
128            // Adding Support for Ansi256 colors, however cansi does not support
129            // Ansi256 Colors, so this is not executed. If we switch to a provider
130            // That has Xterm support we will also get it for Roff
131            add_color_to_roff(doc, control_request, &Some(xterm_to_ansi_or_rgb(*c)));
132        }
133        None => {
134            // TODO: get rid of "default" hardcoded str?
135            doc.control(control_request, ["default"]);
136        }
137    }
138}
139
140/// Non Lossy Conversion of Xterm color to one that Roff can handle
141fn xterm_to_ansi_or_rgb(color: Ansi256Color) -> Color {
142    match color.into_ansi() {
143        Some(ansi_color) => Color::Ansi(ansi_color),
144        None => Color::Rgb(anstyle_lossy::xterm_to_rgb(color, Palette::default())),
145    }
146}
147
148fn rgb_name(c: &RgbColor) -> String {
149    format!("hex_{}", to_hex(c).as_str())
150}
151
152fn to_hex(rgb: &RgbColor) -> String {
153    let val: usize = ((rgb.0 as usize) << 16) + ((rgb.1 as usize) << 8) + (rgb.2 as usize);
154    format!("#{val:06x}")
155}
156
157/// Map Color and Bright Variants to Roff Color styles
158fn ansi_color_to_roff(color: &AnsiColor) -> &'static str {
159    match color {
160        AnsiColor::Black | AnsiColor::BrightBlack => "black",
161        AnsiColor::Red | AnsiColor::BrightRed => "red",
162        AnsiColor::Green | AnsiColor::BrightGreen => "green",
163        AnsiColor::Yellow | AnsiColor::BrightYellow => "yellow",
164        AnsiColor::Blue | AnsiColor::BrightBlue => "blue",
165        AnsiColor::Magenta | AnsiColor::BrightMagenta => "magenta",
166        AnsiColor::Cyan | AnsiColor::BrightCyan => "cyan",
167        AnsiColor::White | AnsiColor::BrightWhite => "white",
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use anstyle::RgbColor;
175
176    #[test]
177    fn test_to_hex() {
178        assert_eq!(to_hex(&RgbColor(0, 0, 0)).as_str(), "#000000");
179        assert_eq!(to_hex(&RgbColor(255, 0, 0)).as_str(), "#ff0000");
180        assert_eq!(to_hex(&RgbColor(0, 255, 0)).as_str(), "#00ff00");
181        assert_eq!(to_hex(&RgbColor(0, 0, 255)).as_str(), "#0000ff");
182    }
183}
184
185#[doc = include_str!("../README.md")]
186#[cfg(doctest)]
187pub struct ReadmeDoctests;