Skip to main content

rusty_rich/
style.rs

1//! Text style — equivalent to Rich's `style.py`.
2//!
3//! A [`Style`] combines foreground/background color with 13 text attributes
4//! (bold, dim, italic, underline, blink, reverse, strike, underline2, frame,
5//! encircle, overline, blink2, conceal), plus optional link and metadata.
6//!
7//! # Quick Example
8//!
9//! ```rust
10//! use rusty_rich::{Style, Color};
11//!
12//! let style = Style::new()
13//!     .color(Color::parse("cyan").unwrap())
14//!     .bgcolor(Color::parse("#1E1E2E").unwrap())
15//!     .bold(true)
16//!     .italic(true);
17//!
18//! // Parse from a string
19//! let parsed = Style::from_str("bold red on blue");
20//! ```
21//!
22//! # Style Combination
23//!
24//! Styles combine left-to-right via [`Style::combine`] with a 3-state attribute
25//! cascade: explicit `true` wins over inherit, explicit `false` resets, and
26//! unset falls through to the parent.
27//!
28//! # StyleStack
29//!
30//! [`StyleStack`] tracks nested style inheritance for markup parsing. Push
31//! a style when entering a tag, pop when leaving.
32
33use std::fmt;
34use std::hash::{Hash, Hasher};
35use std::sync::atomic::{AtomicU32, Ordering};
36
37use crate::color::{Color, ColorType, EIGHT_BIT_PALETTE, STANDARD_COLOR_NAMES, STANDARD_PALETTE};
38
39static NEXT_ID: AtomicU32 = AtomicU32::new(0);
40
41// ---------------------------------------------------------------------------
42// Style attributes — bit flags
43// ---------------------------------------------------------------------------
44
45/// Bit flags for text attributes.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub struct Attributes(u32);
48
49impl Attributes {
50    /// Bit flag for bold text.
51    pub const BOLD: u32 = 1 << 0;
52    /// Bit flag for dim/dark text.
53    pub const DIM: u32 = 1 << 1;
54    /// Bit flag for italic text.
55    pub const ITALIC: u32 = 1 << 2;
56    /// Bit flag for underlined text.
57    pub const UNDERLINE: u32 = 1 << 3;
58    /// Bit flag for blinking text.
59    pub const BLINK: u32 = 1 << 4;
60    /// Bit flag for reverse-video text.
61    pub const REVERSE: u32 = 1 << 5;
62    /// Bit flag for strikethrough text.
63    pub const STRIKE: u32 = 1 << 6;
64    /// Bit flag for double underline.
65    pub const UNDERLINE2: u32 = 1 << 7;
66    /// Bit flag for framed text.
67    pub const FRAME: u32 = 1 << 8;
68    /// Bit flag for encircled text.
69    pub const ENCIRCLE: u32 = 1 << 9;
70    /// Bit flag for overlined text.
71    pub const OVERLINE: u32 = 1 << 10;
72    /// Bit flag for rapid blink.
73    pub const BLINK2: u32 = 1 << 11;
74    /// Bit flag for concealed/hidden text.
75    pub const CONCEAL: u32 = 1 << 12;
76
77    /// Create an empty set of attributes (no flags set).
78    pub const fn empty() -> Self {
79        Self(0)
80    }
81
82    /// Set or clear a specific attribute bit.
83    pub fn set(&mut self, bit: u32, value: bool) {
84        if value {
85            self.0 |= bit;
86        } else {
87            self.0 &= !bit;
88        }
89    }
90
91    /// Check whether a specific attribute bit is set.
92    pub fn get(&self, bit: u32) -> bool {
93        self.0 & bit != 0
94    }
95
96    /// Return the raw bitmask value.
97    pub const fn bits(&self) -> u32 {
98        self.0
99    }
100}
101
102/// All 13 style attribute bits in order (for iteration).
103pub const STYLE_BITS: &[u32] = &[
104    Attributes::BOLD, Attributes::DIM, Attributes::ITALIC,
105    Attributes::UNDERLINE, Attributes::BLINK, Attributes::REVERSE,
106    Attributes::STRIKE, Attributes::UNDERLINE2, Attributes::FRAME,
107    Attributes::ENCIRCLE, Attributes::OVERLINE, Attributes::BLINK2,
108    Attributes::CONCEAL,
109];
110
111/// All 13 style attribute (name, bit) pairs for iteration.
112pub const STYLE_ATTRIBUTES: &[(&str, u32)] = &[
113    ("bold", Attributes::BOLD),
114    ("dim", Attributes::DIM),
115    ("italic", Attributes::ITALIC),
116    ("underline", Attributes::UNDERLINE),
117    ("blink", Attributes::BLINK),
118    ("reverse", Attributes::REVERSE),
119    ("strike", Attributes::STRIKE),
120    ("underline2", Attributes::UNDERLINE2),
121    ("frame", Attributes::FRAME),
122    ("encircle", Attributes::ENCIRCLE),
123    ("overline", Attributes::OVERLINE),
124    ("blink2", Attributes::BLINK2),
125    ("conceal", Attributes::CONCEAL),
126];
127
128impl fmt::Display for Attributes {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        let mut parts: Vec<&str> = Vec::new();
131        if self.get(Self::BOLD) { parts.push("bold"); }
132        if self.get(Self::DIM) { parts.push("dim"); }
133        if self.get(Self::ITALIC) { parts.push("italic"); }
134        if self.get(Self::UNDERLINE) { parts.push("underline"); }
135        if self.get(Self::BLINK) { parts.push("blink"); }
136        if self.get(Self::REVERSE) { parts.push("reverse"); }
137        if self.get(Self::CONCEAL) { parts.push("conceal"); }
138        if self.get(Self::STRIKE) { parts.push("strike"); }
139        if self.get(Self::OVERLINE) { parts.push("overline"); }
140        if parts.is_empty() {
141            write!(f, "none")
142        } else {
143            write!(f, "{}", parts.join(" "))
144        }
145    }
146}
147
148// ---------------------------------------------------------------------------
149// Style
150// ---------------------------------------------------------------------------
151
152/// A terminal style.
153///
154/// Supports foreground color, background color, attributes, and an optional
155/// hyperlink. Attributes use a three-state system: set to `true`, set to
156/// `false`, or not set (`None`).
157#[derive(Debug, Clone)]
158pub struct Style {
159    pub(crate) color: Option<Color>,
160    pub(crate) bgcolor: Option<Color>,
161    pub(crate) attributes: Attributes,
162    /// Which attribute bits have been explicitly set (vs inherited).
163    pub(crate) set_attributes: u32,
164    pub(crate) link: Option<String>,
165    pub(crate) link_id: u32,
166    pub(crate) is_null: bool,
167    /// Arbitrary metadata attached to this style.
168    pub(crate) meta: Option<Vec<u8>>,
169}
170
171impl Style {
172    // -- constructors -------------------------------------------------------
173
174    /// Create a null (empty) style.
175    pub fn null() -> Self {
176        Self {
177            color: None,
178            bgcolor: None,
179            attributes: Attributes::empty(),
180            set_attributes: 0,
181            link: None,
182            link_id: 0,
183            is_null: true,
184            meta: None,
185        }
186    }
187
188    /// Create a new style with optional settings.
189    pub fn new() -> Self {
190        Self {
191            color: None,
192            bgcolor: None,
193            attributes: Attributes::empty(),
194            set_attributes: 0,
195            link: None,
196            link_id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
197            is_null: false,
198            meta: None,
199        }
200    }
201
202    /// Builder: set foreground color.
203    pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
204        self.color = color.into();
205        self
206    }
207
208    /// Builder: set background color.
209    pub fn bgcolor(mut self, bgcolor: impl Into<Option<Color>>) -> Self {
210        self.bgcolor = bgcolor.into();
211        self
212    }
213
214    /// Builder: set bold.
215    pub fn bold(mut self, value: bool) -> Self {
216        self.set_attributes |= Attributes::BOLD;
217        self.attributes.set(Attributes::BOLD, value);
218        self
219    }
220
221    /// Builder: set dim.
222    pub fn dim(mut self, value: bool) -> Self {
223        self.set_attributes |= Attributes::DIM;
224        self.attributes.set(Attributes::DIM, value);
225        self
226    }
227
228    /// Builder: set italic.
229    pub fn italic(mut self, value: bool) -> Self {
230        self.set_attributes |= Attributes::ITALIC;
231        self.attributes.set(Attributes::ITALIC, value);
232        self
233    }
234
235    /// Builder: set underline.
236    pub fn underline(mut self, value: bool) -> Self {
237        self.set_attributes |= Attributes::UNDERLINE;
238        self.attributes.set(Attributes::UNDERLINE, value);
239        self
240    }
241
242    /// Builder: set blink.
243    pub fn blink(mut self, value: bool) -> Self {
244        self.set_attributes |= Attributes::BLINK;
245        self.attributes.set(Attributes::BLINK, value);
246        self
247    }
248
249    /// Builder: set reverse.
250    pub fn reverse(mut self, value: bool) -> Self {
251        self.set_attributes |= Attributes::REVERSE;
252        self.attributes.set(Attributes::REVERSE, value);
253        self
254    }
255
256    /// Builder: set strikethrough.
257    pub fn strike(mut self, value: bool) -> Self {
258        self.set_attributes |= Attributes::STRIKE;
259        self.attributes.set(Attributes::STRIKE, value);
260        self
261    }
262
263    /// Builder: set blink2 (rapid blink).
264    pub fn blink2(mut self, value: bool) -> Self {
265        self.set_attributes |= Attributes::BLINK2;
266        self.attributes.set(Attributes::BLINK2, value);
267        self
268    }
269
270    /// Builder: set conceal.
271    pub fn conceal(mut self, value: bool) -> Self {
272        self.set_attributes |= Attributes::CONCEAL;
273        self.attributes.set(Attributes::CONCEAL, value);
274        self
275    }
276
277    /// Builder: set double underline.
278    pub fn underline2(mut self, value: bool) -> Self {
279        self.set_attributes |= Attributes::UNDERLINE2;
280        self.attributes.set(Attributes::UNDERLINE2, value);
281        self
282    }
283
284    /// Builder: set frame.
285    pub fn frame(mut self, value: bool) -> Self {
286        self.set_attributes |= Attributes::FRAME;
287        self.attributes.set(Attributes::FRAME, value);
288        self
289    }
290
291    /// Builder: set encircle.
292    pub fn encircle(mut self, value: bool) -> Self {
293        self.set_attributes |= Attributes::ENCIRCLE;
294        self.attributes.set(Attributes::ENCIRCLE, value);
295        self
296    }
297
298    /// Builder: set overline.
299    pub fn overline(mut self, value: bool) -> Self {
300        self.set_attributes |= Attributes::OVERLINE;
301        self.attributes.set(Attributes::OVERLINE, value);
302        self
303    }
304
305    /// Return a copy with foreground and background colors stripped.
306    pub fn without_color(&self) -> Self {
307        let mut s = self.clone();
308        s.color = None;
309        s.bgcolor = None;
310        s
311    }
312
313    /// Return a style with the background color set to the foreground color,
314    /// useful for background-only rendering.
315    pub fn background_style(&self) -> Self {
316        let mut s = Self::new();
317        s.bgcolor = self.color.clone();
318        s
319    }
320
321    /// Returns true if the background is not set (transparent).
322    pub fn transparent_background(&self) -> bool {
323        self.bgcolor.is_none()
324    }
325
326    /// Builder: set link.
327    pub fn link(mut self, url: impl Into<String>) -> Self {
328        self.link = Some(url.into());
329        self.link_id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
330        self
331    }
332
333    /// Builder: set style from a string (e.g. "bold red on blue").
334    pub fn from_str(definition: &str) -> Self {
335        let mut style = Self::new();
336        for part in definition.split_whitespace() {
337            match part {
338                "bold" | "b" => { style.set_attributes |= Attributes::BOLD; style.attributes.set(Attributes::BOLD, true); }
339                "dim" | "d" => { style.set_attributes |= Attributes::DIM; style.attributes.set(Attributes::DIM, true); }
340                "italic" | "i" => { style.set_attributes |= Attributes::ITALIC; style.attributes.set(Attributes::ITALIC, true); }
341                "underline" | "u" => { style.set_attributes |= Attributes::UNDERLINE; style.attributes.set(Attributes::UNDERLINE, true); }
342                "blink" => { style.set_attributes |= Attributes::BLINK; style.attributes.set(Attributes::BLINK, true); }
343                "reverse" | "r" => { style.set_attributes |= Attributes::REVERSE; style.attributes.set(Attributes::REVERSE, true); }
344                "strike" | "s" => { style.set_attributes |= Attributes::STRIKE; style.attributes.set(Attributes::STRIKE, true); }
345                "not bold" | "!bold" | "nobold" => { style.set_attributes |= Attributes::BOLD; style.attributes.set(Attributes::BOLD, false); }
346                "not italic" | "!italic" | "noitalic" => { style.set_attributes |= Attributes::ITALIC; style.attributes.set(Attributes::ITALIC, false); }
347                "not underline" | "!underline" | "nounderline" => { style.set_attributes |= Attributes::UNDERLINE; style.attributes.set(Attributes::UNDERLINE, false); }
348                "none" | "default" => {}
349                "on" => { /* "on <color>" handled below */ }
350                part if part.starts_with("on ") => {
351                    if let Ok(c) = Color::parse(&part[3..]) {
352                        style.bgcolor = Some(c);
353                    }
354                }
355                part if part.starts_with("link=") => {
356                    style.link = Some(part[5..].to_string());
357                }
358                part => {
359                    // Try as color name
360                    if let Ok(c) = Color::parse(part) {
361                        if style.bgcolor.is_some() && style.color.is_none() {
362                            // We already saw "on" — don't overwrite fg
363                        } else {
364                            style.color = Some(c);
365                        }
366                    }
367                }
368            }
369        }
370        style
371    }
372
373    // -- queries ------------------------------------------------------------
374
375    /// Returns `true` if this is a null (empty) style.
376    pub fn is_null(&self) -> bool {
377        self.is_null
378    }
379
380    /// Returns `true` if this style has no colors, attributes, or link set.
381    pub fn is_plain(&self) -> bool {
382        self.color.is_none()
383            && self.bgcolor.is_none()
384            && self.set_attributes == 0
385            && self.link.is_none()
386    }
387
388    /// Check if the bold attribute is explicitly set to `true`.
389    pub fn get_bold(&self) -> Option<bool> {
390        if self.set_attributes & Attributes::BOLD != 0 {
391            Some(self.attributes.get(Attributes::BOLD))
392        } else {
393            None
394        }
395    }
396
397    /// Check if the italic attribute is explicitly set to `true`.
398    pub fn get_italic(&self) -> Option<bool> {
399        if self.set_attributes & Attributes::ITALIC != 0 {
400            Some(self.attributes.get(Attributes::ITALIC))
401        } else {
402            None
403        }
404    }
405
406    /// Merge two styles: `self` is the base, `other` overrides.
407    pub fn combine(&self, other: &Style) -> Style {
408        if other.is_null {
409            return self.clone();
410        }
411        if self.is_null {
412            return other.clone();
413        }
414
415        let mut combined = self.clone();
416        if other.color.is_some() {
417            combined.color = other.color.clone();
418        }
419        if other.bgcolor.is_some() {
420            combined.bgcolor = other.bgcolor.clone();
421        }
422        // Attributes: other's set bits override self (3-state cascade)
423        for &bit in STYLE_BITS {
424            if other.set_attributes & bit != 0 {
425                combined.set_attributes |= bit;
426                combined.attributes.set(bit, other.attributes.get(bit));
427            }
428        }
429        if other.link.is_some() {
430            combined.link = other.link.clone();
431            combined.link_id = other.link_id;
432        }
433        if other.meta.is_some() {
434            combined.meta = other.meta.clone();
435        }
436        combined.is_null = false;
437        combined
438    }
439
440    /// Render this style as ANSI SGR escape sequences.
441    pub fn to_ansi(&self) -> String {
442        if self.is_null {
443            return String::new();
444        }
445        let mut codes: Vec<String> = Vec::new();
446
447        // Foreground color
448        if let Some(ref c) = self.color {
449            match c.color_type {
450                crate::color::ColorType::Default => codes.push("39".into()),
451                crate::color::ColorType::Standard => {
452                    if let Some(n) = c.number {
453                        if n < 8 {
454                            codes.push((30 + n).to_string());
455                        } else {
456                            codes.push((82 + n).to_string()); // 90-97 for bright
457                        }
458                    }
459                }
460                crate::color::ColorType::EightBit => {
461                    if let Some(n) = c.number {
462                        codes.push(format!("38;5;{n}"));
463                    }
464                }
465                crate::color::ColorType::TrueColor => {
466                    if let Some((r, g, b)) = c.triplet {
467                        codes.push(format!("38;2;{r};{g};{b}"));
468                    }
469                }
470            }
471        }
472
473        // Background color
474        if let Some(ref c) = self.bgcolor {
475            match c.color_type {
476                crate::color::ColorType::Default => codes.push("49".into()),
477                crate::color::ColorType::Standard => {
478                    if let Some(n) = c.number {
479                        if n < 8 {
480                            codes.push((40 + n).to_string());
481                        } else {
482                            codes.push((92 + n).to_string()); // 100-107
483                        }
484                    }
485                }
486                crate::color::ColorType::EightBit => {
487                    if let Some(n) = c.number {
488                        codes.push(format!("48;5;{n}"));
489                    }
490                }
491                crate::color::ColorType::TrueColor => {
492                    if let Some((r, g, b)) = c.triplet {
493                        codes.push(format!("48;2;{r};{g};{b}"));
494                    }
495                }
496            }
497        }
498
499        // Attributes
500        if self.set_attributes & Attributes::BOLD != 0 {
501            codes.push(if self.attributes.get(Attributes::BOLD) { "1" } else { "22" }.into());
502        }
503        if self.set_attributes & Attributes::DIM != 0 {
504            codes.push(if self.attributes.get(Attributes::DIM) { "2" } else { "22" }.into());
505        }
506        if self.set_attributes & Attributes::ITALIC != 0 {
507            codes.push(if self.attributes.get(Attributes::ITALIC) { "3" } else { "23" }.into());
508        }
509        if self.set_attributes & Attributes::UNDERLINE != 0 {
510            codes.push(if self.attributes.get(Attributes::UNDERLINE) { "4" } else { "24" }.into());
511        }
512        if self.set_attributes & Attributes::BLINK != 0 {
513            codes.push(if self.attributes.get(Attributes::BLINK) { "5" } else { "25" }.into());
514        }
515        if self.set_attributes & Attributes::REVERSE != 0 {
516            codes.push(if self.attributes.get(Attributes::REVERSE) { "7" } else { "27" }.into());
517        }
518        if self.set_attributes & Attributes::CONCEAL != 0 {
519            codes.push(if self.attributes.get(Attributes::CONCEAL) { "8" } else { "28" }.into());
520        }
521        if self.set_attributes & Attributes::STRIKE != 0 {
522            codes.push(if self.attributes.get(Attributes::STRIKE) { "9" } else { "29" }.into());
523        }
524        if self.set_attributes & Attributes::CONCEAL != 0 {
525            codes.push(if self.attributes.get(Attributes::CONCEAL) { "8" } else { "28" }.into());
526        }
527        if self.set_attributes & Attributes::UNDERLINE2 != 0 {
528            codes.push(if self.attributes.get(Attributes::UNDERLINE2) { "21" } else { "24" }.into());
529        }
530        if self.set_attributes & Attributes::BLINK2 != 0 {
531            codes.push(if self.attributes.get(Attributes::BLINK2) { "6" } else { "25" }.into());
532        }
533        if self.set_attributes & Attributes::FRAME != 0 {
534            codes.push(if self.attributes.get(Attributes::FRAME) { "51" } else { "54" }.into());
535        }
536        if self.set_attributes & Attributes::ENCIRCLE != 0 {
537            codes.push(if self.attributes.get(Attributes::ENCIRCLE) { "52" } else { "54" }.into());
538        }
539        if self.set_attributes & Attributes::OVERLINE != 0 {
540            codes.push(if self.attributes.get(Attributes::OVERLINE) { "53" } else { "55" }.into());
541        }
542
543        if codes.is_empty() {
544            String::new()
545        } else {
546            format!("\x1b[{}m", codes.join(";"))
547        }
548    }
549
550    /// Return the ANSI reset sequence needed to turn off this style.
551    pub fn reset_ansi(&self) -> &'static str {
552        "\x1b[0m"
553    }
554
555    // -- chaining --------------------------------------------------------------
556
557    /// Create a chain-of-styles fallback. When `self` has a value set, use it;
558    /// otherwise fall through to `other`.
559    pub fn chain(&self, other: &Style) -> Style {
560        let mut result = Style::new();
561        result.color = self.color.clone().or_else(|| other.color.clone());
562        result.bgcolor = self.bgcolor.clone().or_else(|| other.bgcolor.clone());
563        result.link = self.link.clone().or_else(|| other.link.clone());
564        result.meta = self.meta.clone().or_else(|| other.meta.clone());
565        for &bit in STYLE_BITS {
566            if self.set_attributes & bit != 0 {
567                result.set_attributes |= bit;
568                result.attributes.set(bit, self.attributes.get(bit));
569            } else if other.set_attributes & bit != 0 {
570                result.set_attributes |= bit;
571                result.attributes.set(bit, other.attributes.get(bit));
572            }
573        }
574        result
575    }
576
577    // -- copy / clear ----------------------------------------------------------
578
579    /// Explicit clone (delegates to Clone, named for Python parity).
580    pub fn copy(&self) -> Style {
581        self.clone()
582    }
583
584    /// Clear the meta field and link field, returning self for chaining.
585    pub fn clear_meta_and_links(&mut self) -> &mut Self {
586        self.meta = None;
587        self.link = None;
588        self
589    }
590
591    // -- constructors ----------------------------------------------------------
592
593    /// Create a style with just a foreground color set.
594    pub fn from_color(color: Color) -> Self {
595        Self::new().color(color)
596    }
597
598    /// Create a style with metadata.
599    pub fn from_meta(meta: Vec<u8>) -> Self {
600        let mut s = Self::new();
601        s.meta = Some(meta);
602        s
603    }
604
605    // -- html export -----------------------------------------------------------
606
607    /// Generate CSS style string for HTML export.
608    pub fn get_html_style(&self, _theme: Option<&crate::export::ExportTheme>) -> String {
609        if self.is_null {
610            return String::new();
611        }
612        let mut parts: Vec<String> = Vec::new();
613
614        if let Some(ref c) = self.color {
615            let hex = color_to_css_hex(c);
616            if !hex.is_empty() {
617                parts.push(format!("color: {}", hex));
618            }
619        }
620        if let Some(ref c) = self.bgcolor {
621            let hex = color_to_css_hex(c);
622            if !hex.is_empty() {
623                parts.push(format!("background-color: {}", hex));
624            }
625        }
626        if self.set_attributes & Attributes::BOLD != 0 && self.attributes.get(Attributes::BOLD) {
627            parts.push("font-weight: bold".into());
628        }
629        if self.set_attributes & Attributes::ITALIC != 0 && self.attributes.get(Attributes::ITALIC) {
630            parts.push("font-style: italic".into());
631        }
632
633        // text-decoration: combine underline and strike
634        let mut decor: Vec<&str> = Vec::new();
635        if self.set_attributes & Attributes::UNDERLINE != 0
636            && self.attributes.get(Attributes::UNDERLINE)
637        {
638            decor.push("underline");
639        }
640        if self.set_attributes & Attributes::UNDERLINE2 != 0
641            && self.attributes.get(Attributes::UNDERLINE2)
642        {
643            decor.push("underline");
644        }
645        if self.set_attributes & Attributes::STRIKE != 0
646            && self.attributes.get(Attributes::STRIKE)
647        {
648            decor.push("line-through");
649        }
650        if !decor.is_empty() {
651            parts.push(format!("text-decoration: {}", decor.join(" ")));
652        }
653
654        if parts.is_empty() {
655            String::new()
656        } else {
657            parts.join("; ")
658        }
659    }
660
661    // -- normalize -------------------------------------------------------------
662
663    /// Return a "normalized" style: remove negative (explicitly false) attributes
664    /// that just reset inherited ones. Only keep explicitly true attributes and
665    /// colors.
666    pub fn normalize(&self) -> Style {
667        let mut s = Style::new();
668        s.color = self.color.clone();
669        s.bgcolor = self.bgcolor.clone();
670        s.link = self.link.clone();
671        s.link_id = self.link_id;
672        s.meta = self.meta.clone();
673        for &bit in STYLE_BITS {
674            if self.set_attributes & bit != 0 && self.attributes.get(bit) {
675                s.set_attributes |= bit;
676                s.attributes.set(bit, true);
677            }
678        }
679        s
680    }
681
682    // -- utility ---------------------------------------------------------------
683
684    /// Return the "first" significant color name for display purposes
685    /// (fg color name, or bg color name, or None).
686    pub fn pick_first(&self) -> Option<&'static str> {
687        if let Some(ref c) = self.color {
688            if let Some(name) = color_to_name(c) {
689                return Some(name);
690            }
691        }
692        if let Some(ref c) = self.bgcolor {
693            if let Some(name) = color_to_name(c) {
694                return Some(name);
695            }
696        }
697        None
698    }
699
700    /// Render `text` wrapped in this style's ANSI codes.
701    pub fn render(&self, text: &str) -> String {
702        format!("{}{}{}", self.to_ansi(), text, self.reset_ansi())
703    }
704
705    /// Render a test/demo string. If text is None, use "Lorem ipsum".
706    pub fn test(&self, text: Option<&str>) -> String {
707        let t = text.unwrap_or("Lorem ipsum");
708        self.render(t)
709    }
710
711    /// Update or clear the link, returning self for chaining.
712    pub fn update_link(&mut self, url: Option<String>) -> &mut Self {
713        self.link = url;
714        if self.link.is_some() {
715            self.link_id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
716        }
717        self
718    }
719
720    // -- accessors -------------------------------------------------------------
721
722    /// Get a reference to metadata.
723    pub fn meta(&self) -> Option<&Vec<u8>> {
724        self.meta.as_ref()
725    }
726
727    /// Get a mutable reference to metadata.
728    pub fn meta_mut(&mut self) -> Option<&mut Vec<u8>> {
729        self.meta.as_mut()
730    }
731
732    /// Set metadata, returning self for chaining.
733    pub fn set_meta(&mut self, meta: Option<Vec<u8>>) -> &mut Self {
734        self.meta = meta;
735        self
736    }
737
738    /// Get the link ID.
739    pub fn link_id(&self) -> u32 {
740        self.link_id
741    }
742
743    /// Alias for `bgcolor()` (Python rich has both `.on()` and `.bgcolor()`).
744    pub fn on(self, color: impl Into<Option<Color>>) -> Self {
745        self.bgcolor(color)
746    }
747
748    /// Get a reference to the foreground color.
749    pub fn color_ref(&self) -> Option<&Color> {
750        self.color.as_ref()
751    }
752
753    /// Get a reference to the background color.
754    pub fn bgcolor_ref(&self) -> Option<&Color> {
755        self.bgcolor.as_ref()
756    }
757}
758
759impl Default for Style {
760    fn default() -> Self {
761        Self::new()
762    }
763}
764
765impl PartialEq for Style {
766    fn eq(&self, other: &Self) -> bool {
767        self.color == other.color
768            && self.bgcolor == other.bgcolor
769            && self.attributes == other.attributes
770            && self.set_attributes == other.set_attributes
771            && self.link == other.link
772    }
773}
774
775impl Eq for Style {}
776
777impl Hash for Style {
778    fn hash<H: Hasher>(&self, state: &mut H) {
779        self.color.hash(state);
780        self.bgcolor.hash(state);
781        self.attributes.hash(state);
782        self.set_attributes.hash(state);
783        self.link.hash(state);
784    }
785}
786
787impl fmt::Display for Style {
788    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
789        if self.is_null {
790            return write!(f, "null");
791        }
792        let mut parts: Vec<String> = Vec::new();
793        if let Some(ref c) = self.color {
794            parts.push(c.to_string());
795        }
796        if let Some(ref c) = self.bgcolor {
797            parts.push(format!("on {}", c));
798        }
799        let attrs = self.attributes.to_string();
800        if attrs != "none" {
801            parts.push(attrs);
802        }
803        if parts.is_empty() {
804            write!(f, "none")
805        } else {
806            write!(f, "{}", parts.join(" "))
807        }
808    }
809}
810
811/// Convenience type alias.
812pub type StyleType = Style;
813
814// -- helper functions for html export and color name lookup ------------------
815
816/// Convert a `Color` to a CSS hex string `#rrggbb`.
817fn color_to_css_hex(c: &Color) -> String {
818    match c.color_type {
819        ColorType::Default => String::new(),
820        ColorType::Standard => {
821            if let Some(n) = c.number {
822                let (r, g, b) = STANDARD_PALETTE[n as usize];
823                format!("#{:02x}{:02x}{:02x}", r, g, b)
824            } else {
825                String::new()
826            }
827        }
828        ColorType::EightBit => {
829            if let Some(n) = c.number {
830                let [r, g, b] = EIGHT_BIT_PALETTE[n as usize];
831                format!("#{:02x}{:02x}{:02x}", r, g, b)
832            } else {
833                String::new()
834            }
835        }
836        ColorType::TrueColor => {
837            if let Some((r, g, b)) = c.triplet {
838                format!("#{:02x}{:02x}{:02x}", r, g, b)
839            } else {
840                String::new()
841            }
842        }
843    }
844}
845
846/// Return the static color name for a Standard color, or `None` otherwise.
847fn color_to_name(c: &Color) -> Option<&'static str> {
848    match c.color_type {
849        ColorType::Standard => {
850            if let Some(n) = c.number {
851                Some(STANDARD_COLOR_NAMES[n as usize])
852            } else {
853                None
854            }
855        }
856        _ => None,
857    }
858}
859
860// ---------------------------------------------------------------------------
861// StyleStack — a stack of styles (for nested markup)
862// ---------------------------------------------------------------------------
863
864/// A stack of styles, used when rendering nested markup.
865#[derive(Debug, Clone)]
866pub struct StyleStack {
867    stack: Vec<Style>,
868    default_style: Style,
869}
870
871impl StyleStack {
872    /// Create a new style stack with a given default style.
873    pub fn new(default_style: Style) -> Self {
874        Self {
875            stack: Vec::new(),
876            default_style,
877        }
878    }
879
880    /// Get the current (combined) style.
881    pub fn current(&self) -> Style {
882        let mut combined = self.default_style.clone();
883        for s in &self.stack {
884            combined = combined.combine(s);
885        }
886        combined
887    }
888
889    /// Push a style onto the stack.
890    pub fn push(&mut self, style: Style) {
891        self.stack.push(style);
892    }
893
894    /// Pop the top style.
895    pub fn pop(&mut self) -> Option<Style> {
896        self.stack.pop()
897    }
898
899    /// Get the depth.
900    pub fn len(&self) -> usize {
901        self.stack.len()
902    }
903
904    /// Returns `true` if the stack is empty (no pushed styles).
905    pub fn is_empty(&self) -> bool {
906        self.stack.is_empty()
907    }
908}
909
910#[cfg(test)]
911mod tests {
912    use super::*;
913
914    #[test]
915    fn test_style_parse() {
916        let s = Style::from_str("bold red");
917        assert_eq!(s.get_bold(), Some(true));
918        assert!(s.color.is_some());
919    }
920
921    #[test]
922    fn test_style_combine() {
923        let base = Style::from_str("red");
924        let over = Style::from_str("bold");
925        let combined = base.combine(&over);
926        assert_eq!(combined.get_bold(), Some(true));
927        assert!(combined.color.is_some());
928    }
929
930    #[test]
931    fn test_ansi_output() {
932        let s = Style::new().color(Color::parse("red").unwrap()).bold(true);
933        let ansi = s.to_ansi();
934        assert!(ansi.contains("31")); // red foreground
935        assert!(ansi.contains("1"));  // bold
936    }
937
938    #[test]
939    fn test_chain() {
940        let a = Style::new().bold(true);
941        let b = Style::new().color(Color::parse("red").unwrap()).italic(true);
942        let chained = a.chain(&b);
943        assert_eq!(chained.get_bold(), Some(true));
944        assert!(chained.attributes.get(Attributes::ITALIC));
945        assert!(chained.set_attributes & Attributes::ITALIC != 0);
946        assert!(chained.color.is_some());
947    }
948
949    #[test]
950    fn test_chain_precedence() {
951        let a = Style::new().bold(true).color(Color::parse("red").unwrap());
952        let b = Style::new().bold(false).color(Color::parse("blue").unwrap());
953        let chained = a.chain(&b);
954        // a sets bold(true) and color(red); b sets bold(false) and color(blue)
955        // chain: self's values take priority
956        assert_eq!(chained.get_bold(), Some(true));
957        let c = chained.color.as_ref().unwrap();
958        let name = color_to_name(c);
959        assert_eq!(name, Some("red"));
960    }
961
962    #[test]
963    fn test_copy() {
964        let s = Style::new().bold(true).color(Color::parse("red").unwrap());
965        let c = s.copy();
966        assert_eq!(s, c);
967    }
968
969    #[test]
970    fn test_clear_meta_and_links() {
971        let mut s = Style::new().link("https://example.com");
972        s.meta = Some(vec![1, 2, 3]);
973        s.clear_meta_and_links();
974        assert!(s.link.is_none());
975        assert!(s.meta.is_none());
976    }
977
978    #[test]
979    fn test_from_color() {
980        let s = Style::from_color(Color::parse("red").unwrap());
981        assert!(s.color.is_some());
982        assert!(s.bgcolor.is_none());
983    }
984
985    #[test]
986    fn test_from_meta() {
987        let s = Style::from_meta(vec![10, 20, 30]);
988        assert_eq!(s.meta(), Some(&vec![10, 20, 30]));
989    }
990
991    #[test]
992    fn test_get_html_style() {
993        let s = Style::new()
994            .color(Color::parse("red").unwrap())
995            .bold(true)
996            .italic(true);
997        let css = s.get_html_style(None);
998        assert!(css.contains("color:"));
999        assert!(css.contains("font-weight: bold"));
1000        assert!(css.contains("font-style: italic"));
1001    }
1002
1003    #[test]
1004    fn test_get_html_style_underline_strike() {
1005        let s = Style::new()
1006            .color(Color::parse("red").unwrap())
1007            .underline(true)
1008            .strike(true);
1009        let css = s.get_html_style(None);
1010        assert!(css.contains("text-decoration:"));
1011        assert!(css.contains("underline"));
1012        assert!(css.contains("line-through"));
1013    }
1014
1015    #[test]
1016    fn test_get_html_style_null() {
1017        let s = Style::null();
1018        let css = s.get_html_style(None);
1019        assert!(css.is_empty());
1020    }
1021
1022    #[test]
1023    fn test_normalize() {
1024        let s = Style::new().bold(true).italic(false);
1025        let n = s.normalize();
1026        assert_eq!(n.get_bold(), Some(true));
1027        // italic was set to false, so normalize should remove it
1028        assert!(n.set_attributes & Attributes::ITALIC == 0);
1029    }
1030
1031    #[test]
1032    fn test_pick_first() {
1033        let s = Style::new().color(Color::parse("red").unwrap());
1034        assert_eq!(s.pick_first(), Some("red"));
1035    }
1036
1037    #[test]
1038    fn test_pick_first_fallback() {
1039        let s = Style::new().bgcolor(Color::parse("blue").unwrap());
1040        assert_eq!(s.pick_first(), Some("blue"));
1041    }
1042
1043    #[test]
1044    fn test_pick_first_none() {
1045        let s = Style::new();
1046        assert_eq!(s.pick_first(), None);
1047    }
1048
1049    #[test]
1050    fn test_render() {
1051        let s = Style::new().bold(true).color(Color::parse("red").unwrap());
1052        let rendered = s.render("hello");
1053        assert!(rendered.starts_with("\x1b["));
1054        assert!(rendered.contains("hello"));
1055        assert!(rendered.ends_with("\x1b[0m"));
1056    }
1057
1058    #[test]
1059    fn test_test_with_text() {
1060        let s = Style::new().bold(true);
1061        let out = s.test(Some("custom"));
1062        assert!(out.contains("custom"));
1063    }
1064
1065    #[test]
1066    fn test_test_default() {
1067        let s = Style::new().bold(true);
1068        let out = s.test(None);
1069        assert!(out.contains("Lorem ipsum"));
1070    }
1071
1072    #[test]
1073    fn test_update_link() {
1074        let mut s = Style::new();
1075        s.update_link(Some("https://example.com".into()));
1076        assert!(s.link.is_some());
1077        let first_id = s.link_id;
1078        s.update_link(None);
1079        assert!(s.link.is_none());
1080        assert_eq!(s.link_id, first_id);
1081    }
1082
1083    #[test]
1084    fn test_link_id() {
1085        let s = Style::new().link("https://example.com");
1086        assert!(s.link_id() > 0);
1087    }
1088
1089    #[test]
1090    fn test_meta_methods() {
1091        let mut s = Style::new();
1092        s.set_meta(Some(vec![1, 2, 3]));
1093        assert_eq!(s.meta(), Some(&vec![1, 2, 3]));
1094        if let Some(m) = s.meta_mut() {
1095            m.push(4);
1096        }
1097        assert_eq!(s.meta(), Some(&vec![1, 2, 3, 4]));
1098    }
1099
1100    #[test]
1101    fn test_on() {
1102        let s = Style::new().on(Color::parse("red").unwrap());
1103        assert!(s.bgcolor.is_some());
1104        let b = Color::parse("red").unwrap();
1105        assert_eq!(s.bgcolor.unwrap(), b);
1106    }
1107
1108    #[test]
1109    fn test_references() {
1110        let s = Style::new()
1111            .color(Color::parse("red").unwrap())
1112            .bgcolor(Color::parse("blue").unwrap());
1113        assert!(s.color_ref().is_some());
1114        assert!(s.bgcolor_ref().is_some());
1115    }
1116
1117    #[test]
1118    fn test_color_to_css_hex() {
1119        let c = Color::parse("red").unwrap();
1120        let hex = color_to_css_hex(&c);
1121        assert_eq!(hex, "#800000"); // standard red
1122    }
1123
1124    #[test]
1125    fn test_color_to_css_hex_truecolor() {
1126        let c = Color::from_rgb(255, 0, 128);
1127        let hex = color_to_css_hex(&c);
1128        assert_eq!(hex, "#ff0080");
1129    }
1130
1131    #[test]
1132    fn test_static_attributes() {
1133        assert!(!STYLE_ATTRIBUTES.is_empty());
1134        let names: Vec<&str> = STYLE_ATTRIBUTES.iter().map(|(n, _)| *n).collect();
1135        assert!(names.contains(&"bold"));
1136        assert!(names.contains(&"italic"));
1137        assert!(names.contains(&"underline"));
1138        assert!(!names.contains(&"notexist"));
1139    }
1140}