Skip to main content

styled_str/types/
mod.rs

1//! Basic types.
2
3use core::fmt;
4
5use self::slice::SpansSlice;
6pub(crate) use self::spans::StyledSpan;
7pub use self::{
8    lines::Lines,
9    spans::SpanStr,
10    str::StyledStr,
11    string::{StyledString, StyledStringBuilder},
12};
13use crate::{
14    StyleDiff,
15    utils::{Stack, StackStr},
16};
17
18mod lines;
19mod slice;
20mod spans;
21mod str;
22mod string;
23
24/// Text difference between two strings. ANSI-styled when printed (powered by [`pretty_assertions::Comparison`]).
25///
26/// # [`Display`](fmt::Display) representation
27///
28/// You can specify additional padding at the start of compared lines
29/// via alignment specifiers. For example, `{:>4}` will insert 4 spaces at the start of each line.
30///
31/// # Examples
32///
33/// ```
34/// use styled_str::{StyledString, TextDiff};
35///
36/// let diff = TextDiff::new("Hello, world", "Hello world!");
37/// let diff_str = StyledString::from_ansi(&format!("{diff:>4}"))?;
38/// assert_eq!(
39///     diff_str.text().trim(),
40///     "Diff < left / right > :\n\
41///      <   Hello, world\n\
42///      >   Hello world!"
43/// );
44/// assert!(!diff_str.as_str().is_plain());
45/// # anyhow::Ok(())
46/// ```
47#[derive(Debug)]
48pub struct TextDiff<'a> {
49    lhs: &'a str,
50    rhs: &'a str,
51}
52
53impl<'a> TextDiff<'a> {
54    /// Computes difference between two strings.
55    pub const fn new(lhs: &'a str, rhs: &'a str) -> Self {
56        Self { lhs, rhs }
57    }
58}
59
60impl fmt::Display for TextDiff<'_> {
61    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62        use pretty_assertions::Comparison;
63
64        // Since `Comparison` uses `fmt::Debug`, we define this simple wrapper
65        // to switch to `fmt::Display`.
66        struct DebugStr<'a> {
67            s: &'a str,
68            padding: usize,
69        }
70
71        impl<'a> DebugStr<'a> {
72            fn new(s: &'a str, padding: usize) -> Self {
73                Self { s, padding }
74            }
75        }
76
77        impl fmt::Debug for DebugStr<'_> {
78            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79                if self.padding == 0 {
80                    formatter.write_str(self.s)
81                } else {
82                    for line in self.s.lines() {
83                        writeln!(formatter, "{:>padding$}{line}", "", padding = self.padding)?;
84                    }
85                    Ok(())
86                }
87            }
88        }
89
90        let padding = if matches!(formatter.align(), Some(fmt::Alignment::Right) | None) {
91            formatter.width().map_or(0, |width| width.saturating_sub(1))
92        } else {
93            0
94        };
95
96        write!(
97            formatter,
98            "{}",
99            Comparison::new(
100                &DebugStr::new(self.lhs, padding),
101                &DebugStr::new(self.rhs, padding)
102            )
103        )
104    }
105}
106
107/// Generic difference between two [`StyledStr`]s: either a difference in text, or in styling.
108///
109/// Produced by the [`StyledStr::diff()`] method.
110pub enum Diff<'a> {
111    /// There is a difference in text between the compared strings.
112    Text(TextDiff<'a>),
113    /// String texts match, but there is a difference in ANSI styles.
114    Style(StyleDiff<'a>),
115}
116
117impl fmt::Display for Diff<'_> {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::Text(diff) => write!(formatter, "styled strings differ by text\n{diff}"),
121            Self::Style(diff) => write!(
122                formatter,
123                "styled strings differ by style\n{diff}\n{diff:#}"
124            ),
125        }
126    }
127}
128
129// Delegates to `Display` to get better panic messages on `.diff(_).unwrap()`.
130impl fmt::Debug for Diff<'_> {
131    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132        fmt::Display::fmt(self, formatter)
133    }
134}
135
136#[cfg(feature = "std")]
137impl std::error::Error for Diff<'_> {}
138
139/// Stack-allocated version of [`Styled`] for use in compile-time parsing of rich styling strings.
140#[doc(hidden)]
141#[derive(Debug)]
142pub struct StackStyled<const TEXT_CAP: usize, const SPAN_CAP: usize> {
143    pub(crate) text: StackStr<TEXT_CAP>,
144    pub(crate) spans: Stack<StyledSpan, SPAN_CAP>,
145}
146
147impl<const TEXT_CAP: usize, const SPAN_CAP: usize> StackStyled<TEXT_CAP, SPAN_CAP> {
148    /// Instantiates a new instance from a `rich`-flavored string.
149    ///
150    /// # Panics
151    ///
152    /// Panics if the rich syntax is invalid.
153    #[track_caller]
154    pub const fn new(raw: &str) -> Self {
155        match Self::parse(raw) {
156            Ok(styled) => styled,
157            Err(err) => err.compile_panic(raw),
158        }
159    }
160
161    pub const fn as_ref(&'static self) -> StyledStr<'static> {
162        StyledStr {
163            text: self.text.as_str(),
164            spans: SpansSlice::new(self.spans.as_slice()),
165        }
166    }
167}