Skip to main content

styled_str/types/
string.rs

1//! `StyledString` and its builder.
2
3use core::{fmt, mem, num::NonZeroUsize, ops};
4
5use anstyle::Style;
6
7use super::{StyledStr, slice::SpansSlice, spans::StyledSpan};
8use crate::{
9    AnsiError,
10    alloc::{String, Vec},
11    ansi_parser::AnsiParser,
12    utils::normalize_style,
13};
14
15/// Builder for [`StyledString`]s.
16///
17/// A builder can be initialized from scratch via [`StyledString::builder()`] or [`Default`].
18/// Alternatively, it can be initialized from an existing [`StyledString`].
19/// A builder can be extended by pushing [text](Self::push_text()), [styles](Self::push_style())
20/// or [`StyledStr`]ings into it.
21///
22/// # Examples
23///
24/// ```
25/// # use styled_str::{styled, StyledString};
26/// # use anstyle::{AnsiColor, Style};
27/// let mut builder = StyledString::builder();
28/// builder.push_style(AnsiColor::BrightGreen.on(AnsiColor::White).bold());
29/// builder.push_text("Hello");
30/// builder.push_text(",");
31/// // It's possible to use `+=` as syntactic sugar for `push_str()` / `push_style()`.
32/// builder += Style::new();
33/// builder.push_text(" world");
34/// builder += styled!("[[it, dim]]!");
35///
36/// let s = builder.build();
37/// assert_eq!(
38///     s.to_string(),
39///     "[[bold green! on white]]Hello,[[/]] world[[italic dim]]!"
40/// );
41/// ```
42#[derive(Debug, Clone, Default)]
43pub struct StyledStringBuilder {
44    inner: StyledString,
45    current_style: Style,
46}
47
48impl StyledStringBuilder {
49    /// Returns the current string text.
50    pub fn text(&self) -> &str {
51        self.inner.text()
52    }
53
54    /// Pushes unstyled text at the end of the string.
55    ///
56    /// # Panics
57    ///
58    /// Panics if `text` contains an ANSI escape char (`\x1b`).
59    pub fn push_text(&mut self, text: &str) {
60        assert!(
61            text.bytes().all(|ch| ch != 0x1b),
62            "Text contains 0x1b escape char"
63        );
64        self.inner.text.push_str(text);
65    }
66
67    pub(crate) fn current_style(&self) -> &Style {
68        &self.current_style
69    }
70
71    /// Pushes a style at the end of this string.
72    pub fn push_style(&mut self, style: Style) {
73        let style = normalize_style(style);
74        let spanned_text_len = self.inner.spans.last().map_or(0, StyledSpan::end);
75        let prev_style = mem::replace(&mut self.current_style, style);
76        if let Some(len) = NonZeroUsize::new(self.inner.text.len() - spanned_text_len) {
77            self.push_span(StyledSpan {
78                style: prev_style,
79                start: spanned_text_len,
80                len,
81            });
82        }
83    }
84
85    fn push_span(&mut self, span: StyledSpan) {
86        if let Some(last_span) = self.inner.spans.last_mut() {
87            if last_span.style == span.style {
88                last_span.extend_len(span.len.get());
89                return;
90            }
91        }
92        self.inner.spans.push(span);
93    }
94
95    /// Pushes a styled string at the end of this string.
96    pub fn push_str(&mut self, s: StyledStr<'_>) {
97        // Flush the current style so that `self.inner` is well-formed, which `StyledString::push_str()` relies upon.
98        self.push_style(self.current_style);
99        self.inner.push_str(s);
100
101        if let Some(last_span) = self.inner.spans.last() {
102            self.current_style = last_span.style;
103        }
104    }
105
106    /// Finalizes the [`StyledString`].
107    pub fn build(mut self) -> StyledString {
108        // Push the last style span covering the non-spanned text
109        self.push_style(Style::new());
110        self.inner
111    }
112}
113
114impl From<StyledString> for StyledStringBuilder {
115    fn from(string: StyledString) -> Self {
116        let current_style = string
117            .spans
118            .last()
119            .map_or_else(Style::new, |span| span.style);
120        Self {
121            inner: string,
122            current_style,
123        }
124    }
125}
126
127impl ops::AddAssign<Style> for StyledStringBuilder {
128    fn add_assign(&mut self, rhs: Style) {
129        self.push_style(rhs);
130    }
131}
132
133impl ops::AddAssign<StyledStr<'_>> for StyledStringBuilder {
134    fn add_assign(&mut self, rhs: StyledStr<'_>) {
135        self.push_str(rhs);
136    }
137}
138
139// We don't implement `AddAssign<&str>` for `StyledStringBuilder` to avoid ambiguity what the string represents.
140
141/// Heap-allocated styled string.
142///
143/// `StyledString` represents the owned string variant in contrast to [`StyledStr`], which is borrowed.
144/// Since [conversion](Self::as_str()) to a `StyledStr` is cheap, some immutable methods are accessible via [`StyledStr`]
145/// only (e.g., [iterating over style spans](StyledStr::spans()) or [splitting a string](StyledStr::split_at())).
146/// This allows to define these methods as `const fn`s and/or to propagate the borrowed data lifetime correctly.
147///
148/// A `StyledString` can be parsed from [rich syntax](crate#rich-syntax) via the [`FromStr`](core::str::FromStr) trait,
149/// from a string with ANSI escapes via [`StyledString::from_ansi()`], or manually constructed via [`StyledStringBuilder`].
150#[derive(Clone, Default, PartialEq)]
151pub struct StyledString<T = String> {
152    pub(crate) text: T,
153    pub(crate) spans: Vec<StyledSpan>,
154}
155
156impl<T> StyledString<T>
157where
158    T: ops::Deref<Target = str>,
159{
160    /// Returns the unstyled text behind this string.
161    pub fn into_text(self) -> T {
162        self.text
163    }
164
165    /// Borrows a [`StyledStr`] from this string. This can be used to call more complex methods.
166    pub fn as_str(&self) -> StyledStr<'_> {
167        StyledStr {
168            text: &self.text,
169            spans: SpansSlice::new(&self.spans),
170        }
171    }
172
173    /// Returns the unstyled text.
174    pub fn text(&self) -> &str {
175        &self.text
176    }
177}
178
179impl StyledString {
180    /// Creates a builder for styled strings.
181    pub fn builder() -> StyledStringBuilder {
182        StyledStringBuilder::default()
183    }
184
185    /// Parses a string from a string with embedded ANSI escape sequences.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the input is not a valid ANSI escaped string.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// # use styled_str::{styled, StyledString};
195    /// let str = StyledString::from_ansi(
196    ///     "\u{1b}[1;32mHello,\u{1b}[m world\u{1b}[3m!\u{1b}[m",
197    /// )?;
198    /// assert_eq!(str, styled!("[[bold green]]Hello,[[/]] world[[it]]!"));
199    /// # anyhow::Ok(())
200    /// ```
201    pub fn from_ansi(ansi_str: &str) -> Result<Self, AnsiError> {
202        AnsiParser::parse(ansi_str.as_bytes())
203    }
204
205    /// Parses a string from bytes with embedded ANSI escape sequences. This is similar to
206    /// [`Self::from_ansi()`], just using bytes as an input.
207    ///
208    /// # Errors
209    ///
210    /// Returns an error if the input is not a valid ANSI escaped string.
211    pub fn from_ansi_bytes(ansi_bytes: &[u8]) -> Result<Self, AnsiError> {
212        AnsiParser::parse(ansi_bytes)
213    }
214
215    /// Pushes another styled string at the end of this one.
216    ///
217    /// Use [`StyledStringBuilder`] for more complex string manipulations.
218    ///
219    /// # Examples
220    ///
221    /// ```
222    /// # use styled_str::{styled, StyledString};
223    /// let mut styled = styled!("[[bold green!]]Hello").to_owned();
224    /// styled.push_str(styled!("[[it]]!"));
225    /// // `push_str()` is also available via `+=` operator:
226    /// styled += styled!("[[it]]❤");
227    ///
228    /// assert_eq!(
229    ///     styled,
230    ///     styled!("[[bold green!]]Hello[[it]]!❤")
231    /// );
232    /// ```
233    pub fn push_str(&mut self, other: StyledStr<'_>) {
234        let mut copied_spans = other.spans.iter();
235        if let (Some(last), Some(next)) = (self.spans.last_mut(), other.spans.get(0)) {
236            if last.style == next.style {
237                last.extend_len(next.len.get());
238                copied_spans.next(); // skip copying the first span
239            }
240        }
241
242        // We need to offset the newly added spans, so that their start positions are correct.
243        let offset = self.text.len();
244        self.spans.extend(copied_spans.map(|mut span| {
245            span.start += offset;
246            span
247        }));
248
249        self.text.push_str(other.text);
250    }
251
252    /// Pops a single char from the end of the string.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// # use styled_str::{styled, StyledString};
258    /// # use anstyle::Style;
259    /// let mut styled = styled!("[[bold green!]]Hello[[it]]!❤").to_owned();
260    /// let (ch, style) = styled.pop().unwrap();
261    /// assert_eq!(ch, '❤');
262    /// assert_eq!(style, Style::new().italic());
263    /// assert_eq!(styled, styled!("[[bold green!]]Hello[[it]]!"));
264    ///
265    /// styled.pop().unwrap();
266    /// assert_eq!(styled, styled!("[[bold green!]]Hello"));
267    /// ```
268    #[allow(clippy::missing_panics_doc)] // internal checks; should never be triggered
269    pub fn pop(&mut self) -> Option<(char, Style)> {
270        let ch = self.text.pop()?;
271        let char_len = ch.len_utf8();
272
273        let last_span = self.spans.last_mut().unwrap();
274        assert!(last_span.len.get() >= char_len, "style span divides char");
275
276        let style = last_span.style;
277        if let Some(new_len) = NonZeroUsize::new(last_span.len.get() - char_len) {
278            last_span.len = new_len;
279        } else {
280            self.spans.pop();
281        }
282        Some((ch, style))
283    }
284}
285
286impl ops::AddAssign<StyledStr<'_>> for StyledString {
287    fn add_assign(&mut self, rhs: StyledStr<'_>) {
288        self.push_str(rhs);
289    }
290}
291
292impl<T> fmt::Debug for StyledString<T>
293where
294    T: ops::Deref<Target = str>,
295{
296    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
297        fmt::Debug::fmt(&self.as_str(), formatter)
298    }
299}
300
301impl<T> fmt::Display for StyledString<T>
302where
303    T: ops::Deref<Target = str>,
304{
305    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
306        fmt::Display::fmt(&self.as_str(), formatter)
307    }
308}
309
310impl<'a> FromIterator<StyledStr<'a>> for StyledString {
311    fn from_iter<I: IntoIterator<Item = StyledStr<'a>>>(iter: I) -> Self {
312        iter.into_iter()
313            .fold(StyledString::default(), |mut acc, str| {
314                acc.push_str(str);
315                acc
316            })
317    }
318}
319
320impl<'a> Extend<StyledStr<'a>> for StyledString {
321    fn extend<I: IntoIterator<Item = StyledStr<'a>>>(&mut self, iter: I) {
322        for str in iter {
323            self.push_str(str);
324        }
325    }
326}