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}