Skip to main content

styled_str/rich_parser/
mod.rs

1//! Rich style parsing (incl. in compile time).
2
3use core::{fmt, num::NonZeroUsize, ops, str::FromStr};
4
5use anstyle::{Ansi256Color, AnsiColor, Color, Effects, RgbColor, Style};
6
7use crate::{
8    HexColorError, ParseError, ParseErrorKind, StackStyled, StyledString,
9    alloc::{Cow, String, Vec},
10    types::{StyledSpan, StyledStr},
11    utils::{Stack, StackStr, StrCursor, is_same_style, normalize_style},
12};
13
14#[cfg(test)]
15mod tests;
16
17/// Parses a hexadecimal RGB color like `#c0ffee` or `#fa4`.
18///
19/// # Errors
20///
21/// Returns a parsing error if the string is invalid.
22///
23/// # Examples
24///
25/// ```
26/// # use styled_str::parse_hex_color;
27/// # use anstyle::RgbColor;
28/// let color = parse_hex_color(b"#fb4")?;
29/// assert_eq!(color, RgbColor(0xff, 0xbb, 0x44));
30///
31/// let color = parse_hex_color(b"#c0ffee")?;
32/// assert_eq!(color, RgbColor(0xc0, 0xff, 0xee));
33/// # anyhow::Ok(())
34/// ```
35pub const fn parse_hex_color(hex: &[u8]) -> Result<RgbColor, HexColorError> {
36    if hex.is_empty() || hex[0] != b'#' {
37        return Err(HexColorError::NoHash);
38    }
39
40    if hex.len() == 4 {
41        let r = const_try!(parse_hex_digit(hex[1]));
42        let g = const_try!(parse_hex_digit(hex[2]));
43        let b = const_try!(parse_hex_digit(hex[3]));
44        Ok(RgbColor(r * 17, g * 17, b * 17))
45    } else if hex.len() == 7 {
46        let r = const_try!(parse_hex_digit(hex[1])) * 16 + const_try!(parse_hex_digit(hex[2]));
47        let g = const_try!(parse_hex_digit(hex[3])) * 16 + const_try!(parse_hex_digit(hex[4]));
48        let b = const_try!(parse_hex_digit(hex[5])) * 16 + const_try!(parse_hex_digit(hex[6]));
49        Ok(RgbColor(r, g, b))
50    } else {
51        Err(HexColorError::InvalidLen)
52    }
53}
54
55const fn parse_hex_digit(ch: u8) -> Result<u8, HexColorError> {
56    match ch {
57        b'0'..=b'9' => Ok(ch - b'0'),
58        b'a'..=b'f' => Ok(ch - b'a' + 10),
59        b'A'..=b'F' => Ok(ch - b'A' + 10),
60        _ => Err(HexColorError::InvalidHexDigit),
61    }
62}
63
64/// Converts an RGB color to its rich text representation, like `#fb4` or `#c0ffee`.
65///
66/// # Examples
67///
68/// ```
69/// # use styled_str::rgb_color_to_hex;
70/// # use anstyle::RgbColor;
71/// let color = RgbColor(65, 65, 255);
72/// assert_eq!(rgb_color_to_hex(color), "#4141ff");
73///
74/// let color = RgbColor(255, 0, 255);
75/// assert_eq!(rgb_color_to_hex(color), "#f0f");
76/// ```
77pub fn rgb_color_to_hex(RgbColor(r, g, b): RgbColor) -> String {
78    use core::fmt::Write as _;
79
80    let mut buffer = String::new();
81    if r % 17 == 0 && g % 17 == 0 && b % 17 == 0 {
82        write!(&mut buffer, "#{:x}{:x}{:x}", r / 17, g / 17, b / 17).unwrap();
83    } else {
84        write!(&mut buffer, "#{r:02x}{g:02x}{b:02x}").unwrap();
85    }
86    buffer
87}
88
89impl StyledStr<'static> {
90    #[doc(hidden)] // used in the `styled!` macro; logically private
91    pub const fn capacities(raw: &str) -> (usize, usize) {
92        let mut cursor = StrCursor::new(raw);
93        let mut text_len = 0;
94        let mut span_count = 0;
95        let mut style_end_pos = 0;
96        while !cursor.is_eof() {
97            if cursor.gobble("[[") {
98                while !cursor.is_eof() && cursor.current_byte() == b'[' {
99                    cursor.advance_byte();
100                }
101
102                let end_pos = cursor.pos() - 2;
103                if end_pos - style_end_pos > 0 {
104                    span_count += 1;
105                    text_len += end_pos - style_end_pos;
106                }
107                while !cursor.is_eof() && !cursor.gobble("]]") {
108                    cursor.advance_byte();
109                }
110                style_end_pos = cursor.pos();
111            } else {
112                cursor.advance_byte();
113            }
114        }
115
116        if raw.len() - style_end_pos > 0 {
117            span_count += 1;
118            text_len += raw.len() - style_end_pos;
119        }
120        (text_len, span_count)
121    }
122}
123
124#[derive(Debug)]
125struct SetStyleFields(u16);
126
127impl SetStyleFields {
128    const BOLD: u16 = 1;
129    const ITALIC: u16 = 2;
130    const UNDERLINE: u16 = 4;
131    const STRIKETHROUGH: u16 = 8;
132    const BLINK: u16 = 16;
133    const INVERT: u16 = 32;
134    const HIDDEN: u16 = 64;
135    const DIMMED: u16 = 128;
136
137    const FG: u16 = 256;
138    const BG: u16 = 512;
139
140    const fn new() -> Self {
141        Self(0)
142    }
143
144    const fn from_style(style: &Style) -> Self {
145        let mut this = Self::new();
146        this.set_effects(style.get_effects());
147        if style.get_fg_color().is_some() {
148            this.set_fg();
149        }
150        if style.get_bg_color().is_some() {
151            this.set_bg();
152        }
153        this
154    }
155
156    const fn set_effects(&mut self, effects: Effects) -> bool {
157        let prev = self.0;
158
159        if effects.contains(Effects::BOLD) {
160            self.0 |= Self::BOLD;
161        }
162        if effects.contains(Effects::ITALIC) {
163            self.0 |= Self::ITALIC;
164        }
165        if effects.contains(Effects::UNDERLINE) {
166            self.0 |= Self::UNDERLINE;
167        }
168        if effects.contains(Effects::STRIKETHROUGH) {
169            self.0 |= Self::STRIKETHROUGH;
170        }
171        if effects.contains(Effects::BLINK) {
172            self.0 |= Self::BLINK;
173        }
174        if effects.contains(Effects::INVERT) {
175            self.0 |= Self::INVERT;
176        }
177        if effects.contains(Effects::HIDDEN) {
178            self.0 |= Self::HIDDEN;
179        }
180        if effects.contains(Effects::DIMMED) {
181            self.0 |= Self::DIMMED;
182        }
183
184        self.0 != prev
185    }
186
187    const fn set_fg(&mut self) -> bool {
188        let prev = self.0;
189        self.0 |= Self::FG;
190        self.0 != prev
191    }
192
193    const fn set_bg(&mut self) -> bool {
194        let prev = self.0;
195        self.0 |= Self::BG;
196        self.0 != prev
197    }
198}
199
200/// Parser for `rich` like styling, e.g. `[[bold red on white]]text[[/]]`.
201#[derive(Debug)]
202struct RichParser<'a> {
203    cursor: StrCursor<'a>,
204    current_style: Style,
205    style_end_pos: usize,
206}
207
208impl<'a> RichParser<'a> {
209    const fn new(raw: &'a str) -> Self {
210        Self {
211            cursor: StrCursor::new(raw),
212            current_style: Style::new(),
213            style_end_pos: 0,
214        }
215    }
216
217    const fn step(&mut self) -> Result<ops::ControlFlow<(), StyledSpan>, ParseError> {
218        if self.cursor.is_eof() {
219            // Push the final span if necessary.
220            return Ok(self.final_step());
221        }
222
223        while !self.cursor.is_eof() {
224            if self.cursor.gobble("[[") {
225                // Gobble any additional `[`s. It's always right to interpret the rightmost sequence of `[`
226                // as the start of the style, because the style itself cannot contain `[`s.
227                while !self.cursor.is_eof() && self.cursor.current_byte() == b'[' {
228                    self.cursor.advance_byte();
229                }
230                if self.cursor.is_eof() {
231                    return Err(self.cursor.error(ParseErrorKind::UnfinishedStyle));
232                }
233
234                // Push the previous styled span
235                let end_pos = self.cursor.pos() - 2;
236                let span = if let Some(len) = NonZeroUsize::new(end_pos - self.style_end_pos) {
237                    Some(StyledSpan {
238                        style: self.current_style,
239                        start: self.style_end_pos,
240                        len,
241                    })
242                } else {
243                    None
244                };
245
246                self.current_style = const_try!(self.cursor.parse_style(&self.current_style, true));
247                self.style_end_pos = self.cursor.pos();
248
249                if let Some(span) = span {
250                    return Ok(ops::ControlFlow::Continue(span));
251                }
252            } else {
253                let ch = self.cursor.current_byte();
254                if ch == 0x1b {
255                    return Err(self.cursor.error(ParseErrorKind::EscapeInText));
256                }
257                self.cursor.advance_byte();
258            }
259        }
260
261        Ok(self.final_step())
262    }
263
264    const fn final_step(&mut self) -> ops::ControlFlow<(), StyledSpan> {
265        if let Some(len) = NonZeroUsize::new(self.cursor.pos() - self.style_end_pos) {
266            let span = StyledSpan {
267                style: self.current_style,
268                start: self.style_end_pos,
269                len,
270            };
271            self.style_end_pos = self.cursor.pos();
272            ops::ControlFlow::Continue(span)
273        } else {
274            ops::ControlFlow::Break(())
275        }
276    }
277}
278
279const fn strip_prefix<'s>(haystack: &'s [u8], prefix: &[u8]) -> Option<&'s [u8]> {
280    if haystack.len() < prefix.len() {
281        return None;
282    }
283    let mut i = 0;
284    while i < prefix.len() {
285        if haystack[i] != prefix[i] {
286            return None;
287        }
288        i += 1;
289    }
290    let (_, suffix) = haystack.split_at(prefix.len());
291    Some(suffix)
292}
293
294impl StrCursor<'_> {
295    /// Produces the error spanned at the next char.
296    const fn error(&self, kind: ParseErrorKind) -> ParseError {
297        let range = if self.is_eof() {
298            self.pos()..self.pos()
299        } else {
300            self.expand_to_char_boundaries(self.pos()..self.pos() + 1)
301        };
302        kind.with_pos(range)
303    }
304
305    const fn error_on_empty_token(&self, unfinished: ParseErrorKind) -> ParseError {
306        if self.is_eof() {
307            unfinished.with_pos(self.pos()..self.pos())
308        } else {
309            let next_char_pos = self.expand_to_char_boundaries(self.pos()..self.pos() + 1);
310            ParseErrorKind::BogusDelimiter.with_pos(next_char_pos)
311        }
312    }
313
314    /// The cursor is positioned just after the opening `[[`.
315    const fn parse_style(
316        &mut self,
317        current_style: &Style,
318        expect_closing_bracket: bool,
319    ) -> Result<Style, ParseError> {
320        let mut style = Style::new();
321        let mut is_initial = true;
322        let mut closing_bracket = false;
323        let mut copied_fields = None;
324        let mut set_fields = SetStyleFields::new();
325        let mut clear_pos = None::<ops::Range<usize>>;
326
327        while !self.is_eof() {
328            self.gobble_whitespace();
329            if !is_initial && self.gobble_punct() {
330                self.gobble_whitespace();
331            }
332
333            if expect_closing_bracket && self.gobble("]]") {
334                closing_bracket = true;
335                break;
336            }
337            if self.is_eof() {
338                break;
339            }
340
341            if let Some(clear_pos) = clear_pos {
342                return Err(ParseErrorKind::NonIsolatedClear.with_pos(clear_pos));
343            }
344
345            let token_range = self.take_token();
346            let token = self.range(&token_range);
347            if matches!(token, b"*") {
348                if !is_initial {
349                    return Err(ParseErrorKind::NonInitialCopy.with_pos(token_range));
350                }
351                copied_fields = Some(SetStyleFields::from_style(current_style));
352                style = *current_style;
353            } else if matches!(token, b"/") {
354                if !is_initial {
355                    return Err(ParseErrorKind::NonIsolatedClear.with_pos(token_range));
356                }
357                clear_pos = Some(token_range);
358            } else {
359                const_try!(self.parse_token(
360                    &mut style,
361                    token_range,
362                    &mut set_fields,
363                    copied_fields.as_mut(),
364                ));
365            }
366
367            is_initial = false;
368        }
369
370        if expect_closing_bracket && !closing_bracket {
371            Err(self.error(ParseErrorKind::UnfinishedStyle))
372        } else {
373            Ok(normalize_style(style))
374        }
375    }
376
377    const fn parse_token(
378        &mut self,
379        style: &mut Style,
380        token_range: ops::Range<usize>,
381        set_fields: &mut SetStyleFields,
382        copied_fields: Option<&mut SetStyleFields>,
383    ) -> Result<(), ParseError> {
384        let token = self.range(&token_range);
385        match token {
386            b"" => {
387                return Err(self.error_on_empty_token(ParseErrorKind::UnfinishedStyle));
388            }
389
390            b"on" => {
391                if !set_fields.set_bg() {
392                    return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
393                }
394
395                self.gobble_whitespace();
396                if self.is_eof() {
397                    return Err(self.error(ParseErrorKind::UnfinishedStyle));
398                }
399
400                let on_token_range = token_range;
401                let token_range = self.take_token();
402                let token = self.range(&token_range);
403                let color = match Self::parse_color(token) {
404                    Ok(Some(color)) => color,
405                    Ok(None) => {
406                        return Err(ParseErrorKind::UnfinishedBackground.with_pos(on_token_range));
407                    }
408                    Err(err) => return Err(err.with_pos(token_range)),
409                };
410                *style = style.bg_color(Some(color));
411            }
412
413            b"-color" | b"-fg" | b"!color" | b"!fg" => {
414                if !set_fields.set_fg() {
415                    return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
416                }
417                let Some(copied_fields) = copied_fields else {
418                    // This can be checked earlier, but we'd like to return an `UnsupportedEffect` error
419                    // on a bogus specifier.
420                    return Err(ParseErrorKind::NegationWithoutCopy.with_pos(token_range));
421                };
422                if copied_fields.set_fg() {
423                    // The effect wasn't set in the copied style
424                    return Err(ParseErrorKind::RedundantNegation.with_pos(token_range));
425                }
426                *style = style.fg_color(None);
427            }
428            b"-on" | b"-bg" | b"!on" | b"!bg" => {
429                if !set_fields.set_bg() {
430                    return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
431                }
432                let Some(copied_fields) = copied_fields else {
433                    // This can be checked earlier, but we'd like to return an `UnsupportedEffect` error
434                    // on a bogus specifier.
435                    return Err(ParseErrorKind::NegationWithoutCopy.with_pos(token_range));
436                };
437                if copied_fields.set_bg() {
438                    // The effect wasn't set in the copied style
439                    return Err(ParseErrorKind::RedundantNegation.with_pos(token_range));
440                }
441                *style = style.bg_color(None);
442            }
443            neg if neg[0] == b'-' || neg[0] == b'!' => {
444                let (_, token_without_prefix) = token.split_at(1);
445                let Some(effects) = Self::parse_effects(token_without_prefix) else {
446                    return Err(ParseErrorKind::UnsupportedEffect.with_pos(token_range));
447                };
448                let Some(copied_fields) = copied_fields else {
449                    // This can be checked earlier, but we'd like to return an `UnsupportedEffect` error
450                    // on a bogus specifier.
451                    return Err(ParseErrorKind::NegationWithoutCopy.with_pos(token_range));
452                };
453
454                if !set_fields.set_effects(effects) {
455                    return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
456                }
457                if copied_fields.set_effects(effects) {
458                    // The effect wasn't set in the copied style
459                    return Err(ParseErrorKind::RedundantNegation.with_pos(token_range));
460                }
461
462                *style = style.effects(style.get_effects().remove(effects));
463            }
464
465            _ => {
466                if let Some(effects) = Self::parse_effects(token) {
467                    if !set_fields.set_effects(effects) {
468                        return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
469                    }
470                    *style = style.effects(style.get_effects().insert(effects));
471                } else {
472                    let color = match Self::parse_color(token) {
473                        Ok(Some(color)) => color,
474                        Ok(None) => {
475                            return Err(ParseErrorKind::UnsupportedStyle.with_pos(token_range));
476                        }
477                        Err(err) => return Err(err.with_pos(token_range)),
478                    };
479                    if !set_fields.set_fg() {
480                        return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
481                    }
482                    *style = style.fg_color(Some(color));
483                }
484            }
485        }
486        Ok(())
487    }
488
489    const fn parse_effects(token: &[u8]) -> Option<Effects> {
490        Some(match token {
491            b"bold" | b"b" => Effects::BOLD,
492            b"italic" | b"i" | b"it" => Effects::ITALIC,
493            b"underline" | b"u" | b"ul" => Effects::UNDERLINE,
494            b"strikethrough" | b"strike" | b"s" => Effects::STRIKETHROUGH,
495            b"dim" | b"dimmed" => Effects::DIMMED,
496            b"invert" | b"inverted" | b"inv" => Effects::INVERT,
497            b"blink" => Effects::BLINK,
498            b"hide" | b"hidden" | b"conceal" | b"concealed" => Effects::HIDDEN,
499            _ => return None,
500        })
501    }
502
503    const fn parse_index(s: &[u8]) -> Result<u8, ParseErrorKind> {
504        if s.is_empty() || s.len() > 3 || s[0] == b'0' {
505            // We disallow colors starting from `0` (e.g., `001`) to avoid ambiguity.
506            return Err(ParseErrorKind::InvalidIndexColor);
507        }
508
509        let mut i = 0;
510        let mut index = 0_u8;
511        while i < s.len() {
512            let digit = if s[i].is_ascii_digit() {
513                s[i] - b'0'
514            } else {
515                return Err(ParseErrorKind::InvalidIndexColor);
516            };
517            index = match index.checked_mul(10) {
518                Some(val) => val,
519                None => return Err(ParseErrorKind::InvalidIndexColor),
520            };
521            index = match index.checked_add(digit) {
522                Some(val) => val,
523                None => return Err(ParseErrorKind::InvalidIndexColor),
524            };
525            i += 1;
526        }
527        Ok(index)
528    }
529
530    const fn parse_color(token: &[u8]) -> Result<Option<Color>, ParseErrorKind> {
531        Ok(match token {
532            b"black" => Some(Color::Ansi(AnsiColor::Black)),
533            b"black!" | b"bright-black" => Some(Color::Ansi(AnsiColor::BrightBlack)),
534            b"red" => Some(Color::Ansi(AnsiColor::Red)),
535            b"red!" | b"bright-red" => Some(Color::Ansi(AnsiColor::BrightRed)),
536            b"green" => Some(Color::Ansi(AnsiColor::Green)),
537            b"green!" | b"bright-green" => Some(Color::Ansi(AnsiColor::BrightGreen)),
538            b"yellow" => Some(Color::Ansi(AnsiColor::Yellow)),
539            b"yellow!" | b"bright-yellow" => Some(Color::Ansi(AnsiColor::BrightYellow)),
540            b"blue" => Some(Color::Ansi(AnsiColor::Blue)),
541            b"blue!" | b"bright-blue" => Some(Color::Ansi(AnsiColor::BrightBlue)),
542            b"magenta" => Some(Color::Ansi(AnsiColor::Magenta)),
543            b"magenta!" | b"bright-magenta" => Some(Color::Ansi(AnsiColor::BrightMagenta)),
544            b"cyan" => Some(Color::Ansi(AnsiColor::Cyan)),
545            b"cyan!" | b"bright-cyan" => Some(Color::Ansi(AnsiColor::BrightCyan)),
546            b"white" => Some(Color::Ansi(AnsiColor::White)),
547            b"white!" | b"bright-white" => Some(Color::Ansi(AnsiColor::BrightWhite)),
548
549            hex if !hex.is_empty() && hex[0] == b'#' => match parse_hex_color(hex) {
550                Ok(color) => Some(Color::Rgb(color)),
551                Err(err) => return Err(ParseErrorKind::HexColor(err)),
552            },
553
554            _ => {
555                let Some(mut color_idx) = strip_prefix(token, b"color") else {
556                    return Ok(None);
557                };
558                if color_idx.is_empty() {
559                    return Err(ParseErrorKind::UnfinishedColor);
560                }
561
562                // Trim `()` from tokens like `color(124)`.
563                if color_idx[0] == b'(' {
564                    if !matches!(color_idx.last(), Some(b')')) {
565                        return Err(ParseErrorKind::UnfinishedColor);
566                    }
567                    (_, color_idx) = color_idx.split_at(1);
568                    (color_idx, _) = color_idx.split_at(color_idx.len() - 1);
569                }
570
571                let index = const_try!(Self::parse_index(color_idx));
572                Some(Color::Ansi256(Ansi256Color(index)))
573            }
574        })
575    }
576
577    const fn gobble_whitespace(&mut self) {
578        while !self.is_eof() {
579            let ch = self.current_byte();
580            if ch.is_ascii_whitespace() {
581                self.advance_byte();
582            } else {
583                break;
584            }
585        }
586    }
587
588    const fn gobble_punct(&mut self) -> bool {
589        if !self.is_eof() && matches!(self.current_byte(), b',' | b';') {
590            self.advance_byte();
591            true
592        } else {
593            false
594        }
595    }
596
597    const fn take_token(&mut self) -> ops::Range<usize> {
598        const fn is_delimiter(ch: u8) -> bool {
599            ch.is_ascii_whitespace() || matches!(ch, b',' | b';' | b']')
600        }
601
602        let start_pos = self.pos();
603        while !self.is_eof() && !is_delimiter(self.current_byte()) {
604            self.advance_byte();
605        }
606        start_pos..self.pos()
607    }
608}
609
610impl<const TEXT_CAP: usize, const SPAN_CAP: usize> StackStyled<TEXT_CAP, SPAN_CAP> {
611    pub(crate) const fn parse(raw: &str) -> Result<Self, ParseError> {
612        let mut parser = RichParser::new(raw);
613        let mut text = StackStr::new();
614        let mut spans = Stack::new(StyledSpan::DUMMY);
615
616        while let ops::ControlFlow::Continue(mut span) = const_try!(parser.step()) {
617            let mut span_extended = false;
618            let plaintext_start = if let Some(last_span) = spans.last_mut() {
619                if is_same_style(&last_span.style, &span.style) {
620                    last_span.extend_len(span.len.get());
621                    span_extended = true;
622                }
623                last_span.end()
624            } else {
625                0
626            };
627
628            let text_range = span.start..span.end();
629            // `span.start` refers to the position in the original rich-syntax string, so now we need
630            // to shift it so that it refers to the plain text.
631            span.start = plaintext_start;
632
633            if !span_extended && spans.push(span).is_err() {
634                return Err(parser.cursor.error(ParseErrorKind::SpanOverflow));
635            }
636
637            let text_chunk = parser.cursor.range(&text_range);
638            let mut i = 0;
639            while i < text_chunk.len() {
640                if text.push(text_chunk[i]).is_err() {
641                    return Err(parser.cursor.error(ParseErrorKind::TextOverflow));
642                }
643                i += 1;
644            }
645        }
646        Ok(Self { text, spans })
647    }
648}
649
650impl FromStr for StyledString {
651    type Err = ParseError;
652
653    fn from_str(raw: &str) -> Result<Self, Self::Err> {
654        let mut parser = RichParser::new(raw);
655        let mut builder = Self::builder();
656
657        while let ops::ControlFlow::Continue(span) = parser.step()? {
658            builder.push_style(span.style);
659            builder.push_text(&raw[span.start..span.end()]);
660        }
661        Ok(builder.build())
662    }
663}
664
665/// Wrapper for a [`Style`] that allows to represent it in [rich syntax](crate#rich-syntax).
666///
667/// # Examples
668///
669/// ```
670/// use anstyle::{Style, Effects, AnsiColor};
671/// use styled_str::RichStyle;
672///
673/// let style = RichStyle::parse("bold, ul red! on white", &Style::new())?;
674/// assert_eq!(style.get_effects(), Effects::BOLD | Effects::UNDERLINE);
675/// assert_eq!(style.get_fg_color(), Some(AnsiColor::BrightRed.into()));
676/// assert_eq!(style.get_bg_color(), Some(AnsiColor::White.into()));
677///
678/// let canonical_str = RichStyle(&style).to_string();
679/// assert_eq!(canonical_str, "bold underline red! on white");
680/// # anyhow::Ok(())
681/// ```
682#[derive(Debug, Clone, Copy)]
683pub struct RichStyle<'a>(pub &'a Style);
684
685impl RichStyle<'_> {
686    /// # Errors
687    ///
688    /// Returns an error if the string doesn't represent a valid style specification.
689    pub const fn parse(raw: &str, current_style: &Style) -> Result<Style, ParseError> {
690        StrCursor::new(raw).parse_style(current_style, false)
691    }
692
693    pub(crate) fn tokens(self) -> Vec<Cow<'static, str>> {
694        let mut tokens = Vec::new();
695
696        let effects = self.0.get_effects();
697        if effects.contains(Effects::BOLD) {
698            tokens.push("bold".into());
699        }
700        if effects.contains(Effects::ITALIC) {
701            tokens.push("italic".into());
702        }
703        if effects.contains(Effects::DIMMED) {
704            tokens.push("dim".into());
705        }
706        if effects.contains(Effects::UNDERLINE) {
707            tokens.push("underline".into());
708        }
709        if effects.contains(Effects::STRIKETHROUGH) {
710            tokens.push("strike".into());
711        }
712        if effects.contains(Effects::INVERT) {
713            tokens.push("invert".into());
714        }
715        if effects.contains(Effects::BLINK) {
716            tokens.push("blink".into());
717        }
718        if effects.contains(Effects::HIDDEN) {
719            tokens.push("hidden".into());
720        }
721
722        if let Some(color) = self.0.get_fg_color() {
723            tokens.push(write_color(color).into());
724        }
725
726        if let Some(color) = self.0.get_bg_color() {
727            tokens.push("on".into());
728            tokens.push(write_color(color).into());
729        }
730        tokens
731    }
732}
733
734impl fmt::Display for RichStyle<'_> {
735    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
736        let mut tokens = self.tokens();
737        if tokens.is_empty() {
738            tokens.push("/".into());
739        }
740
741        for (i, token) in tokens.iter().enumerate() {
742            write!(formatter, "{token}")?;
743            if i + 1 < tokens.len() {
744                formatter.write_str(" ")?;
745            }
746        }
747        Ok(())
748    }
749}
750
751fn write_color(color: Color) -> String {
752    use core::fmt::Write as _;
753
754    let mut buffer = String::new();
755    match color {
756        Color::Ansi(color) => write!(
757            &mut buffer,
758            "{base}{bright}",
759            base = ansi_color_str(color),
760            bright = if color.is_bright() { "!" } else { "" }
761        )
762        .unwrap(),
763        Color::Ansi256(Ansi256Color(idx)) => write!(&mut buffer, "color{idx}").unwrap(),
764        Color::Rgb(color) => {
765            buffer = rgb_color_to_hex(color);
766        }
767    }
768    buffer
769}
770
771fn ansi_color_str(color: AnsiColor) -> &'static str {
772    match color.bright(false) {
773        AnsiColor::Black => "black",
774        AnsiColor::Red => "red",
775        AnsiColor::Green => "green",
776        AnsiColor::Yellow => "yellow",
777        AnsiColor::Blue => "blue",
778        AnsiColor::Magenta => "magenta",
779        AnsiColor::Cyan => "cyan",
780        AnsiColor::White => "white",
781        _ => unreachable!(),
782    }
783}
784
785/// Escapes sequences of >=2 opening brackets (i.e., `[[`, `[[[` etc.) by appending `[[*]]` to each sequence
786/// (a no-op style copy).
787#[derive(Debug)]
788pub(crate) struct EscapedText<'a> {
789    inner: &'a str,
790    /// Whether to escape chars in the printed text.
791    escape_chars: bool,
792}
793
794impl<'a> EscapedText<'a> {
795    pub(crate) fn new(inner: &'a str, escape_chars: bool) -> Self {
796        Self {
797            inner,
798            escape_chars,
799        }
800    }
801}
802
803impl fmt::Display for EscapedText<'_> {
804    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
805        let mut remainder = self.inner;
806        while let Some(mut pos) = remainder.find("[[") {
807            // Increase `pos` until it points at a non-`[` char.
808            pos += 2;
809            while remainder.as_bytes().get(pos).copied() == Some(b'[') {
810                pos += 1;
811            }
812
813            let head;
814            (head, remainder) = remainder.split_at(pos);
815            let escaped;
816            write!(
817                formatter,
818                "{}[[*]]",
819                if self.escape_chars {
820                    escaped = head.escape_debug();
821                    &escaped as &dyn fmt::Display
822                } else {
823                    &head
824                }
825            )?;
826        }
827
828        let escaped;
829        write!(
830            formatter,
831            "{}",
832            if self.escape_chars {
833                escaped = remainder.escape_debug();
834                &escaped as &dyn fmt::Display
835            } else {
836                &remainder
837            }
838        )
839    }
840}