Skip to main content

rich_rs/
style.rs

1//! Style: text formatting attributes.
2//!
3//! Styles are immutable and can be combined using the `+` operator or `combine` method.
4//!
5//! The core `Style` struct is `Copy` for efficiency. For advanced features like
6//! hyperlinks and metadata (used by Textual), use `StyleMeta` separately.
7
8use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use crate::color::{ColorSystem, SimpleColor as Color};
12
13/// A null style with all attributes set to `None`.
14///
15/// This is useful as a default or starting point for style combinations.
16pub const NULL_STYLE: Style = Style {
17    color: None,
18    bgcolor: None,
19    bold: None,
20    dim: None,
21    italic: None,
22    underline: None,
23    blink: None,
24    blink2: None,
25    reverse: None,
26    conceal: None,
27    strike: None,
28    underline2: None,
29    frame: None,
30    encircle: None,
31    overline: None,
32};
33
34/// Text style with color and attributes.
35///
36/// Uses `Option<bool>` for attributes to support three states:
37/// - `None`: inherit from parent
38/// - `Some(true)`: enable
39/// - `Some(false)`: disable
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct Style {
42    /// Foreground color.
43    pub color: Option<Color>,
44    /// Background color.
45    pub bgcolor: Option<Color>,
46    /// Bold text.
47    pub bold: Option<bool>,
48    /// Dim/faint text.
49    pub dim: Option<bool>,
50    /// Italic text.
51    pub italic: Option<bool>,
52    /// Underlined text.
53    pub underline: Option<bool>,
54    /// Blinking text.
55    pub blink: Option<bool>,
56    /// Rapid blinking text (SGR 6).
57    pub blink2: Option<bool>,
58    /// Reverse video (swap fg/bg).
59    pub reverse: Option<bool>,
60    /// Concealed text (SGR 8).
61    pub conceal: Option<bool>,
62    /// Strikethrough text.
63    pub strike: Option<bool>,
64    /// Double underline (SGR 21).
65    pub underline2: Option<bool>,
66    /// Framed text (SGR 51).
67    pub frame: Option<bool>,
68    /// Encircled text (SGR 52).
69    pub encircle: Option<bool>,
70    /// Overlined text (SGR 53).
71    pub overline: Option<bool>,
72}
73
74impl Style {
75    /// Create a new empty style.
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Create a style with a foreground color.
81    pub fn color(color: Color) -> Self {
82        Self {
83            color: Some(color),
84            ..Default::default()
85        }
86    }
87
88    /// Create a style with a background color.
89    pub fn bgcolor(color: Color) -> Self {
90        Self {
91            bgcolor: Some(color),
92            ..Default::default()
93        }
94    }
95
96    /// Builder: set foreground color.
97    pub fn with_color(mut self, color: Color) -> Self {
98        self.color = Some(color);
99        self
100    }
101
102    /// Builder: set background color.
103    pub fn with_bgcolor(mut self, color: Color) -> Self {
104        self.bgcolor = Some(color);
105        self
106    }
107
108    /// Builder: set bold.
109    pub fn with_bold(mut self, bold: bool) -> Self {
110        self.bold = Some(bold);
111        self
112    }
113
114    /// Builder: set dim.
115    pub fn with_dim(mut self, dim: bool) -> Self {
116        self.dim = Some(dim);
117        self
118    }
119
120    /// Builder: set italic.
121    pub fn with_italic(mut self, italic: bool) -> Self {
122        self.italic = Some(italic);
123        self
124    }
125
126    /// Builder: set underline.
127    pub fn with_underline(mut self, underline: bool) -> Self {
128        self.underline = Some(underline);
129        self
130    }
131
132    /// Builder: set reverse video.
133    pub fn with_reverse(mut self, reverse: bool) -> Self {
134        self.reverse = Some(reverse);
135        self
136    }
137
138    /// Builder: set strike.
139    pub fn with_strike(mut self, strike: bool) -> Self {
140        self.strike = Some(strike);
141        self
142    }
143
144    /// Builder: set blink2 (rapid blink).
145    pub fn with_blink2(mut self, blink2: bool) -> Self {
146        self.blink2 = Some(blink2);
147        self
148    }
149
150    /// Builder: set conceal.
151    pub fn with_conceal(mut self, conceal: bool) -> Self {
152        self.conceal = Some(conceal);
153        self
154    }
155
156    /// Builder: set underline2 (double underline).
157    pub fn with_underline2(mut self, underline2: bool) -> Self {
158        self.underline2 = Some(underline2);
159        self
160    }
161
162    /// Builder: set frame.
163    pub fn with_frame(mut self, frame: bool) -> Self {
164        self.frame = Some(frame);
165        self
166    }
167
168    /// Builder: set encircle.
169    pub fn with_encircle(mut self, encircle: bool) -> Self {
170        self.encircle = Some(encircle);
171        self
172    }
173
174    /// Builder: set overline.
175    pub fn with_overline(mut self, overline: bool) -> Self {
176        self.overline = Some(overline);
177        self
178    }
179
180    /// Combine this style with another, with `other` taking precedence.
181    ///
182    /// Values from `other` override values from `self` only if they are `Some`.
183    pub fn combine(&self, other: &Style) -> Self {
184        Style {
185            color: other.color.or(self.color),
186            bgcolor: other.bgcolor.or(self.bgcolor),
187            bold: other.bold.or(self.bold),
188            dim: other.dim.or(self.dim),
189            italic: other.italic.or(self.italic),
190            underline: other.underline.or(self.underline),
191            blink: other.blink.or(self.blink),
192            blink2: other.blink2.or(self.blink2),
193            reverse: other.reverse.or(self.reverse),
194            conceal: other.conceal.or(self.conceal),
195            strike: other.strike.or(self.strike),
196            underline2: other.underline2.or(self.underline2),
197            frame: other.frame.or(self.frame),
198            encircle: other.encircle.or(self.encircle),
199            overline: other.overline.or(self.overline),
200        }
201    }
202
203    /// Parse a style from a string.
204    ///
205    /// Supports space-separated style definitions like:
206    /// - "bold red on blue"
207    /// - "italic #ff0000"
208    /// - "bold underline"
209    /// - "not bold" (negation)
210    pub fn parse(s: &str) -> Option<Self> {
211        let mut style = Style::new();
212        let mut on_background = false;
213
214        let mut words = s.split_whitespace().peekable();
215
216        while let Some(word) = words.next() {
217            let word_lower = word.to_lowercase();
218
219            if word_lower == "on" {
220                on_background = true;
221                continue;
222            }
223
224            if on_background {
225                if let Some(color) = Color::parse(&word_lower) {
226                    style.bgcolor = Some(color);
227                    on_background = false;
228                    continue;
229                }
230                // If the token wasn't a valid color, drop the background flag and
231                // continue processing it normally.
232                on_background = false;
233            }
234
235            // Support Rich-style named styles from the default theme, e.g. "progress.percentage".
236            // If a token matches a default style name, merge it into the current style.
237            if let Some(named) = crate::theme::get_default_style(&word_lower) {
238                style = style.combine(&named);
239                continue;
240            }
241
242            // Handle negation: "not bold", "not italic", etc.
243            // Also handles shorthand: "not b", "not i", "not u", "not s"
244            if word_lower == "not" {
245                if let Some(&next_word) = words.peek() {
246                    let next_lower = next_word.to_lowercase();
247                    match next_lower.as_str() {
248                        "bold" | "b" => {
249                            style.bold = Some(false);
250                            words.next();
251                            continue;
252                        }
253                        "dim" | "d" => {
254                            style.dim = Some(false);
255                            words.next();
256                            continue;
257                        }
258                        "italic" | "i" => {
259                            style.italic = Some(false);
260                            words.next();
261                            continue;
262                        }
263                        "underline" | "u" => {
264                            style.underline = Some(false);
265                            words.next();
266                            continue;
267                        }
268                        "blink" => {
269                            style.blink = Some(false);
270                            words.next();
271                            continue;
272                        }
273                        "blink2" => {
274                            style.blink2 = Some(false);
275                            words.next();
276                            continue;
277                        }
278                        "reverse" | "r" => {
279                            style.reverse = Some(false);
280                            words.next();
281                            continue;
282                        }
283                        "conceal" | "c" => {
284                            style.conceal = Some(false);
285                            words.next();
286                            continue;
287                        }
288                        "strike" | "s" => {
289                            style.strike = Some(false);
290                            words.next();
291                            continue;
292                        }
293                        "underline2" | "uu" => {
294                            style.underline2 = Some(false);
295                            words.next();
296                            continue;
297                        }
298                        "frame" => {
299                            style.frame = Some(false);
300                            words.next();
301                            continue;
302                        }
303                        "encircle" => {
304                            style.encircle = Some(false);
305                            words.next();
306                            continue;
307                        }
308                        "overline" | "o" => {
309                            style.overline = Some(false);
310                            words.next();
311                            continue;
312                        }
313                        _ => {}
314                    }
315                }
316                // "not" without valid attribute - ignore
317                continue;
318            }
319
320            // Check for attributes (including shorthand: b, d, i, u, s, r, c, o, uu)
321            match word_lower.as_str() {
322                "bold" | "b" => style.bold = Some(true),
323                "dim" | "d" => style.dim = Some(true),
324                "italic" | "i" => style.italic = Some(true),
325                "underline" | "u" => style.underline = Some(true),
326                "blink" => style.blink = Some(true),
327                "blink2" => style.blink2 = Some(true),
328                "reverse" | "r" => style.reverse = Some(true),
329                "conceal" | "c" => style.conceal = Some(true),
330                "strike" | "s" => style.strike = Some(true),
331                "underline2" | "uu" => style.underline2 = Some(true),
332                "frame" => style.frame = Some(true),
333                "encircle" => style.encircle = Some(true),
334                "overline" | "o" => style.overline = Some(true),
335                _ => {
336                    // Try to parse as color
337                    if let Some(color) = Color::parse(&word_lower) {
338                        style.color = Some(color);
339                    }
340                }
341            }
342        }
343
344        Some(style)
345    }
346
347    /// Check if this is a null style (all attributes are None).
348    pub fn is_null(&self) -> bool {
349        *self == NULL_STYLE
350    }
351
352    /// Render text with this style using ANSI escape codes.
353    ///
354    /// # Arguments
355    ///
356    /// * `text` - The text to style.
357    /// * `color_system` - The color system to render to.
358    ///
359    /// # Returns
360    ///
361    /// A string containing the text wrapped in ANSI escape codes.
362    /// If text is empty, returns empty string.
363    ///
364    /// # Examples
365    ///
366    /// ```
367    /// use rich_rs::{Style, ColorSystem, SimpleColor};
368    ///
369    /// let style = Style::new().with_bold(true).with_color(SimpleColor::Standard(1));
370    /// let rendered = style.render("Hello", ColorSystem::TrueColor);
371    /// assert!(rendered.contains("\x1b["));
372    /// assert!(rendered.contains("Hello"));
373    /// assert!(rendered.ends_with("\x1b[0m"));
374    /// ```
375    pub fn render(&self, text: &str, color_system: ColorSystem) -> String {
376        if text.is_empty() {
377            return String::new();
378        }
379
380        let attrs = self.make_ansi_codes(color_system);
381        if attrs.is_empty() {
382            text.to_string()
383        } else {
384            format!("\x1b[{}m{}\x1b[0m", attrs, text)
385        }
386    }
387
388    /// Render styled text WITHOUT a trailing reset.
389    ///
390    /// This is used for streaming output where we want to minimize SGR resets
391    /// to avoid visual artifacts (like black hairlines between colored lines).
392    /// The caller is responsible for emitting a reset at the end.
393    pub fn render_open(&self, text: &str, color_system: ColorSystem) -> String {
394        if text.is_empty() {
395            return String::new();
396        }
397
398        let attrs = self.make_ansi_codes(color_system);
399        if attrs.is_empty() {
400            text.to_string()
401        } else {
402            format!("\x1b[{}m{}", attrs, text)
403        }
404    }
405
406    /// Generate the ANSI SGR codes for this style.
407    ///
408    /// Returns a semicolon-separated string of SGR parameters.
409    fn make_ansi_codes(&self, color_system: ColorSystem) -> String {
410        let mut sgr: Vec<String> = Vec::new();
411
412        // SGR reset codes for explicitly disabled attributes (emit "off" before "on"):
413        // 22 = bold/dim off (resets both)
414        // 23 = italic off
415        // 24 = underline/underline2 off (resets both)
416        // 25 = blink/blink2 off (resets both)
417        // 27 = reverse off
418        // 28 = conceal off
419        // 29 = strike off
420        // 54 = frame/encircle off (resets both)
421        // 55 = overline off
422        //
423        // Note: SGR 22 resets both bold AND dim, so we only emit it once if either is false.
424        // Similarly SGR 24 resets underline AND underline2, SGR 25 resets blink AND blink2,
425        // and SGR 54 resets frame AND encircle.
426        if self.bold == Some(false) || self.dim == Some(false) {
427            sgr.push("22".to_string());
428        }
429        if self.italic == Some(false) {
430            sgr.push("23".to_string());
431        }
432        if self.underline == Some(false) || self.underline2 == Some(false) {
433            sgr.push("24".to_string());
434        }
435        if self.blink == Some(false) || self.blink2 == Some(false) {
436            sgr.push("25".to_string());
437        }
438        if self.reverse == Some(false) {
439            sgr.push("27".to_string());
440        }
441        if self.conceal == Some(false) {
442            sgr.push("28".to_string());
443        }
444        if self.strike == Some(false) {
445            sgr.push("29".to_string());
446        }
447        if self.frame == Some(false) || self.encircle == Some(false) {
448            sgr.push("54".to_string());
449        }
450        if self.overline == Some(false) {
451            sgr.push("55".to_string());
452        }
453
454        // SGR codes for enabled attributes:
455        // bold=1, dim=2, italic=3, underline=4, blink=5, blink2=6,
456        // reverse=7, conceal=8, strike=9, underline2=21,
457        // frame=51, encircle=52, overline=53
458        if self.bold == Some(true) {
459            sgr.push("1".to_string());
460        }
461        if self.dim == Some(true) {
462            sgr.push("2".to_string());
463        }
464        if self.italic == Some(true) {
465            sgr.push("3".to_string());
466        }
467        if self.underline == Some(true) {
468            sgr.push("4".to_string());
469        }
470        if self.blink == Some(true) {
471            sgr.push("5".to_string());
472        }
473        if self.blink2 == Some(true) {
474            sgr.push("6".to_string());
475        }
476        if self.reverse == Some(true) {
477            sgr.push("7".to_string());
478        }
479        if self.conceal == Some(true) {
480            sgr.push("8".to_string());
481        }
482        if self.strike == Some(true) {
483            sgr.push("9".to_string());
484        }
485        if self.underline2 == Some(true) {
486            sgr.push("21".to_string());
487        }
488        if self.frame == Some(true) {
489            sgr.push("51".to_string());
490        }
491        if self.encircle == Some(true) {
492            sgr.push("52".to_string());
493        }
494        if self.overline == Some(true) {
495            sgr.push("53".to_string());
496        }
497
498        // Foreground color
499        if let Some(color) = self.color {
500            let downgraded = color.downgrade(color_system);
501            sgr.extend(downgraded.get_ansi_codes(true));
502        }
503
504        // Background color
505        if let Some(bgcolor) = self.bgcolor {
506            let downgraded = bgcolor.downgrade(color_system);
507            sgr.extend(downgraded.get_ansi_codes(false));
508        }
509
510        sgr.join(";")
511    }
512
513    /// Get a CSS style string for this style.
514    ///
515    /// # Returns
516    ///
517    /// A semicolon-separated CSS string suitable for use in a `style` attribute.
518    ///
519    /// # Examples
520    ///
521    /// ```
522    /// use rich_rs::{Style, SimpleColor};
523    ///
524    /// let style = Style::new()
525    ///     .with_bold(true)
526    ///     .with_color(SimpleColor::Rgb { r: 255, g: 0, b: 0 });
527    /// let css = style.get_html_style();
528    /// assert!(css.contains("font-weight: bold"));
529    /// assert!(css.contains("color:"));
530    /// ```
531    pub fn get_html_style(&self) -> String {
532        let mut css: Vec<String> = Vec::new();
533
534        // Handle reverse by swapping colors conceptually
535        let (color, bgcolor) = if self.reverse == Some(true) {
536            (self.bgcolor, self.color)
537        } else {
538            (self.color, self.bgcolor)
539        };
540
541        // Foreground color
542        if let Some(c) = color {
543            let hex = c.get_hex();
544            css.push(format!("color: {}", hex));
545            css.push(format!("text-decoration-color: {}", hex));
546        }
547
548        // Background color
549        if let Some(c) = bgcolor {
550            let hex = c.get_hex();
551            css.push(format!("background-color: {}", hex));
552        }
553
554        // Text attributes
555        if self.bold == Some(true) {
556            css.push("font-weight: bold".to_string());
557        }
558        if self.italic == Some(true) {
559            css.push("font-style: italic".to_string());
560        }
561
562        // Collect text-decoration values to avoid clobbering
563        let mut decorations = Vec::new();
564        if self.underline == Some(true) {
565            decorations.push("underline");
566        }
567        if self.strike == Some(true) {
568            decorations.push("line-through");
569        }
570        if self.overline == Some(true) {
571            decorations.push("overline");
572        }
573        if !decorations.is_empty() {
574            css.push(format!("text-decoration: {}", decorations.join(" ")));
575        }
576
577        css.join("; ")
578    }
579
580    /// Chain multiple styles together. Like `combine` but applied left-to-right.
581    ///
582    /// This is equivalent to Python Rich's `Style.chain(*styles)`.
583    pub fn chain(styles: &[Style]) -> Self {
584        let mut result = Style::new();
585        for style in styles {
586            result = result.combine(style);
587        }
588        result
589    }
590
591    /// Create a style with only foreground/background colors.
592    ///
593    /// This is equivalent to Python Rich's `Style.from_color`.
594    pub fn from_color(color: Option<Color>, bgcolor: Option<Color>) -> Self {
595        Self {
596            color,
597            bgcolor,
598            ..Default::default()
599        }
600    }
601
602    /// Create blank style metadata with optional event handlers.
603    ///
604    /// In Python Rich, `Style.on(...)` returns a Style with metadata attached.
605    /// In this crate, metadata is represented separately as `StyleMeta`.
606    pub fn on<I, K>(meta: Option<BTreeMap<String, MetaValue>>, handlers: I) -> StyleMeta
607    where
608        I: IntoIterator<Item = (K, MetaValue)>,
609        K: Into<String>,
610    {
611        let mut merged = meta.unwrap_or_default();
612        for (key, value) in handlers {
613            merged.insert(format!("@{}", key.into()), value);
614        }
615
616        StyleMeta {
617            meta: if merged.is_empty() {
618                None
619            } else {
620                Some(Arc::new(merged))
621            },
622            ..Default::default()
623        }
624    }
625
626    /// Normalize a style definition to a canonical representation.
627    ///
628    /// This is equivalent to Python Rich's `Style.normalize`.
629    /// Returns lowercased/trimmed input for syntactically invalid definitions.
630    pub fn normalize(style: &str) -> String {
631        let normalized = style.trim().to_lowercase();
632        if normalized.is_empty() {
633            return "none".to_string();
634        }
635
636        fn is_attr(word: &str) -> bool {
637            matches!(
638                word,
639                "bold"
640                    | "b"
641                    | "dim"
642                    | "d"
643                    | "italic"
644                    | "i"
645                    | "underline"
646                    | "u"
647                    | "blink"
648                    | "blink2"
649                    | "reverse"
650                    | "r"
651                    | "conceal"
652                    | "c"
653                    | "strike"
654                    | "s"
655                    | "underline2"
656                    | "uu"
657                    | "frame"
658                    | "encircle"
659                    | "overline"
660                    | "o"
661            )
662        }
663
664        let mut words = normalized.split_whitespace().peekable();
665        while let Some(word) = words.next() {
666            match word {
667                "on" => match words.next() {
668                    Some(color) if Color::parse(color).is_some() => {}
669                    _ => return normalized,
670                },
671                "not" => match words.next() {
672                    Some(attr) if is_attr(attr) => {}
673                    _ => return normalized,
674                },
675                _ => {
676                    if is_attr(word)
677                        || Color::parse(word).is_some()
678                        || crate::theme::get_default_style(word).is_some()
679                    {
680                        continue;
681                    }
682                    return normalized;
683                }
684            }
685        }
686
687        let parsed = Self::parse(&normalized).unwrap_or_default();
688        let canonical = parsed.to_markup_string();
689        if canonical.is_empty() {
690            "none".to_string()
691        } else {
692            canonical
693        }
694    }
695
696    /// Return the first non-`None` style from a sequence.
697    ///
698    /// Panics if all styles are `None`, matching Python Rich's ValueError behavior.
699    pub fn pick_first(values: &[Option<Style>]) -> Style {
700        values
701            .iter()
702            .flatten()
703            .copied()
704            .next()
705            .expect("expected at least one non-None style")
706    }
707
708    /// Apply the style to text and return the ANSI string.
709    ///
710    /// This is equivalent to Python Rich's `Style.test()`.
711    pub fn test(&self, text: &str, color_system: ColorSystem) -> String {
712        self.render(text, color_system)
713    }
714
715    /// Return a new Style with only the background color.
716    ///
717    /// This is equivalent to Python Rich's `Style.background_style`.
718    pub fn background_style(&self) -> Self {
719        Style {
720            bgcolor: self.bgcolor,
721            ..Default::default()
722        }
723    }
724
725    /// Return a new Style with colors stripped but attributes preserved.
726    ///
727    /// This is equivalent to Python Rich's `Style.without_color`.
728    pub fn without_color(&self) -> Self {
729        Style {
730            color: None,
731            bgcolor: None,
732            bold: self.bold,
733            dim: self.dim,
734            italic: self.italic,
735            underline: self.underline,
736            blink: self.blink,
737            blink2: self.blink2,
738            reverse: self.reverse,
739            conceal: self.conceal,
740            strike: self.strike,
741            underline2: self.underline2,
742            frame: self.frame,
743            encircle: self.encircle,
744            overline: self.overline,
745        }
746    }
747
748    /// Check if the style has a transparent (unset or default) background.
749    ///
750    /// This is equivalent to Python Rich's `Style.transparent_background`.
751    pub fn has_transparent_background(&self) -> bool {
752        matches!(self.bgcolor, None | Some(Color::Default))
753    }
754
755    /// Convert this style to its markup-compatible string representation.
756    ///
757    /// Returns a string like `"bold red on blue"` that can be used in Rich markup tags.
758    /// This is the canonical way to serialize a Style for markup — all consumers
759    /// (Text::to_markup, Theme serialization, etc.) should use this method.
760    pub fn to_markup_string(&self) -> String {
761        use crate::color::ANSI_COLOR_NAMES;
762
763        let mut parts: Vec<&str> = Vec::new();
764        // Macro to reduce repetition for bool attributes
765        macro_rules! attr {
766            ($field:ident, $name:expr, $neg:expr) => {
767                match self.$field {
768                    Some(true) => parts.push($name),
769                    Some(false) => parts.push($neg),
770                    None => {}
771                }
772            };
773            ($field:ident, $name:expr) => {
774                if self.$field == Some(true) {
775                    parts.push($name);
776                }
777            };
778        }
779        attr!(bold, "bold", "not bold");
780        attr!(dim, "dim", "not dim");
781        attr!(italic, "italic", "not italic");
782        attr!(underline, "underline", "not underline");
783        attr!(blink, "blink", "not blink");
784        attr!(blink2, "blink2", "not blink2");
785        attr!(reverse, "reverse", "not reverse");
786        attr!(conceal, "conceal", "not conceal");
787        attr!(strike, "strike", "not strike");
788        attr!(underline2, "underline2", "not underline2");
789        attr!(frame, "frame", "not frame");
790        attr!(encircle, "encircle", "not encircle");
791        attr!(overline, "overline", "not overline");
792
793        let mut owned_parts: Vec<String> = parts.iter().map(|s| s.to_string()).collect();
794
795        // Foreground color
796        if let Some(ref color) = self.color {
797            owned_parts.push(simple_color_name(color, &ANSI_COLOR_NAMES));
798        }
799
800        // Background color
801        if let Some(ref bgcolor) = self.bgcolor {
802            owned_parts.push(format!(
803                "on {}",
804                simple_color_name(bgcolor, &ANSI_COLOR_NAMES)
805            ));
806        }
807
808        owned_parts.join(" ")
809    }
810}
811
812/// Convert a SimpleColor to its markup name.
813fn simple_color_name(color: &Color, color_names: &std::collections::HashMap<&str, u8>) -> String {
814    match color {
815        Color::Default => String::new(),
816        Color::Standard(n) => {
817            for (name, &idx) in color_names.iter() {
818                if idx == *n {
819                    return name.to_string();
820                }
821            }
822            format!("color({})", n)
823        }
824        Color::EightBit(n) => format!("color({})", n),
825        Color::Rgb { r, g, b } => format!("#{:02x}{:02x}{:02x}", r, g, b),
826    }
827}
828
829/// A stack of styles where the current style is the combination of all pushed styles.
830///
831/// This mirrors Python Rich's `StyleStack`.
832#[derive(Debug, Clone)]
833pub struct StyleStack {
834    _stack: Vec<Style>,
835}
836
837impl StyleStack {
838    /// Create a new StyleStack with the given default style.
839    pub fn new(default_style: Style) -> Self {
840        StyleStack {
841            _stack: vec![default_style],
842        }
843    }
844
845    /// Get the current (top) style.
846    pub fn current(&self) -> Style {
847        *self._stack.last().unwrap()
848    }
849
850    /// Push a new style, combined with the current style.
851    pub fn push(&mut self, style: Style) {
852        let combined = self.current().combine(&style);
853        self._stack.push(combined);
854    }
855
856    /// Pop the top style and return the new current.
857    pub fn pop(&mut self) -> Style {
858        self._stack.pop();
859        self.current()
860    }
861}
862
863impl std::ops::Add for Style {
864    type Output = Self;
865
866    fn add(self, other: Self) -> Self {
867        self.combine(&other)
868    }
869}
870
871/// Metadata for styles, used for hyperlinks and custom data.
872///
873/// This is kept separate from `Style` to preserve `Style: Copy` for the
874/// common case. Only segments with links or metadata need a `StyleMeta`.
875///
876/// Uses `BTreeMap` instead of `HashMap` for deterministic ordering,
877/// which is important for segment simplification and serialization.
878#[derive(Debug, Clone, PartialEq, Eq, Default)]
879pub struct StyleMeta {
880    /// Hyperlink URL (terminal OSC 8 escape sequence).
881    pub link: Option<Arc<str>>,
882    /// Link ID for grouping multiple segments with the same link.
883    pub link_id: Option<Arc<str>>,
884    /// Custom metadata (used by Textual for event handlers).
885    pub meta: Option<Arc<BTreeMap<String, MetaValue>>>,
886}
887
888impl StyleMeta {
889    /// Create a new empty StyleMeta.
890    pub fn new() -> Self {
891        Self::default()
892    }
893
894    /// Create a StyleMeta with a hyperlink.
895    pub fn with_link(link: impl Into<Arc<str>>) -> Self {
896        StyleMeta {
897            link: Some(link.into()),
898            ..Default::default()
899        }
900    }
901
902    /// Check if this meta has any content.
903    pub fn is_empty(&self) -> bool {
904        self.link.is_none() && self.link_id.is_none() && self.meta.is_none()
905    }
906
907    /// Combine with another StyleMeta, with `other` taking precedence.
908    pub fn combine(&self, other: &StyleMeta) -> Self {
909        StyleMeta {
910            link: other.link.clone().or_else(|| self.link.clone()),
911            link_id: other.link_id.clone().or_else(|| self.link_id.clone()),
912            meta: match (&self.meta, &other.meta) {
913                (Some(a), Some(b)) => {
914                    let mut merged = (**a).clone();
915                    merged.extend((**b).clone());
916                    Some(Arc::new(merged))
917                }
918                (None, Some(b)) => Some(b.clone()),
919                (Some(a), None) => Some(a.clone()),
920                (None, None) => None,
921            },
922        }
923    }
924}
925
926/// Structured metadata values (used by Textual handlers and richer annotations).
927///
928/// This is intentionally deterministic and `Eq` so it can be compared / merged
929/// and used in segment simplification.
930#[derive(Debug, Clone, PartialEq, Eq)]
931pub enum MetaValue {
932    None,
933    Bool(bool),
934    Int(i64),
935    Str(Arc<str>),
936    List(Vec<MetaValue>),
937    Tuple(Vec<MetaValue>),
938    Map(BTreeMap<String, MetaValue>),
939}
940
941impl Default for MetaValue {
942    fn default() -> Self {
943        MetaValue::None
944    }
945}
946
947impl MetaValue {
948    pub fn str(value: impl Into<Arc<str>>) -> Self {
949        MetaValue::Str(value.into())
950    }
951
952    /// Parse a small, deterministic subset of Python literals (like `ast.literal_eval`).
953    ///
954    /// Supported:
955    /// - `None`, `True`, `False`
956    /// - integers (base-10)
957    /// - quoted strings `'...'` / `"..."` with basic escapes
958    /// - lists `[a, b]`
959    /// - tuples `(a, b)` (single element tuples require a trailing comma, like Python)
960    /// - dicts `{'k': 1}` (string keys only)
961    pub fn parse_python_literal(input: &str) -> Option<Self> {
962        struct Parser<'a> {
963            s: &'a str,
964            i: usize,
965        }
966
967        impl<'a> Parser<'a> {
968            fn new(s: &'a str) -> Self {
969                Self { s, i: 0 }
970            }
971
972            fn is_eof(&self) -> bool {
973                self.i >= self.s.len()
974            }
975
976            fn rest(&self) -> &'a str {
977                &self.s[self.i..]
978            }
979
980            fn skip_ws(&mut self) {
981                while let Some(ch) = self.peek() {
982                    if ch.is_whitespace() {
983                        self.bump(ch);
984                    } else {
985                        break;
986                    }
987                }
988            }
989
990            fn peek(&self) -> Option<char> {
991                self.rest().chars().next()
992            }
993
994            fn bump(&mut self, ch: char) {
995                self.i += ch.len_utf8();
996            }
997
998            fn eat(&mut self, expected: char) -> bool {
999                self.skip_ws();
1000                if self.peek() == Some(expected) {
1001                    self.bump(expected);
1002                    true
1003                } else {
1004                    false
1005                }
1006            }
1007
1008            fn parse_value(&mut self) -> Option<MetaValue> {
1009                self.skip_ws();
1010                let ch = self.peek()?;
1011
1012                match ch {
1013                    '\'' | '"' => self.parse_string().map(|s| MetaValue::Str(Arc::from(s))),
1014                    '[' => self.parse_list(),
1015                    '{' => self.parse_map(),
1016                    '(' => self.parse_parens(),
1017                    '-' | '0'..='9' => self.parse_int().map(MetaValue::Int),
1018                    _ => self.parse_ident_or_keyword(),
1019                }
1020            }
1021
1022            fn parse_ident_or_keyword(&mut self) -> Option<MetaValue> {
1023                self.skip_ws();
1024                let start = self.i;
1025                while let Some(ch) = self.peek() {
1026                    if ch.is_ascii_alphanumeric() || ch == '_' {
1027                        self.bump(ch);
1028                    } else {
1029                        break;
1030                    }
1031                }
1032                if self.i == start {
1033                    return None;
1034                }
1035                let ident = &self.s[start..self.i];
1036                match ident {
1037                    "None" => Some(MetaValue::None),
1038                    "True" => Some(MetaValue::Bool(true)),
1039                    "False" => Some(MetaValue::Bool(false)),
1040                    _ => None,
1041                }
1042            }
1043
1044            fn parse_int(&mut self) -> Option<i64> {
1045                self.skip_ws();
1046                let start = self.i;
1047                if self.peek() == Some('-') {
1048                    self.bump('-');
1049                }
1050                let mut saw_digit = false;
1051                while let Some(ch) = self.peek() {
1052                    if ch.is_ascii_digit() {
1053                        saw_digit = true;
1054                        self.bump(ch);
1055                    } else {
1056                        break;
1057                    }
1058                }
1059                if !saw_digit {
1060                    self.i = start;
1061                    return None;
1062                }
1063                self.s[start..self.i].parse::<i64>().ok()
1064            }
1065
1066            fn parse_string(&mut self) -> Option<String> {
1067                self.skip_ws();
1068                let quote = self.peek()?;
1069                if quote != '\'' && quote != '"' {
1070                    return None;
1071                }
1072                self.bump(quote);
1073                let mut out = String::new();
1074                while let Some(ch) = self.peek() {
1075                    self.bump(ch);
1076                    if ch == quote {
1077                        return Some(out);
1078                    }
1079                    if ch == '\\' {
1080                        let esc = self.peek()?;
1081                        self.bump(esc);
1082                        match esc {
1083                            'n' => out.push('\n'),
1084                            'r' => out.push('\r'),
1085                            't' => out.push('\t'),
1086                            '\\' => out.push('\\'),
1087                            '\'' => out.push('\''),
1088                            '"' => out.push('"'),
1089                            other => out.push(other),
1090                        }
1091                    } else {
1092                        out.push(ch);
1093                    }
1094                }
1095                None
1096            }
1097
1098            fn parse_list(&mut self) -> Option<MetaValue> {
1099                if !self.eat('[') {
1100                    return None;
1101                }
1102                let mut items = Vec::new();
1103                loop {
1104                    self.skip_ws();
1105                    if self.eat(']') {
1106                        break;
1107                    }
1108                    let value = self.parse_value()?;
1109                    items.push(value);
1110                    self.skip_ws();
1111                    if self.eat(']') {
1112                        break;
1113                    }
1114                    if !self.eat(',') {
1115                        return None;
1116                    }
1117                }
1118                Some(MetaValue::List(items))
1119            }
1120
1121            fn parse_map(&mut self) -> Option<MetaValue> {
1122                if !self.eat('{') {
1123                    return None;
1124                }
1125                let mut map: BTreeMap<String, MetaValue> = BTreeMap::new();
1126                loop {
1127                    self.skip_ws();
1128                    if self.eat('}') {
1129                        break;
1130                    }
1131                    let key = match self.parse_value()? {
1132                        MetaValue::Str(s) => s.to_string(),
1133                        _ => return None,
1134                    };
1135                    if !self.eat(':') {
1136                        return None;
1137                    }
1138                    let value = self.parse_value()?;
1139                    map.insert(key, value);
1140                    self.skip_ws();
1141                    if self.eat('}') {
1142                        break;
1143                    }
1144                    if !self.eat(',') {
1145                        return None;
1146                    }
1147                }
1148                Some(MetaValue::Map(map))
1149            }
1150
1151            fn parse_parens(&mut self) -> Option<MetaValue> {
1152                if !self.eat('(') {
1153                    return None;
1154                }
1155                self.skip_ws();
1156                if self.eat(')') {
1157                    return Some(MetaValue::Tuple(Vec::new()));
1158                }
1159
1160                let first = self.parse_value()?;
1161                self.skip_ws();
1162
1163                // If there's no comma, treat as grouping: `(x)` -> `x`
1164                if self.eat(')') {
1165                    return Some(first);
1166                }
1167                if !self.eat(',') {
1168                    return None;
1169                }
1170
1171                let mut items = vec![first];
1172                loop {
1173                    self.skip_ws();
1174                    if self.eat(')') {
1175                        break;
1176                    }
1177                    let value = self.parse_value()?;
1178                    items.push(value);
1179                    self.skip_ws();
1180                    if self.eat(')') {
1181                        break;
1182                    }
1183                    if !self.eat(',') {
1184                        return None;
1185                    }
1186                }
1187
1188                Some(MetaValue::Tuple(items))
1189            }
1190        }
1191
1192        let mut p = Parser::new(input);
1193        let value = p.parse_value()?;
1194        p.skip_ws();
1195        if !p.is_eof() {
1196            return None;
1197        }
1198        Some(value)
1199    }
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204    use super::*;
1205
1206    #[test]
1207    fn test_style_builder() {
1208        let style = Style::new().with_bold(true).with_color(Color::Standard(1));
1209        assert_eq!(style.bold, Some(true));
1210        assert_eq!(style.color, Some(Color::Standard(1)));
1211    }
1212
1213    #[test]
1214    fn test_style_combine() {
1215        let base = Style::new().with_bold(true);
1216        let overlay = Style::new().with_italic(true);
1217        let combined = base.combine(&overlay);
1218        assert_eq!(combined.bold, Some(true));
1219        assert_eq!(combined.italic, Some(true));
1220    }
1221
1222    #[test]
1223    fn test_style_parse() {
1224        let style = Style::parse("bold red").unwrap();
1225        assert_eq!(style.bold, Some(true));
1226        assert_eq!(style.color, Some(Color::Standard(1)));
1227    }
1228
1229    #[test]
1230    fn test_style_parse_background() {
1231        let style = Style::parse("on blue").unwrap();
1232        assert_eq!(style.color, None);
1233        assert_eq!(style.bgcolor, Some(Color::Standard(4)));
1234
1235        let style = Style::parse("bold red on blue").unwrap();
1236        assert_eq!(style.bold, Some(true));
1237        assert_eq!(style.color, Some(Color::Standard(1)));
1238        assert_eq!(style.bgcolor, Some(Color::Standard(4)));
1239    }
1240
1241    // --- NULL_STYLE tests ---
1242
1243    #[test]
1244    fn test_null_style_is_default() {
1245        assert_eq!(NULL_STYLE, Style::default());
1246        assert!(NULL_STYLE.is_null());
1247    }
1248
1249    #[test]
1250    fn test_null_style_all_none() {
1251        assert_eq!(NULL_STYLE.color, None);
1252        assert_eq!(NULL_STYLE.bgcolor, None);
1253        assert_eq!(NULL_STYLE.bold, None);
1254        assert_eq!(NULL_STYLE.dim, None);
1255        assert_eq!(NULL_STYLE.italic, None);
1256        assert_eq!(NULL_STYLE.underline, None);
1257        assert_eq!(NULL_STYLE.blink, None);
1258        assert_eq!(NULL_STYLE.blink2, None);
1259        assert_eq!(NULL_STYLE.reverse, None);
1260        assert_eq!(NULL_STYLE.conceal, None);
1261        assert_eq!(NULL_STYLE.strike, None);
1262        assert_eq!(NULL_STYLE.underline2, None);
1263        assert_eq!(NULL_STYLE.frame, None);
1264        assert_eq!(NULL_STYLE.encircle, None);
1265        assert_eq!(NULL_STYLE.overline, None);
1266    }
1267
1268    #[test]
1269    fn test_is_null() {
1270        assert!(Style::new().is_null());
1271        assert!(!Style::new().with_bold(true).is_null());
1272        assert!(!Style::new().with_color(Color::Standard(1)).is_null());
1273    }
1274
1275    // --- render() tests ---
1276
1277    #[test]
1278    fn test_render_empty_text() {
1279        let style = Style::new().with_bold(true);
1280        assert_eq!(style.render("", ColorSystem::TrueColor), "");
1281    }
1282
1283    #[test]
1284    fn test_render_null_style() {
1285        let style = Style::new();
1286        // Null style should return text without ANSI codes
1287        assert_eq!(style.render("Hello", ColorSystem::TrueColor), "Hello");
1288    }
1289
1290    #[test]
1291    fn test_render_bold() {
1292        let style = Style::new().with_bold(true);
1293        let rendered = style.render("Hello", ColorSystem::TrueColor);
1294        assert_eq!(rendered, "\x1b[1mHello\x1b[0m");
1295    }
1296
1297    #[test]
1298    fn test_render_multiple_attributes() {
1299        let style = Style::new().with_bold(true).with_italic(true);
1300        let rendered = style.render("Hello", ColorSystem::TrueColor);
1301        assert_eq!(rendered, "\x1b[1;3mHello\x1b[0m");
1302    }
1303
1304    #[test]
1305    fn test_render_all_attributes() {
1306        let style = Style {
1307            bold: Some(true),
1308            dim: Some(true),
1309            italic: Some(true),
1310            underline: Some(true),
1311            blink: Some(true),
1312            reverse: Some(true),
1313            strike: Some(true),
1314            ..Default::default()
1315        };
1316        let rendered = style.render("X", ColorSystem::TrueColor);
1317        // SGR codes: bold=1, dim=2, italic=3, underline=4, blink=5, reverse=7, strike=9
1318        assert_eq!(rendered, "\x1b[1;2;3;4;5;7;9mX\x1b[0m");
1319    }
1320
1321    #[test]
1322    fn test_render_with_standard_color() {
1323        let style = Style::new().with_color(Color::Standard(1)); // red
1324        let rendered = style.render("Hi", ColorSystem::TrueColor);
1325        // Standard color 1 = red, foreground code = 31
1326        assert_eq!(rendered, "\x1b[31mHi\x1b[0m");
1327    }
1328
1329    #[test]
1330    fn test_render_with_bright_color() {
1331        let style = Style::new().with_color(Color::Standard(9)); // bright red
1332        let rendered = style.render("Hi", ColorSystem::TrueColor);
1333        // Bright red, foreground code = 91
1334        assert_eq!(rendered, "\x1b[91mHi\x1b[0m");
1335    }
1336
1337    #[test]
1338    fn test_render_with_256_color() {
1339        let style = Style::new().with_color(Color::EightBit(196));
1340        let rendered = style.render("Hi", ColorSystem::TrueColor);
1341        assert_eq!(rendered, "\x1b[38;5;196mHi\x1b[0m");
1342    }
1343
1344    #[test]
1345    fn test_render_with_rgb_color() {
1346        let style = Style::new().with_color(Color::Rgb {
1347            r: 255,
1348            g: 128,
1349            b: 0,
1350        });
1351        let rendered = style.render("Hi", ColorSystem::TrueColor);
1352        assert_eq!(rendered, "\x1b[38;2;255;128;0mHi\x1b[0m");
1353    }
1354
1355    #[test]
1356    fn test_render_with_bgcolor() {
1357        let style = Style::new().with_bgcolor(Color::Standard(4)); // blue bg
1358        let rendered = style.render("Hi", ColorSystem::TrueColor);
1359        // Blue background code = 44
1360        assert_eq!(rendered, "\x1b[44mHi\x1b[0m");
1361    }
1362
1363    #[test]
1364    fn test_render_with_fg_and_bg() {
1365        let style = Style::new()
1366            .with_color(Color::Standard(1)) // red fg
1367            .with_bgcolor(Color::Standard(7)); // white bg
1368        let rendered = style.render("Hi", ColorSystem::TrueColor);
1369        assert_eq!(rendered, "\x1b[31;47mHi\x1b[0m");
1370    }
1371
1372    #[test]
1373    fn test_render_bold_and_color() {
1374        let style = Style::new().with_bold(true).with_color(Color::Standard(2)); // green
1375        let rendered = style.render("OK", ColorSystem::TrueColor);
1376        assert_eq!(rendered, "\x1b[1;32mOK\x1b[0m");
1377    }
1378
1379    #[test]
1380    fn test_render_color_downgrade_to_256() {
1381        // RGB color should be downgraded when using 256 color system
1382        let style = Style::new().with_color(Color::Rgb { r: 255, g: 0, b: 0 });
1383        let rendered = style.render("X", ColorSystem::EightBit);
1384        // Should contain 38;5;N format, not 38;2;R;G;B
1385        assert!(rendered.contains("38;5;"));
1386        assert!(!rendered.contains("38;2;"));
1387    }
1388
1389    #[test]
1390    fn test_render_color_downgrade_to_standard() {
1391        // RGB color should be downgraded when using standard color system
1392        let style = Style::new().with_color(Color::Rgb { r: 255, g: 0, b: 0 });
1393        let rendered = style.render("X", ColorSystem::Standard);
1394        // Should contain simple code like 31 or 91, not extended format
1395        assert!(!rendered.contains("38;5;"));
1396        assert!(!rendered.contains("38;2;"));
1397    }
1398
1399    // --- get_html_style() tests ---
1400
1401    #[test]
1402    fn test_html_style_empty() {
1403        let style = Style::new();
1404        assert_eq!(style.get_html_style(), "");
1405    }
1406
1407    #[test]
1408    fn test_html_style_bold() {
1409        let style = Style::new().with_bold(true);
1410        assert_eq!(style.get_html_style(), "font-weight: bold");
1411    }
1412
1413    #[test]
1414    fn test_html_style_italic() {
1415        let style = Style::new().with_italic(true);
1416        assert_eq!(style.get_html_style(), "font-style: italic");
1417    }
1418
1419    #[test]
1420    fn test_html_style_underline() {
1421        let style = Style::new().with_underline(true);
1422        assert_eq!(style.get_html_style(), "text-decoration: underline");
1423    }
1424
1425    #[test]
1426    fn test_html_style_strike() {
1427        let style = Style::new().with_strike(true);
1428        assert_eq!(style.get_html_style(), "text-decoration: line-through");
1429    }
1430
1431    #[test]
1432    fn test_with_reverse_builder_sets_flag() {
1433        let style = Style::new().with_reverse(true);
1434        assert_eq!(style.reverse, Some(true));
1435    }
1436
1437    #[test]
1438    fn test_html_style_color_rgb() {
1439        let style = Style::new().with_color(Color::Rgb { r: 255, g: 0, b: 0 });
1440        let css = style.get_html_style();
1441        assert!(css.contains("color: #ff0000"));
1442        assert!(css.contains("text-decoration-color: #ff0000"));
1443    }
1444
1445    #[test]
1446    fn test_html_style_bgcolor() {
1447        let style = Style::new().with_bgcolor(Color::Rgb { r: 0, g: 0, b: 255 });
1448        let css = style.get_html_style();
1449        assert!(css.contains("background-color: #0000ff"));
1450    }
1451
1452    #[test]
1453    fn test_html_style_reverse_swaps_colors() {
1454        let style = Style {
1455            color: Some(Color::Rgb { r: 255, g: 0, b: 0 }),
1456            bgcolor: Some(Color::Rgb { r: 0, g: 0, b: 255 }),
1457            reverse: Some(true),
1458            ..Default::default()
1459        };
1460        let css = style.get_html_style();
1461        // After reverse, fg should be blue and bg should be red
1462        assert!(css.contains("color: #0000ff"));
1463        assert!(css.contains("background-color: #ff0000"));
1464    }
1465
1466    #[test]
1467    fn test_html_style_combined() {
1468        let style = Style::new()
1469            .with_bold(true)
1470            .with_italic(true)
1471            .with_color(Color::Rgb {
1472                r: 255,
1473                g: 128,
1474                b: 0,
1475            });
1476        let css = style.get_html_style();
1477        assert!(css.contains("font-weight: bold"));
1478        assert!(css.contains("font-style: italic"));
1479        assert!(css.contains("color: #ff8000"));
1480    }
1481
1482    #[test]
1483    fn test_html_style_standard_color() {
1484        // Standard color 1 = red
1485        let style = Style::new().with_color(Color::Standard(1));
1486        let css = style.get_html_style();
1487        // Should look up in palette and return hex
1488        assert!(css.contains("color: #"));
1489    }
1490
1491    #[test]
1492    fn test_html_style_underline_and_strike_combined() {
1493        // Bug fix: underline + strike should combine into single text-decoration
1494        let style = Style::new().with_underline(true).with_strike(true);
1495        let css = style.get_html_style();
1496        // Should emit "text-decoration: underline line-through" (single property)
1497        assert!(css.contains("text-decoration: underline line-through"));
1498        // Should NOT have two separate text-decoration properties
1499        assert_eq!(css.matches("text-decoration").count(), 1);
1500    }
1501
1502    // --- make_ansi_codes() tests ---
1503
1504    #[test]
1505    fn test_make_ansi_codes_empty() {
1506        let style = Style::new();
1507        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "");
1508    }
1509
1510    #[test]
1511    fn test_make_ansi_codes_attributes_only() {
1512        let style = Style::new().with_bold(true).with_dim(true);
1513        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "1;2");
1514    }
1515
1516    #[test]
1517    fn test_make_ansi_codes_false_attributes_emit_reset() {
1518        // Explicitly false attributes should emit SGR reset codes before "on" codes
1519        let style = Style {
1520            bold: Some(false),
1521            italic: Some(true),
1522            ..Default::default()
1523        };
1524        // 22 = bold/dim off, 3 = italic on
1525        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "22;3");
1526    }
1527
1528    // --- Bug fix tests ---
1529
1530    #[test]
1531    fn test_parse_not_bold() {
1532        // Bug 1: "not bold" should set bold = Some(false)
1533        let style = Style::parse("not bold").unwrap();
1534        assert_eq!(style.bold, Some(false));
1535    }
1536
1537    #[test]
1538    fn test_parse_not_italic() {
1539        let style = Style::parse("not italic").unwrap();
1540        assert_eq!(style.italic, Some(false));
1541    }
1542
1543    #[test]
1544    fn test_parse_not_underline() {
1545        let style = Style::parse("not underline").unwrap();
1546        assert_eq!(style.underline, Some(false));
1547    }
1548
1549    #[test]
1550    fn test_parse_not_dim() {
1551        let style = Style::parse("not dim").unwrap();
1552        assert_eq!(style.dim, Some(false));
1553    }
1554
1555    #[test]
1556    fn test_parse_not_blink() {
1557        let style = Style::parse("not blink").unwrap();
1558        assert_eq!(style.blink, Some(false));
1559    }
1560
1561    #[test]
1562    fn test_parse_not_reverse() {
1563        let style = Style::parse("not reverse").unwrap();
1564        assert_eq!(style.reverse, Some(false));
1565    }
1566
1567    #[test]
1568    fn test_parse_not_strike() {
1569        let style = Style::parse("not strike").unwrap();
1570        assert_eq!(style.strike, Some(false));
1571    }
1572
1573    #[test]
1574    fn test_parse_mixed_attributes_with_negation() {
1575        // "bold not italic red" should set bold=true, italic=false, color=red
1576        let style = Style::parse("bold not italic red").unwrap();
1577        assert_eq!(style.bold, Some(true));
1578        assert_eq!(style.italic, Some(false));
1579        assert_eq!(style.color, Some(Color::Standard(1)));
1580    }
1581
1582    #[test]
1583    fn test_make_ansi_codes_bold_false_emits_22() {
1584        // Bug 2: bold = Some(false) should emit SGR code 22
1585        let style = Style {
1586            bold: Some(false),
1587            ..Default::default()
1588        };
1589        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "22");
1590    }
1591
1592    #[test]
1593    fn test_make_ansi_codes_italic_false_emits_23() {
1594        let style = Style {
1595            italic: Some(false),
1596            ..Default::default()
1597        };
1598        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "23");
1599    }
1600
1601    #[test]
1602    fn test_make_ansi_codes_underline_false_emits_24() {
1603        let style = Style {
1604            underline: Some(false),
1605            ..Default::default()
1606        };
1607        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "24");
1608    }
1609
1610    #[test]
1611    fn test_make_ansi_codes_blink_false_emits_25() {
1612        let style = Style {
1613            blink: Some(false),
1614            ..Default::default()
1615        };
1616        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "25");
1617    }
1618
1619    #[test]
1620    fn test_make_ansi_codes_reverse_false_emits_27() {
1621        let style = Style {
1622            reverse: Some(false),
1623            ..Default::default()
1624        };
1625        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "27");
1626    }
1627
1628    #[test]
1629    fn test_make_ansi_codes_strike_false_emits_29() {
1630        let style = Style {
1631            strike: Some(false),
1632            ..Default::default()
1633        };
1634        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "29");
1635    }
1636
1637    #[test]
1638    fn test_make_ansi_codes_dim_false_emits_22() {
1639        // dim=false also uses 22 (same as bold off)
1640        let style = Style {
1641            dim: Some(false),
1642            ..Default::default()
1643        };
1644        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "22");
1645    }
1646
1647    #[test]
1648    fn test_make_ansi_codes_bold_false_dim_true() {
1649        // Edge case: bold=false, dim=true should emit 22 (off), then 2 (dim on)
1650        let style = Style {
1651            bold: Some(false),
1652            dim: Some(true),
1653            ..Default::default()
1654        };
1655        assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "22;2");
1656    }
1657
1658    #[test]
1659    fn test_render_with_false_attribute() {
1660        let style = Style {
1661            bold: Some(false),
1662            ..Default::default()
1663        };
1664        let rendered = style.render("Hi", ColorSystem::TrueColor);
1665        assert_eq!(rendered, "\x1b[22mHi\x1b[0m");
1666    }
1667
1668    // --- Shorthand aliases tests ---
1669
1670    #[test]
1671    fn test_parse_shorthand_b_for_bold() {
1672        let style = Style::parse("b").unwrap();
1673        assert_eq!(style.bold, Some(true));
1674    }
1675
1676    #[test]
1677    fn test_parse_shorthand_i_for_italic() {
1678        let style = Style::parse("i").unwrap();
1679        assert_eq!(style.italic, Some(true));
1680    }
1681
1682    #[test]
1683    fn test_parse_shorthand_u_for_underline() {
1684        let style = Style::parse("u").unwrap();
1685        assert_eq!(style.underline, Some(true));
1686    }
1687
1688    #[test]
1689    fn test_parse_shorthand_s_for_strike() {
1690        let style = Style::parse("s").unwrap();
1691        assert_eq!(style.strike, Some(true));
1692    }
1693
1694    #[test]
1695    fn test_parse_shorthand_combined() {
1696        let style = Style::parse("b i red").unwrap();
1697        assert_eq!(style.bold, Some(true));
1698        assert_eq!(style.italic, Some(true));
1699        assert_eq!(style.color, Some(Color::Standard(1)));
1700    }
1701
1702    #[test]
1703    fn test_parse_shorthand_negation() {
1704        let style = Style::parse("not b").unwrap();
1705        assert_eq!(style.bold, Some(false));
1706
1707        let style = Style::parse("not i").unwrap();
1708        assert_eq!(style.italic, Some(false));
1709    }
1710
1711    // --- New attribute tests ---
1712
1713    #[test]
1714    fn test_parse_new_attributes() {
1715        let style = Style::parse("overline").unwrap();
1716        assert_eq!(style.overline, Some(true));
1717
1718        let style = Style::parse("blink2").unwrap();
1719        assert_eq!(style.blink2, Some(true));
1720
1721        let style = Style::parse("conceal").unwrap();
1722        assert_eq!(style.conceal, Some(true));
1723
1724        let style = Style::parse("underline2").unwrap();
1725        assert_eq!(style.underline2, Some(true));
1726
1727        let style = Style::parse("frame").unwrap();
1728        assert_eq!(style.frame, Some(true));
1729
1730        let style = Style::parse("encircle").unwrap();
1731        assert_eq!(style.encircle, Some(true));
1732    }
1733
1734    #[test]
1735    fn test_parse_new_attribute_shorthand() {
1736        let style = Style::parse("o").unwrap();
1737        assert_eq!(style.overline, Some(true));
1738
1739        let style = Style::parse("uu").unwrap();
1740        assert_eq!(style.underline2, Some(true));
1741
1742        let style = Style::parse("c").unwrap();
1743        assert_eq!(style.conceal, Some(true));
1744    }
1745
1746    #[test]
1747    fn test_parse_not_new_attributes() {
1748        let style = Style::parse("not overline").unwrap();
1749        assert_eq!(style.overline, Some(false));
1750
1751        let style = Style::parse("not blink2").unwrap();
1752        assert_eq!(style.blink2, Some(false));
1753
1754        let style = Style::parse("not conceal").unwrap();
1755        assert_eq!(style.conceal, Some(false));
1756
1757        let style = Style::parse("not underline2").unwrap();
1758        assert_eq!(style.underline2, Some(false));
1759
1760        let style = Style::parse("not frame").unwrap();
1761        assert_eq!(style.frame, Some(false));
1762
1763        let style = Style::parse("not encircle").unwrap();
1764        assert_eq!(style.encircle, Some(false));
1765    }
1766
1767    #[test]
1768    fn test_builder_new_attributes() {
1769        let style = Style::new().with_overline(true);
1770        assert_eq!(style.overline, Some(true));
1771
1772        let style = Style::new().with_blink2(true);
1773        assert_eq!(style.blink2, Some(true));
1774
1775        let style = Style::new().with_conceal(true);
1776        assert_eq!(style.conceal, Some(true));
1777
1778        let style = Style::new().with_underline2(true);
1779        assert_eq!(style.underline2, Some(true));
1780
1781        let style = Style::new().with_frame(true);
1782        assert_eq!(style.frame, Some(true));
1783
1784        let style = Style::new().with_encircle(true);
1785        assert_eq!(style.encircle, Some(true));
1786    }
1787
1788    #[test]
1789    fn test_render_new_attributes() {
1790        // overline = SGR 53
1791        let style = Style::new().with_overline(true);
1792        assert_eq!(
1793            style.render("X", ColorSystem::TrueColor),
1794            "\x1b[53mX\x1b[0m"
1795        );
1796
1797        // blink2 = SGR 6
1798        let style = Style::new().with_blink2(true);
1799        assert_eq!(style.render("X", ColorSystem::TrueColor), "\x1b[6mX\x1b[0m");
1800
1801        // conceal = SGR 8
1802        let style = Style::new().with_conceal(true);
1803        assert_eq!(style.render("X", ColorSystem::TrueColor), "\x1b[8mX\x1b[0m");
1804
1805        // underline2 = SGR 21
1806        let style = Style::new().with_underline2(true);
1807        assert_eq!(
1808            style.render("X", ColorSystem::TrueColor),
1809            "\x1b[21mX\x1b[0m"
1810        );
1811
1812        // frame = SGR 51
1813        let style = Style::new().with_frame(true);
1814        assert_eq!(
1815            style.render("X", ColorSystem::TrueColor),
1816            "\x1b[51mX\x1b[0m"
1817        );
1818
1819        // encircle = SGR 52
1820        let style = Style::new().with_encircle(true);
1821        assert_eq!(
1822            style.render("X", ColorSystem::TrueColor),
1823            "\x1b[52mX\x1b[0m"
1824        );
1825    }
1826
1827    #[test]
1828    fn test_render_new_attributes_off() {
1829        // overline off = SGR 55
1830        let style = Style::new().with_overline(false);
1831        assert_eq!(
1832            style.render("X", ColorSystem::TrueColor),
1833            "\x1b[55mX\x1b[0m"
1834        );
1835
1836        // conceal off = SGR 28
1837        let style = Style::new().with_conceal(false);
1838        assert_eq!(
1839            style.render("X", ColorSystem::TrueColor),
1840            "\x1b[28mX\x1b[0m"
1841        );
1842
1843        // frame off = SGR 54
1844        let style = Style::new().with_frame(false);
1845        assert_eq!(
1846            style.render("X", ColorSystem::TrueColor),
1847            "\x1b[54mX\x1b[0m"
1848        );
1849    }
1850
1851    #[test]
1852    fn test_combine_new_attributes() {
1853        let a = Style::new().with_overline(true);
1854        let b = Style::new().with_blink2(true);
1855        let combined = a.combine(&b);
1856        assert_eq!(combined.overline, Some(true));
1857        assert_eq!(combined.blink2, Some(true));
1858    }
1859
1860    #[test]
1861    fn test_render_all_new_attributes() {
1862        let style = Style {
1863            blink2: Some(true),
1864            conceal: Some(true),
1865            underline2: Some(true),
1866            frame: Some(true),
1867            encircle: Some(true),
1868            overline: Some(true),
1869            ..Default::default()
1870        };
1871        let rendered = style.render("X", ColorSystem::TrueColor);
1872        // blink2=6, conceal=8, underline2=21, frame=51, encircle=52, overline=53
1873        assert_eq!(rendered, "\x1b[6;8;21;51;52;53mX\x1b[0m");
1874    }
1875
1876    // --- chain / test / background_style / without_color / transparent_background ---
1877
1878    #[test]
1879    fn test_chain() {
1880        let a = Style::new().with_bold(true);
1881        let b = Style::new()
1882            .with_italic(true)
1883            .with_color(Color::Standard(1));
1884        let c = Style::new().with_underline(true);
1885        let result = Style::chain(&[a, b, c]);
1886        assert_eq!(result.bold, Some(true));
1887        assert_eq!(result.italic, Some(true));
1888        assert_eq!(result.underline, Some(true));
1889        assert_eq!(result.color, Some(Color::Standard(1)));
1890    }
1891
1892    #[test]
1893    fn test_from_color() {
1894        let style = Style::from_color(Some(Color::Standard(1)), Some(Color::Standard(4)));
1895        assert_eq!(style.color, Some(Color::Standard(1)));
1896        assert_eq!(style.bgcolor, Some(Color::Standard(4)));
1897        assert_eq!(style.bold, None);
1898        assert_eq!(style.italic, None);
1899    }
1900
1901    #[test]
1902    fn test_on_builds_meta_with_handlers() {
1903        let meta = Style::on(
1904            None,
1905            [
1906                ("click", MetaValue::str("handler")),
1907                ("focus", MetaValue::Bool(true)),
1908            ],
1909        );
1910
1911        assert!(meta.link.is_none());
1912        assert!(meta.link_id.is_none());
1913        let map = meta.meta.as_ref().expect("meta should be present");
1914        assert_eq!(map.get("@click"), Some(&MetaValue::str("handler")));
1915        assert_eq!(map.get("@focus"), Some(&MetaValue::Bool(true)));
1916    }
1917
1918    #[test]
1919    fn test_on_merges_existing_meta() {
1920        let mut base = BTreeMap::new();
1921        base.insert("existing".to_string(), MetaValue::Int(1));
1922
1923        let meta = Style::on(Some(base), [("blur", MetaValue::Bool(false))]);
1924        let map = meta.meta.as_ref().expect("meta should be present");
1925        assert_eq!(map.get("existing"), Some(&MetaValue::Int(1)));
1926        assert_eq!(map.get("@blur"), Some(&MetaValue::Bool(false)));
1927    }
1928
1929    #[test]
1930    fn test_normalize_valid_style() {
1931        assert_eq!(Style::normalize("  BOLD red ON blue "), "bold red on blue");
1932        assert_eq!(Style::normalize(""), "none");
1933        assert_eq!(Style::normalize(" none "), "none");
1934    }
1935
1936    #[test]
1937    fn test_normalize_invalid_style_returns_trimmed_lower() {
1938        assert_eq!(Style::normalize("  no_such_token  "), "no_such_token");
1939        assert_eq!(Style::normalize("foo bold"), "foo bold");
1940        assert_eq!(
1941            Style::normalize("bold on not_a_color"),
1942            "bold on not_a_color"
1943        );
1944    }
1945
1946    #[test]
1947    fn test_pick_first_returns_first_non_none() {
1948        let first = Style::new().with_bold(true);
1949        let second = Style::new().with_italic(true);
1950        let picked = Style::pick_first(&[None, Some(first), Some(second)]);
1951        assert_eq!(picked, first);
1952    }
1953
1954    #[test]
1955    #[should_panic(expected = "expected at least one non-None style")]
1956    fn test_pick_first_panics_when_all_none() {
1957        let _ = Style::pick_first(&[None, None]);
1958    }
1959
1960    #[test]
1961    fn test_background_style() {
1962        let style = Style::new()
1963            .with_bold(true)
1964            .with_color(Color::Standard(1))
1965            .with_bgcolor(Color::Standard(4));
1966        let bg = style.background_style();
1967        assert_eq!(bg.bgcolor, Some(Color::Standard(4)));
1968        assert_eq!(bg.color, None);
1969        assert_eq!(bg.bold, None);
1970    }
1971
1972    #[test]
1973    fn test_without_color() {
1974        let style = Style::new()
1975            .with_bold(true)
1976            .with_color(Color::Standard(1))
1977            .with_bgcolor(Color::Standard(4));
1978        let nc = style.without_color();
1979        assert_eq!(nc.color, None);
1980        assert_eq!(nc.bgcolor, None);
1981        assert_eq!(nc.bold, Some(true));
1982    }
1983
1984    #[test]
1985    fn test_has_transparent_background() {
1986        assert!(Style::new().has_transparent_background());
1987        assert!(
1988            Style::new()
1989                .with_bgcolor(Color::Default)
1990                .has_transparent_background()
1991        );
1992        assert!(
1993            !Style::new()
1994                .with_bgcolor(Color::Standard(1))
1995                .has_transparent_background()
1996        );
1997    }
1998
1999    // --- StyleStack tests ---
2000
2001    #[test]
2002    fn test_style_stack_new() {
2003        let stack = StyleStack::new(Style::new().with_bold(true));
2004        assert_eq!(stack.current().bold, Some(true));
2005    }
2006
2007    #[test]
2008    fn test_style_stack_push_pop() {
2009        let mut stack = StyleStack::new(Style::new().with_bold(true));
2010        stack.push(Style::new().with_italic(true));
2011        let current = stack.current();
2012        assert_eq!(current.bold, Some(true));
2013        assert_eq!(current.italic, Some(true));
2014
2015        stack.pop();
2016        let current = stack.current();
2017        assert_eq!(current.bold, Some(true));
2018        assert_eq!(current.italic, None);
2019    }
2020
2021    #[test]
2022    fn test_style_stack_multiple_push() {
2023        let mut stack = StyleStack::new(Style::new());
2024        stack.push(Style::new().with_bold(true));
2025        stack.push(Style::new().with_italic(true));
2026        stack.push(Style::new().with_underline(true));
2027
2028        let current = stack.current();
2029        assert_eq!(current.bold, Some(true));
2030        assert_eq!(current.italic, Some(true));
2031        assert_eq!(current.underline, Some(true));
2032
2033        stack.pop();
2034        let current = stack.current();
2035        assert_eq!(current.bold, Some(true));
2036        assert_eq!(current.italic, Some(true));
2037        assert_eq!(current.underline, None);
2038    }
2039
2040    // --- HTML style with overline ---
2041
2042    #[test]
2043    fn test_html_style_overline() {
2044        let style = Style::new().with_overline(true);
2045        let css = style.get_html_style();
2046        assert!(css.contains("text-decoration: overline"));
2047    }
2048
2049    #[test]
2050    fn test_html_style_underline_strike_overline_combined() {
2051        let style = Style::new()
2052            .with_underline(true)
2053            .with_strike(true)
2054            .with_overline(true);
2055        let css = style.get_html_style();
2056        assert!(css.contains("text-decoration: underline line-through overline"));
2057        assert_eq!(css.matches("text-decoration").count(), 1);
2058    }
2059}