parse_style/
style.rs

1use super::attributes::{Attribute, AttributeSet};
2use super::color::Color;
3use std::fmt;
4use thiserror::Error;
5
6/// A terminal text style
7///
8/// `Style` stores two sets of [`Attribute`]s: those attributes that the style
9/// enables, plus those that the style explicitly disables/turns off.  The
10/// latter are relevant if you're applying a style in the middle of text with a
11/// different style; e.g., if a text styled with `"red bold"` contains a
12/// substring styled with `"blue not bold"`, you'd want to know that the bold
13/// effect should be disabled for that substring.
14#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
15pub struct Style {
16    /// The foreground color
17    foreground: Option<Color>,
18
19    /// The background color
20    background: Option<Color>,
21
22    /// Active/enabled attributes
23    enabled_attributes: AttributeSet,
24
25    /// Explicitly disabled attributes
26    ///
27    /// The individual `Style` methods must ensure that no attribute is ever in
28    /// both `enabled_attributes` and `disabled_attributes` at once.
29    disabled_attributes: AttributeSet,
30}
31
32impl Style {
33    /// Create a new, empty style
34    pub const fn new() -> Style {
35        Style {
36            foreground: None,
37            background: None,
38            enabled_attributes: AttributeSet::EMPTY,
39            disabled_attributes: AttributeSet::EMPTY,
40        }
41    }
42
43    /// Set or clear the foreground color.
44    ///
45    /// Note that setting the foreground to `None` is different from setting it
46    /// to [`Color::Default`]: if the style is applied in the middle of text
47    /// with a foreground color set, `None` will leave the foreground color
48    /// as-is while `Color::Default` will reset it.
49    pub const fn foreground(mut self, fg: Option<Color>) -> Style {
50        self.foreground = fg;
51        self
52    }
53
54    /// Set or clear the background color
55    ///
56    /// Note that setting the background to `None` is different from setting it
57    /// to [`Color::Default`]: if the style is applied in the middle of text
58    /// with a background color set, `None` will leave the background color
59    /// as-is while `Color::Default` will reset it.
60    pub const fn background(mut self, bg: Option<Color>) -> Style {
61        self.background = bg;
62        self
63    }
64
65    /// Set the enabled attributes
66    pub fn enabled_attributes<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
67        self.enabled_attributes = attrs.into();
68        self.disabled_attributes -= self.enabled_attributes;
69        self
70    }
71
72    /// Set the disabled attributes
73    pub fn disabled_attributes<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
74        self.disabled_attributes = attrs.into();
75        self.enabled_attributes -= self.disabled_attributes;
76        self
77    }
78
79    /// `true` if the style does not set any colors or attributes
80    pub fn is_empty(self) -> bool {
81        self.foreground.is_none()
82            && self.background.is_none()
83            && self.enabled_attributes.is_empty()
84            && self.disabled_attributes.is_empty()
85    }
86
87    /// `true` if `attr` is enabled
88    pub fn is_enabled(self, attr: Attribute) -> bool {
89        self.enabled_attributes.contains(attr) && !self.disabled_attributes.contains(attr)
90    }
91
92    /// `true` if `attr` is explicitly disabled
93    pub fn is_disabled(self, attr: Attribute) -> bool {
94        self.disabled_attributes.contains(attr) && !self.enabled_attributes.contains(attr)
95    }
96
97    /// Return the foreground color
98    pub const fn get_foreground(self) -> Option<Color> {
99        self.foreground
100    }
101
102    /// Return the background color
103    pub const fn get_background(self) -> Option<Color> {
104        self.background
105    }
106
107    /// Return the enabled attributes
108    pub const fn get_enabled_attributes(self) -> AttributeSet {
109        self.enabled_attributes
110    }
111
112    /// Return the disabled attributes
113    pub const fn get_disabled_attributes(self) -> AttributeSet {
114        self.disabled_attributes
115    }
116
117    /// Combine two styles, applying the effects of `other` after `self`
118    pub fn patch(self, other: Style) -> Style {
119        let foreground = self.foreground.or(other.foreground);
120        let background = self.background.or(other.background);
121        let enabled_attributes =
122            (self.enabled_attributes - other.disabled_attributes) | other.enabled_attributes;
123        let disabled_attributes =
124            (self.disabled_attributes - other.enabled_attributes) | other.disabled_attributes;
125        Style {
126            foreground,
127            background,
128            enabled_attributes,
129            disabled_attributes,
130        }
131    }
132
133    /// Enable the given attribute(s)
134    pub fn enable<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
135        let attrs = attrs.into();
136        self.enabled_attributes |= attrs;
137        self.disabled_attributes -= attrs;
138        self
139    }
140
141    /// Disable the given attribute(s)
142    pub fn disable<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
143        let attrs = attrs.into();
144        self.enabled_attributes -= attrs;
145        self.disabled_attributes |= attrs;
146        self
147    }
148
149    /// Enable bold text
150    pub fn bold(self) -> Style {
151        self.enable(Attribute::Bold)
152    }
153
154    /// Enable dim text
155    pub fn dim(self) -> Style {
156        self.enable(Attribute::Dim)
157    }
158
159    /// Enable italic text
160    pub fn italic(self) -> Style {
161        self.enable(Attribute::Italic)
162    }
163
164    /// Enable underlining
165    pub fn underline(self) -> Style {
166        self.enable(Attribute::Underline)
167    }
168
169    /// Enable blinking
170    pub fn blink(self) -> Style {
171        self.enable(Attribute::Blink)
172    }
173
174    /// Enable fast blinking
175    pub fn blink2(self) -> Style {
176        self.enable(Attribute::Blink2)
177    }
178
179    /// Enable reverse video
180    pub fn reverse(self) -> Style {
181        self.enable(Attribute::Reverse)
182    }
183
184    /// Enable concealed/hidden text
185    pub fn conceal(self) -> Style {
186        self.enable(Attribute::Conceal)
187    }
188
189    /// Enable strikethrough
190    pub fn strike(self) -> Style {
191        self.enable(Attribute::Strike)
192    }
193
194    /// Enable double-underlining
195    pub fn underline2(self) -> Style {
196        self.enable(Attribute::Underline2)
197    }
198
199    /// Enable framed text
200    pub fn frame(self) -> Style {
201        self.enable(Attribute::Frame)
202    }
203
204    /// Enable encircled text
205    pub fn encircle(self) -> Style {
206        self.enable(Attribute::Encircle)
207    }
208
209    /// Enable overlining
210    pub fn overline(self) -> Style {
211        self.enable(Attribute::Overline)
212    }
213
214    /// Disable bold text
215    pub fn not_bold(self) -> Style {
216        self.disable(Attribute::Bold)
217    }
218
219    /// Disable dim text
220    pub fn not_dim(self) -> Style {
221        self.disable(Attribute::Dim)
222    }
223
224    /// Disable italic text
225    pub fn not_italic(self) -> Style {
226        self.disable(Attribute::Italic)
227    }
228
229    /// Disable underlining
230    pub fn not_underline(self) -> Style {
231        self.disable(Attribute::Underline)
232    }
233
234    /// Disable blinking
235    pub fn not_blink(self) -> Style {
236        self.disable(Attribute::Blink)
237    }
238
239    /// Disable fast blinking
240    pub fn not_blink2(self) -> Style {
241        self.disable(Attribute::Blink2)
242    }
243
244    /// Disable reverse video
245    pub fn not_reverse(self) -> Style {
246        self.disable(Attribute::Reverse)
247    }
248
249    /// Disable concealed/hidden text
250    pub fn not_conceal(self) -> Style {
251        self.disable(Attribute::Conceal)
252    }
253
254    /// Disable strikethrough
255    pub fn not_strike(self) -> Style {
256        self.disable(Attribute::Strike)
257    }
258
259    /// Disable double-underlining
260    pub fn not_underline2(self) -> Style {
261        self.disable(Attribute::Underline2)
262    }
263
264    /// Disable framed text
265    pub fn not_frame(self) -> Style {
266        self.disable(Attribute::Frame)
267    }
268
269    /// Disable encircled text
270    pub fn not_encircle(self) -> Style {
271        self.disable(Attribute::Encircle)
272    }
273
274    /// Disable overlining
275    pub fn not_overline(self) -> Style {
276        self.disable(Attribute::Overline)
277    }
278}
279
280impl<C: Into<Color>> From<C> for Style {
281    /// Construct a new `Style` using the given color as the foreground color
282    fn from(value: C) -> Style {
283        Style::new().foreground(Some(value.into()))
284    }
285}
286
287impl From<Attribute> for Style {
288    /// Construct a new `Style` that enables the given attribute
289    fn from(value: Attribute) -> Style {
290        Style::new().enable(value)
291    }
292}
293
294impl From<AttributeSet> for Style {
295    /// Construct a new `Style` that enables the given attributes
296    fn from(value: AttributeSet) -> Style {
297        Style::new().enabled_attributes(value)
298    }
299}
300
301#[cfg(feature = "anstyle")]
302#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
303impl From<Style> for anstyle::Style {
304    /// Convert a `Style` to an [`anstyle::Style`]
305    ///
306    /// # Data Loss
307    ///
308    /// If the `Style`'s foreground or background color is [`Color::Default`],
309    /// it will be converted to `None`.
310    ///
311    /// The following attributes will be discarded during conversion:
312    ///
313    /// - [`Attribute::Blink2`]
314    /// - [`Attribute::Frame`]
315    /// - [`Attribute::Encircle`]
316    /// - [`Attribute::Overline`]
317    ///
318    /// Disabled attributes are discarded during conversion.
319    fn from(value: Style) -> anstyle::Style {
320        anstyle::Style::new()
321            .fg_color(
322                value
323                    .get_foreground()
324                    .and_then(|c| anstyle::Color::try_from(c).ok()),
325            )
326            .bg_color(
327                value
328                    .get_background()
329                    .and_then(|c| anstyle::Color::try_from(c).ok()),
330            )
331            .effects(value.enabled_attributes.into())
332    }
333}
334
335#[cfg(feature = "anstyle")]
336#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
337impl From<anstyle::Style> for Style {
338    /// Convert an [`anstyle::Style`] to a `Style`
339    ///
340    /// # Data Loss
341    ///
342    /// Underline color is discarded during conversion.
343    ///
344    /// The following effects are discarded during conversion:
345    ///
346    /// - [`anstyle::Effects::CURLY_UNDERLINE`]
347    /// - [`anstyle::Effects::DOTTED_UNDERLINE`]
348    /// - [`anstyle::Effects::DASHED_UNDERLINE`]
349    fn from(value: anstyle::Style) -> Style {
350        Style::new()
351            .foreground(value.get_fg_color().map(Color::from))
352            .background(value.get_bg_color().map(Color::from))
353            .enabled_attributes(AttributeSet::from(value.get_effects()))
354    }
355}
356
357#[cfg(feature = "crossterm")]
358#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
359impl From<crossterm::style::Attributes> for Style {
360    /// Convert a [`crossterm::style::Attributes`] to a `Style`
361    ///
362    /// # Data Loss
363    ///
364    /// The following attributes are discarded during conversion:
365    ///
366    /// - [`crossterm::style::Attribute::Undercurled`]
367    /// - [`crossterm::style::Attribute::Underdotted`]
368    /// - [`crossterm::style::Attribute::Underdashed`]
369    /// - [`crossterm::style::Attribute::Fraktur`]
370    /// - [`crossterm::style::Attribute::NoBold`] (because it's dysfunctional)
371    fn from(value: crossterm::style::Attributes) -> Style {
372        use crossterm::style::Attribute as CrossAttrib;
373        let mut set = Style::new();
374        for attr in CrossAttrib::iterator().filter(|&attr| value.has(attr)) {
375            match attr {
376                CrossAttrib::Reset => set = Style::new(),
377                CrossAttrib::Bold => set = set.bold(),
378                CrossAttrib::Dim => set = set.dim(),
379                CrossAttrib::Italic => set = set.italic(),
380                CrossAttrib::Underlined => set = set.underline(),
381                CrossAttrib::DoubleUnderlined => set = set.underline2(),
382                CrossAttrib::Undercurled => (),
383                CrossAttrib::Underdotted => (),
384                CrossAttrib::Underdashed => (),
385                CrossAttrib::SlowBlink => set = set.blink(),
386                CrossAttrib::RapidBlink => set = set.blink2(),
387                CrossAttrib::Reverse => set = set.reverse(),
388                CrossAttrib::Hidden => set = set.conceal(),
389                CrossAttrib::CrossedOut => set = set.strike(),
390                CrossAttrib::Fraktur => (),
391                CrossAttrib::NoBold => (),
392                CrossAttrib::NormalIntensity => set = set.not_bold().not_dim(),
393                CrossAttrib::NoItalic => set = set.not_italic(),
394                CrossAttrib::NoUnderline => set = set.not_underline().not_underline2(),
395                CrossAttrib::NoBlink => set = set.not_blink().not_blink2(),
396                CrossAttrib::NoReverse => set = set.not_reverse(),
397                CrossAttrib::NoHidden => set = set.not_conceal(),
398                CrossAttrib::NotCrossedOut => set = set.not_strike(),
399                CrossAttrib::Framed => set = set.frame(),
400                CrossAttrib::Encircled => set = set.encircle(),
401                CrossAttrib::OverLined => set = set.overline(),
402                CrossAttrib::NotFramedOrEncircled => set = set.not_frame().not_encircle(),
403                CrossAttrib::NotOverLined => set = set.not_overline(),
404                _ => (), // non-exhaustive
405            }
406        }
407        set
408    }
409}
410
411#[cfg(feature = "crossterm")]
412#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
413impl From<Style> for crossterm::style::ContentStyle {
414    /// Convert a `Style` to a [`crossterm::style::ContentStyle`]
415    ///
416    /// # Data Loss
417    ///
418    /// Certain pairs of `parse-style` attributes are disabled by a single
419    /// shared `crossterm` attribute.  Thus, when one element of a pair occurs
420    /// in the disabled attributes, the resulting `ContentStyle` will disable
421    /// both elements of the pair.
422    ///
423    /// The pairs are as follows:
424    ///
425    /// - [`Attribute::Bold`] and [`Attribute::Dim`] — both disabled by
426    ///   [`crossterm::style::Attribute::NormalIntensity`]
427    ///
428    /// - [`Attribute::Blink`] and [`Attribute::Blink2`] — both disabled by
429    ///   [`crossterm::style::Attribute::NoBlink`]
430    ///
431    /// - [`Attribute::Underline`] and [`Attribute::Underline2`] — both
432    ///   disabled by [`crossterm::style::Attribute::NoUnderline`]
433    ///
434    /// - [`Attribute::Frame`] and [`Attribute::Encircle`] — both disabled by
435    ///   [`crossterm::style::Attribute::NotFramedOrEncircled`]
436    fn from(value: Style) -> crossterm::style::ContentStyle {
437        use crossterm::style::Attribute as CrossAttrib;
438        let foreground_color = value.foreground.map(crossterm::style::Color::from);
439        let background_color = value.background.map(crossterm::style::Color::from);
440        let mut attributes = crossterm::style::Attributes::from(value.enabled_attributes);
441        for attr in value.disabled_attributes {
442            match attr {
443                Attribute::Bold => attributes.set(CrossAttrib::NormalIntensity),
444                Attribute::Dim => attributes.set(CrossAttrib::NormalIntensity),
445                Attribute::Italic => attributes.set(CrossAttrib::NoItalic),
446                Attribute::Underline => attributes.set(CrossAttrib::NoUnderline),
447                Attribute::Blink => attributes.set(CrossAttrib::NoBlink),
448                Attribute::Blink2 => attributes.set(CrossAttrib::NoBlink),
449                Attribute::Reverse => attributes.set(CrossAttrib::NoReverse),
450                Attribute::Conceal => attributes.set(CrossAttrib::NoHidden),
451                Attribute::Strike => attributes.set(CrossAttrib::NotCrossedOut),
452                Attribute::Underline2 => attributes.set(CrossAttrib::NoUnderline),
453                Attribute::Frame => attributes.set(CrossAttrib::NotFramedOrEncircled),
454                Attribute::Encircle => attributes.set(CrossAttrib::NotFramedOrEncircled),
455                Attribute::Overline => attributes.set(CrossAttrib::NotOverLined),
456            }
457        }
458        crossterm::style::ContentStyle {
459            foreground_color,
460            background_color,
461            attributes,
462            underline_color: None,
463        }
464    }
465}
466
467#[cfg(feature = "crossterm")]
468#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
469impl From<crossterm::style::ContentStyle> for Style {
470    /// Convert a [`crossterm::style::ContentStyle`] to a `Style`
471    ///
472    /// # Data Loss
473    ///
474    /// Underline color is discarded during conversion.
475    ///
476    /// The following attributes are discarded during conversion:
477    ///
478    /// - [`crossterm::style::Attribute::Undercurled`]
479    /// - [`crossterm::style::Attribute::Underdotted`]
480    /// - [`crossterm::style::Attribute::Underdashed`]
481    /// - [`crossterm::style::Attribute::Fraktur`]
482    fn from(value: crossterm::style::ContentStyle) -> Style {
483        Style::from(value.attributes)
484            .foreground(value.foreground_color.map(Color::from))
485            .background(value.background_color.map(Color::from))
486    }
487}
488
489#[cfg(feature = "ratatui")]
490#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
491impl From<Style> for ratatui_core::style::Style {
492    /// Convert a `Style` to a [`ratatui_core::style::Style`]
493    ///
494    /// # Data Loss
495    ///
496    /// The following attributes are discarded during conversion:
497    ///
498    /// - [`Attribute::Underline2`]
499    /// - [`Attribute::Frame`]
500    /// - [`Attribute::Encircle`]
501    /// - [`Attribute::Overline`]
502    fn from(value: Style) -> ratatui_core::style::Style {
503        // Don't try to construct a ratatui Style using struct notation, as the
504        // `underline_color` field is feature-based.
505        let mut style = ratatui_core::style::Style::new();
506        if let Some(fg) = value.foreground.map(ratatui_core::style::Color::from) {
507            style = style.fg(fg);
508        }
509        if let Some(bg) = value.background.map(ratatui_core::style::Color::from) {
510            style = style.bg(bg);
511        }
512        style = style.add_modifier(value.enabled_attributes.into());
513        style = style.remove_modifier(value.disabled_attributes.into());
514        style
515    }
516}
517
518#[cfg(feature = "ratatui")]
519#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
520impl From<ratatui_core::style::Style> for Style {
521    /// Convert a [`ratatui_core::style::Style`] to a `Style`
522    ///
523    /// # Data Loss
524    ///
525    /// Underline color is discarded during conversion.
526    fn from(value: ratatui_core::style::Style) -> Style {
527        let foreground = value.fg.map(Color::from);
528        let background = value.bg.map(Color::from);
529        let enabled_attributes = AttributeSet::from(value.add_modifier);
530        let disabled_attributes = AttributeSet::from(value.sub_modifier);
531        Style {
532            foreground,
533            background,
534            enabled_attributes,
535            disabled_attributes,
536        }
537    }
538}
539
540impl fmt::Display for Style {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        let mut first = true;
543        for attr in Attribute::iter() {
544            if self.is_enabled(attr) {
545                if !std::mem::replace(&mut first, false) {
546                    write!(f, " ")?;
547                }
548                write!(f, "{attr}")?;
549            } else if self.is_disabled(attr) {
550                if !std::mem::replace(&mut first, false) {
551                    write!(f, " ")?;
552                }
553                write!(f, "not {attr}")?;
554            }
555        }
556        if let Some(fg) = self.foreground {
557            if !std::mem::replace(&mut first, false) {
558                write!(f, " ")?;
559            }
560            write!(f, "{fg}")?;
561        }
562        if let Some(bg) = self.background {
563            if !std::mem::replace(&mut first, false) {
564                write!(f, " ")?;
565            }
566            write!(f, "on {bg}")?;
567        }
568        if first {
569            write!(f, "none")?;
570        }
571        Ok(())
572    }
573}
574
575impl std::str::FromStr for Style {
576    type Err = ParseStyleError;
577
578    fn from_str(s: &str) -> Result<Style, ParseStyleError> {
579        let mut style = Style::new();
580        if s.is_empty() || s.trim().eq_ignore_ascii_case("none") {
581            return Ok(style);
582        }
583        let mut words = s.split_whitespace();
584        while let Some(token) = words.next() {
585            if token.eq_ignore_ascii_case("on") {
586                let Some(bg) = words.next().and_then(|s| s.parse::<Color>().ok()) else {
587                    return Err(ParseStyleError::MissingBackground);
588                };
589                style.background = Some(bg);
590            } else if token.eq_ignore_ascii_case("not") {
591                let Some(attr) = words.next().and_then(|s| s.parse::<Attribute>().ok()) else {
592                    return Err(ParseStyleError::MissingAttribute);
593                };
594                style = style.disable(attr);
595            } else if let Ok(color) = token.parse::<Color>() {
596                style.foreground = Some(color);
597            } else if let Ok(attr) = token.parse::<Attribute>() {
598                style = style.enable(attr);
599            } else {
600                return Err(ParseStyleError::Token(token.to_owned()));
601            }
602        }
603        Ok(style)
604    }
605}
606
607#[cfg(feature = "serde")]
608#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
609impl serde::Serialize for Style {
610    fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
611        serializer.collect_str(self)
612    }
613}
614
615#[cfg(feature = "serde")]
616#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
617impl<'de> serde::Deserialize<'de> for Style {
618    fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
619        struct Visitor;
620
621        impl serde::de::Visitor<'_> for Visitor {
622            type Value = Style;
623
624            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625                f.write_str("a style string")
626            }
627
628            fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
629            where
630                E: serde::de::Error,
631            {
632                input
633                    .parse::<Style>()
634                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(input), &self))
635            }
636        }
637
638        deserializer.deserialize_str(Visitor)
639    }
640}
641
642/// Error returned when parsing a style fails
643#[derive(Clone, Debug, Eq, Error, PartialEq)]
644pub enum ParseStyleError {
645    /// An invalid/unexpected token was enountered
646    #[error("unexpected token in style string: {0:?}")]
647    Token(
648        /// The invalid token
649        String,
650    ),
651
652    /// `"on"` was not followed by a valid color word
653    #[error(r#""on" not followed by valid color word"#)]
654    MissingBackground,
655
656    /// `"not"` was not followed by a valid attribute name
657    #[error(r#""not" not followed by valid attribute name"#)]
658    MissingAttribute,
659}
660
661#[cfg(test)]
662mod test {
663    use super::*;
664
665    #[test]
666    fn test_new_is_default() {
667        assert_eq!(Style::new(), Style::default());
668    }
669
670    mod display {
671        use super::*;
672        use crate::Color256;
673
674        #[test]
675        fn none() {
676            assert_eq!(Style::new().to_string(), "none");
677        }
678
679        #[test]
680        fn fg_color() {
681            let style = Style::from(Color256::RED);
682            assert_eq!(style.to_string(), "red");
683        }
684
685        #[test]
686        fn bg_color() {
687            let style = Color256::RED.as_background();
688            assert_eq!(style.to_string(), "on red");
689        }
690
691        #[test]
692        fn fg_on_bg() {
693            let style = Color256::BLUE.on(Color256::RED);
694            assert_eq!(style.to_string(), "blue on red");
695        }
696
697        #[test]
698        fn attr() {
699            let style = Style::from(Attribute::Bold);
700            assert_eq!(style.to_string(), "bold");
701        }
702
703        #[test]
704        fn multiple_attrs() {
705            let style = Style::from(Attribute::Bold | Attribute::Reverse);
706            assert_eq!(style.to_string(), "bold reverse");
707        }
708
709        #[test]
710        fn not_attr() {
711            let style = Style::new().disable(Attribute::Bold);
712            assert_eq!(style.to_string(), "not bold");
713        }
714
715        #[test]
716        fn multiple_not_attrs() {
717            let style = Style::new()
718                .disable(Attribute::Bold)
719                .disable(Attribute::Reverse);
720            assert_eq!(style.to_string(), "not bold not reverse");
721        }
722
723        #[test]
724        fn attr_and_not_attr() {
725            let style = Style::from(Attribute::Bold).disable(Attribute::Blink);
726            assert_eq!(style.to_string(), "bold not blink");
727        }
728
729        #[test]
730        fn gamut() {
731            let style = Color256::YELLOW
732                .on(Color::Default)
733                .enable(Attribute::Italic)
734                .disable(Attribute::Bold);
735            assert_eq!(style.to_string(), "not bold italic yellow on default");
736        }
737
738        #[test]
739        fn all_attrs() {
740            let style = Style::from(AttributeSet::ALL);
741            assert_eq!(
742                style.to_string(),
743                "bold dim italic underline blink blink2 reverse conceal strike underline2 frame encircle overline"
744            );
745        }
746
747        #[test]
748        fn not_all_attrs() {
749            let style = Style::new().disabled_attributes(AttributeSet::ALL);
750            assert_eq!(
751                style.to_string(),
752                "not bold not dim not italic not underline not blink not blink2 not reverse not conceal not strike not underline2 not frame not encircle not overline"
753            );
754        }
755    }
756
757    mod parse {
758        use super::*;
759        use crate::Color256;
760        use rstest::rstest;
761
762        #[test]
763        fn none() {
764            assert_eq!("".parse::<Style>().unwrap(), Style::new());
765            assert_eq!("none".parse::<Style>().unwrap(), Style::new());
766            assert_eq!("NONE".parse::<Style>().unwrap(), Style::new());
767            assert_eq!(" none ".parse::<Style>().unwrap(), Style::new());
768        }
769
770        #[test]
771        fn fg() {
772            assert_eq!(
773                "green".parse::<Style>().unwrap(),
774                Style::from(Color256::GREEN)
775            );
776        }
777
778        #[test]
779        fn bg() {
780            assert_eq!(
781                "on green".parse::<Style>().unwrap(),
782                Color256::GREEN.as_background()
783            );
784            assert_eq!(
785                " on  green ".parse::<Style>().unwrap(),
786                Color256::GREEN.as_background()
787            );
788            assert_eq!(
789                " ON  GREEN ".parse::<Style>().unwrap(),
790                Color256::GREEN.as_background()
791            );
792        }
793
794        #[test]
795        fn fg_on_bg() {
796            assert_eq!(
797                "blue on white".parse::<Style>().unwrap(),
798                Color256::BLUE.on(Color256::WHITE)
799            );
800            assert_eq!(
801                "on white blue".parse::<Style>().unwrap(),
802                Color256::BLUE.on(Color256::WHITE)
803            );
804        }
805
806        #[test]
807        fn attr() {
808            assert_eq!(
809                "bold".parse::<Style>().unwrap(),
810                Style::from(Attribute::Bold)
811            );
812        }
813
814        #[test]
815        fn multiple_attr() {
816            assert_eq!(
817                "bold underline".parse::<Style>().unwrap(),
818                Style::from(Attribute::Bold | Attribute::Underline)
819            );
820            assert_eq!(
821                "underline bold".parse::<Style>().unwrap(),
822                Style::from(Attribute::Bold | Attribute::Underline)
823            );
824        }
825
826        #[test]
827        fn not_attr() {
828            assert_eq!(
829                "not bold".parse::<Style>().unwrap(),
830                Style::new().disable(Attribute::Bold)
831            );
832            assert_eq!(
833                " NOT  BOLD ".parse::<Style>().unwrap(),
834                Style::new().disable(Attribute::Bold)
835            );
836        }
837
838        #[test]
839        fn multiple_not_attrs() {
840            assert_eq!(
841                "not bold not s".parse::<Style>().unwrap(),
842                Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
843            );
844            assert_eq!(
845                "not s not bold".parse::<Style>().unwrap(),
846                Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
847            );
848        }
849
850        #[test]
851        fn attr_and_not_attr() {
852            assert_eq!(
853                "dim not blink2".parse::<Style>().unwrap(),
854                Style::new()
855                    .enable(Attribute::Dim)
856                    .disable(Attribute::Blink2)
857            );
858            assert_eq!(
859                "not blink2 dim".parse::<Style>().unwrap(),
860                Style::new()
861                    .enable(Attribute::Dim)
862                    .disable(Attribute::Blink2)
863            );
864        }
865
866        #[test]
867        fn gamut() {
868            for s in [
869                "bold not underline red on blue",
870                "not underline red on blue bold",
871                "on blue red not underline bold",
872            ] {
873                assert_eq!(
874                    s.parse::<Style>().unwrap(),
875                    Color256::RED.on(Color256::BLUE).bold().not_underline()
876                );
877            }
878        }
879
880        #[test]
881        fn multiple_fg() {
882            assert_eq!(
883                "red blue".parse::<Style>().unwrap(),
884                Style::from(Color256::BLUE)
885            );
886        }
887
888        #[test]
889        fn multiple_bg() {
890            assert_eq!(
891                "on red on blue".parse::<Style>().unwrap(),
892                Color256::BLUE.as_background()
893            );
894        }
895
896        #[test]
897        fn attr_on_and_off() {
898            assert_eq!(
899                "bold magenta not bold".parse::<Style>().unwrap(),
900                Style::from(Color256::MAGENTA).not_bold()
901            );
902        }
903
904        #[test]
905        fn attr_off_and_on() {
906            assert_eq!(
907                "not bold magenta bold".parse::<Style>().unwrap(),
908                Style::from(Color256::MAGENTA).bold()
909            );
910        }
911
912        #[rstest]
913        #[case("on bold")]
914        #[case("on foo")]
915        #[case("blue on")]
916        #[case("on")]
917        #[case("not blue")]
918        #[case("not foo")]
919        #[case("bold not")]
920        #[case("not not bold italic")]
921        #[case("not")]
922        #[case("none red")]
923        #[case("red none")]
924        #[case("foo")]
925        #[case("rgb(1, 2, 3)")]
926        #[case("bright blue")]
927        fn err(#[case] s: &str) {
928            assert!(s.parse::<Style>().is_err());
929        }
930    }
931}