1use core::{fmt, num::NonZeroUsize, ops, str::FromStr};
4
5use anstyle::{Ansi256Color, AnsiColor, Color, Effects, RgbColor, Style};
6
7use crate::{
8 HexColorError, ParseError, ParseErrorKind, StackStyled, StyledString,
9 alloc::{Cow, String, Vec},
10 types::{StyledSpan, StyledStr},
11 utils::{Stack, StackStr, StrCursor, is_same_style, normalize_style},
12};
13
14#[cfg(test)]
15mod tests;
16
17pub const fn parse_hex_color(hex: &[u8]) -> Result<RgbColor, HexColorError> {
36 if hex.is_empty() || hex[0] != b'#' {
37 return Err(HexColorError::NoHash);
38 }
39
40 if hex.len() == 4 {
41 let r = const_try!(parse_hex_digit(hex[1]));
42 let g = const_try!(parse_hex_digit(hex[2]));
43 let b = const_try!(parse_hex_digit(hex[3]));
44 Ok(RgbColor(r * 17, g * 17, b * 17))
45 } else if hex.len() == 7 {
46 let r = const_try!(parse_hex_digit(hex[1])) * 16 + const_try!(parse_hex_digit(hex[2]));
47 let g = const_try!(parse_hex_digit(hex[3])) * 16 + const_try!(parse_hex_digit(hex[4]));
48 let b = const_try!(parse_hex_digit(hex[5])) * 16 + const_try!(parse_hex_digit(hex[6]));
49 Ok(RgbColor(r, g, b))
50 } else {
51 Err(HexColorError::InvalidLen)
52 }
53}
54
55const fn parse_hex_digit(ch: u8) -> Result<u8, HexColorError> {
56 match ch {
57 b'0'..=b'9' => Ok(ch - b'0'),
58 b'a'..=b'f' => Ok(ch - b'a' + 10),
59 b'A'..=b'F' => Ok(ch - b'A' + 10),
60 _ => Err(HexColorError::InvalidHexDigit),
61 }
62}
63
64pub fn rgb_color_to_hex(RgbColor(r, g, b): RgbColor) -> String {
78 use core::fmt::Write as _;
79
80 let mut buffer = String::new();
81 if r % 17 == 0 && g % 17 == 0 && b % 17 == 0 {
82 write!(&mut buffer, "#{:x}{:x}{:x}", r / 17, g / 17, b / 17).unwrap();
83 } else {
84 write!(&mut buffer, "#{r:02x}{g:02x}{b:02x}").unwrap();
85 }
86 buffer
87}
88
89impl StyledStr<'static> {
90 #[doc(hidden)] pub const fn capacities(raw: &str) -> (usize, usize) {
92 let mut cursor = StrCursor::new(raw);
93 let mut text_len = 0;
94 let mut span_count = 0;
95 let mut style_end_pos = 0;
96 while !cursor.is_eof() {
97 if cursor.gobble("[[") {
98 while !cursor.is_eof() && cursor.current_byte() == b'[' {
99 cursor.advance_byte();
100 }
101
102 let end_pos = cursor.pos() - 2;
103 if end_pos - style_end_pos > 0 {
104 span_count += 1;
105 text_len += end_pos - style_end_pos;
106 }
107 while !cursor.is_eof() && !cursor.gobble("]]") {
108 cursor.advance_byte();
109 }
110 style_end_pos = cursor.pos();
111 } else {
112 cursor.advance_byte();
113 }
114 }
115
116 if raw.len() - style_end_pos > 0 {
117 span_count += 1;
118 text_len += raw.len() - style_end_pos;
119 }
120 (text_len, span_count)
121 }
122}
123
124#[derive(Debug)]
125struct SetStyleFields(u16);
126
127impl SetStyleFields {
128 const BOLD: u16 = 1;
129 const ITALIC: u16 = 2;
130 const UNDERLINE: u16 = 4;
131 const STRIKETHROUGH: u16 = 8;
132 const BLINK: u16 = 16;
133 const INVERT: u16 = 32;
134 const HIDDEN: u16 = 64;
135 const DIMMED: u16 = 128;
136
137 const FG: u16 = 256;
138 const BG: u16 = 512;
139
140 const fn new() -> Self {
141 Self(0)
142 }
143
144 const fn from_style(style: &Style) -> Self {
145 let mut this = Self::new();
146 this.set_effects(style.get_effects());
147 if style.get_fg_color().is_some() {
148 this.set_fg();
149 }
150 if style.get_bg_color().is_some() {
151 this.set_bg();
152 }
153 this
154 }
155
156 const fn set_effects(&mut self, effects: Effects) -> bool {
157 let prev = self.0;
158
159 if effects.contains(Effects::BOLD) {
160 self.0 |= Self::BOLD;
161 }
162 if effects.contains(Effects::ITALIC) {
163 self.0 |= Self::ITALIC;
164 }
165 if effects.contains(Effects::UNDERLINE) {
166 self.0 |= Self::UNDERLINE;
167 }
168 if effects.contains(Effects::STRIKETHROUGH) {
169 self.0 |= Self::STRIKETHROUGH;
170 }
171 if effects.contains(Effects::BLINK) {
172 self.0 |= Self::BLINK;
173 }
174 if effects.contains(Effects::INVERT) {
175 self.0 |= Self::INVERT;
176 }
177 if effects.contains(Effects::HIDDEN) {
178 self.0 |= Self::HIDDEN;
179 }
180 if effects.contains(Effects::DIMMED) {
181 self.0 |= Self::DIMMED;
182 }
183
184 self.0 != prev
185 }
186
187 const fn set_fg(&mut self) -> bool {
188 let prev = self.0;
189 self.0 |= Self::FG;
190 self.0 != prev
191 }
192
193 const fn set_bg(&mut self) -> bool {
194 let prev = self.0;
195 self.0 |= Self::BG;
196 self.0 != prev
197 }
198}
199
200#[derive(Debug)]
202struct RichParser<'a> {
203 cursor: StrCursor<'a>,
204 current_style: Style,
205 style_end_pos: usize,
206}
207
208impl<'a> RichParser<'a> {
209 const fn new(raw: &'a str) -> Self {
210 Self {
211 cursor: StrCursor::new(raw),
212 current_style: Style::new(),
213 style_end_pos: 0,
214 }
215 }
216
217 const fn step(&mut self) -> Result<ops::ControlFlow<(), StyledSpan>, ParseError> {
218 if self.cursor.is_eof() {
219 return Ok(self.final_step());
221 }
222
223 while !self.cursor.is_eof() {
224 if self.cursor.gobble("[[") {
225 while !self.cursor.is_eof() && self.cursor.current_byte() == b'[' {
228 self.cursor.advance_byte();
229 }
230 if self.cursor.is_eof() {
231 return Err(self.cursor.error(ParseErrorKind::UnfinishedStyle));
232 }
233
234 let end_pos = self.cursor.pos() - 2;
236 let span = if let Some(len) = NonZeroUsize::new(end_pos - self.style_end_pos) {
237 Some(StyledSpan {
238 style: self.current_style,
239 start: self.style_end_pos,
240 len,
241 })
242 } else {
243 None
244 };
245
246 self.current_style = const_try!(self.cursor.parse_style(&self.current_style, true));
247 self.style_end_pos = self.cursor.pos();
248
249 if let Some(span) = span {
250 return Ok(ops::ControlFlow::Continue(span));
251 }
252 } else {
253 let ch = self.cursor.current_byte();
254 if ch == 0x1b {
255 return Err(self.cursor.error(ParseErrorKind::EscapeInText));
256 }
257 self.cursor.advance_byte();
258 }
259 }
260
261 Ok(self.final_step())
262 }
263
264 const fn final_step(&mut self) -> ops::ControlFlow<(), StyledSpan> {
265 if let Some(len) = NonZeroUsize::new(self.cursor.pos() - self.style_end_pos) {
266 let span = StyledSpan {
267 style: self.current_style,
268 start: self.style_end_pos,
269 len,
270 };
271 self.style_end_pos = self.cursor.pos();
272 ops::ControlFlow::Continue(span)
273 } else {
274 ops::ControlFlow::Break(())
275 }
276 }
277}
278
279const fn strip_prefix<'s>(haystack: &'s [u8], prefix: &[u8]) -> Option<&'s [u8]> {
280 if haystack.len() < prefix.len() {
281 return None;
282 }
283 let mut i = 0;
284 while i < prefix.len() {
285 if haystack[i] != prefix[i] {
286 return None;
287 }
288 i += 1;
289 }
290 let (_, suffix) = haystack.split_at(prefix.len());
291 Some(suffix)
292}
293
294impl StrCursor<'_> {
295 const fn error(&self, kind: ParseErrorKind) -> ParseError {
297 let range = if self.is_eof() {
298 self.pos()..self.pos()
299 } else {
300 self.expand_to_char_boundaries(self.pos()..self.pos() + 1)
301 };
302 kind.with_pos(range)
303 }
304
305 const fn error_on_empty_token(&self, unfinished: ParseErrorKind) -> ParseError {
306 if self.is_eof() {
307 unfinished.with_pos(self.pos()..self.pos())
308 } else {
309 let next_char_pos = self.expand_to_char_boundaries(self.pos()..self.pos() + 1);
310 ParseErrorKind::BogusDelimiter.with_pos(next_char_pos)
311 }
312 }
313
314 const fn parse_style(
316 &mut self,
317 current_style: &Style,
318 expect_closing_bracket: bool,
319 ) -> Result<Style, ParseError> {
320 let mut style = Style::new();
321 let mut is_initial = true;
322 let mut closing_bracket = false;
323 let mut copied_fields = None;
324 let mut set_fields = SetStyleFields::new();
325 let mut clear_pos = None::<ops::Range<usize>>;
326
327 while !self.is_eof() {
328 self.gobble_whitespace();
329 if !is_initial && self.gobble_punct() {
330 self.gobble_whitespace();
331 }
332
333 if expect_closing_bracket && self.gobble("]]") {
334 closing_bracket = true;
335 break;
336 }
337 if self.is_eof() {
338 break;
339 }
340
341 if let Some(clear_pos) = clear_pos {
342 return Err(ParseErrorKind::NonIsolatedClear.with_pos(clear_pos));
343 }
344
345 let token_range = self.take_token();
346 let token = self.range(&token_range);
347 if matches!(token, b"*") {
348 if !is_initial {
349 return Err(ParseErrorKind::NonInitialCopy.with_pos(token_range));
350 }
351 copied_fields = Some(SetStyleFields::from_style(current_style));
352 style = *current_style;
353 } else if matches!(token, b"/") {
354 if !is_initial {
355 return Err(ParseErrorKind::NonIsolatedClear.with_pos(token_range));
356 }
357 clear_pos = Some(token_range);
358 } else {
359 const_try!(self.parse_token(
360 &mut style,
361 token_range,
362 &mut set_fields,
363 copied_fields.as_mut(),
364 ));
365 }
366
367 is_initial = false;
368 }
369
370 if expect_closing_bracket && !closing_bracket {
371 Err(self.error(ParseErrorKind::UnfinishedStyle))
372 } else {
373 Ok(normalize_style(style))
374 }
375 }
376
377 const fn parse_token(
378 &mut self,
379 style: &mut Style,
380 token_range: ops::Range<usize>,
381 set_fields: &mut SetStyleFields,
382 copied_fields: Option<&mut SetStyleFields>,
383 ) -> Result<(), ParseError> {
384 let token = self.range(&token_range);
385 match token {
386 b"" => {
387 return Err(self.error_on_empty_token(ParseErrorKind::UnfinishedStyle));
388 }
389
390 b"on" => {
391 if !set_fields.set_bg() {
392 return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
393 }
394
395 self.gobble_whitespace();
396 if self.is_eof() {
397 return Err(self.error(ParseErrorKind::UnfinishedStyle));
398 }
399
400 let on_token_range = token_range;
401 let token_range = self.take_token();
402 let token = self.range(&token_range);
403 let color = match Self::parse_color(token) {
404 Ok(Some(color)) => color,
405 Ok(None) => {
406 return Err(ParseErrorKind::UnfinishedBackground.with_pos(on_token_range));
407 }
408 Err(err) => return Err(err.with_pos(token_range)),
409 };
410 *style = style.bg_color(Some(color));
411 }
412
413 b"-color" | b"-fg" | b"!color" | b"!fg" => {
414 if !set_fields.set_fg() {
415 return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
416 }
417 let Some(copied_fields) = copied_fields else {
418 return Err(ParseErrorKind::NegationWithoutCopy.with_pos(token_range));
421 };
422 if copied_fields.set_fg() {
423 return Err(ParseErrorKind::RedundantNegation.with_pos(token_range));
425 }
426 *style = style.fg_color(None);
427 }
428 b"-on" | b"-bg" | b"!on" | b"!bg" => {
429 if !set_fields.set_bg() {
430 return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
431 }
432 let Some(copied_fields) = copied_fields else {
433 return Err(ParseErrorKind::NegationWithoutCopy.with_pos(token_range));
436 };
437 if copied_fields.set_bg() {
438 return Err(ParseErrorKind::RedundantNegation.with_pos(token_range));
440 }
441 *style = style.bg_color(None);
442 }
443 neg if neg[0] == b'-' || neg[0] == b'!' => {
444 let (_, token_without_prefix) = token.split_at(1);
445 let Some(effects) = Self::parse_effects(token_without_prefix) else {
446 return Err(ParseErrorKind::UnsupportedEffect.with_pos(token_range));
447 };
448 let Some(copied_fields) = copied_fields else {
449 return Err(ParseErrorKind::NegationWithoutCopy.with_pos(token_range));
452 };
453
454 if !set_fields.set_effects(effects) {
455 return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
456 }
457 if copied_fields.set_effects(effects) {
458 return Err(ParseErrorKind::RedundantNegation.with_pos(token_range));
460 }
461
462 *style = style.effects(style.get_effects().remove(effects));
463 }
464
465 _ => {
466 if let Some(effects) = Self::parse_effects(token) {
467 if !set_fields.set_effects(effects) {
468 return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
469 }
470 *style = style.effects(style.get_effects().insert(effects));
471 } else {
472 let color = match Self::parse_color(token) {
473 Ok(Some(color)) => color,
474 Ok(None) => {
475 return Err(ParseErrorKind::UnsupportedStyle.with_pos(token_range));
476 }
477 Err(err) => return Err(err.with_pos(token_range)),
478 };
479 if !set_fields.set_fg() {
480 return Err(ParseErrorKind::DuplicateSpecifier.with_pos(token_range));
481 }
482 *style = style.fg_color(Some(color));
483 }
484 }
485 }
486 Ok(())
487 }
488
489 const fn parse_effects(token: &[u8]) -> Option<Effects> {
490 Some(match token {
491 b"bold" | b"b" => Effects::BOLD,
492 b"italic" | b"i" | b"it" => Effects::ITALIC,
493 b"underline" | b"u" | b"ul" => Effects::UNDERLINE,
494 b"strikethrough" | b"strike" | b"s" => Effects::STRIKETHROUGH,
495 b"dim" | b"dimmed" => Effects::DIMMED,
496 b"invert" | b"inverted" | b"inv" => Effects::INVERT,
497 b"blink" => Effects::BLINK,
498 b"hide" | b"hidden" | b"conceal" | b"concealed" => Effects::HIDDEN,
499 _ => return None,
500 })
501 }
502
503 const fn parse_index(s: &[u8]) -> Result<u8, ParseErrorKind> {
504 if s.is_empty() || s.len() > 3 || s[0] == b'0' {
505 return Err(ParseErrorKind::InvalidIndexColor);
507 }
508
509 let mut i = 0;
510 let mut index = 0_u8;
511 while i < s.len() {
512 let digit = if s[i].is_ascii_digit() {
513 s[i] - b'0'
514 } else {
515 return Err(ParseErrorKind::InvalidIndexColor);
516 };
517 index = match index.checked_mul(10) {
518 Some(val) => val,
519 None => return Err(ParseErrorKind::InvalidIndexColor),
520 };
521 index = match index.checked_add(digit) {
522 Some(val) => val,
523 None => return Err(ParseErrorKind::InvalidIndexColor),
524 };
525 i += 1;
526 }
527 Ok(index)
528 }
529
530 const fn parse_color(token: &[u8]) -> Result<Option<Color>, ParseErrorKind> {
531 Ok(match token {
532 b"black" => Some(Color::Ansi(AnsiColor::Black)),
533 b"black!" | b"bright-black" => Some(Color::Ansi(AnsiColor::BrightBlack)),
534 b"red" => Some(Color::Ansi(AnsiColor::Red)),
535 b"red!" | b"bright-red" => Some(Color::Ansi(AnsiColor::BrightRed)),
536 b"green" => Some(Color::Ansi(AnsiColor::Green)),
537 b"green!" | b"bright-green" => Some(Color::Ansi(AnsiColor::BrightGreen)),
538 b"yellow" => Some(Color::Ansi(AnsiColor::Yellow)),
539 b"yellow!" | b"bright-yellow" => Some(Color::Ansi(AnsiColor::BrightYellow)),
540 b"blue" => Some(Color::Ansi(AnsiColor::Blue)),
541 b"blue!" | b"bright-blue" => Some(Color::Ansi(AnsiColor::BrightBlue)),
542 b"magenta" => Some(Color::Ansi(AnsiColor::Magenta)),
543 b"magenta!" | b"bright-magenta" => Some(Color::Ansi(AnsiColor::BrightMagenta)),
544 b"cyan" => Some(Color::Ansi(AnsiColor::Cyan)),
545 b"cyan!" | b"bright-cyan" => Some(Color::Ansi(AnsiColor::BrightCyan)),
546 b"white" => Some(Color::Ansi(AnsiColor::White)),
547 b"white!" | b"bright-white" => Some(Color::Ansi(AnsiColor::BrightWhite)),
548
549 hex if !hex.is_empty() && hex[0] == b'#' => match parse_hex_color(hex) {
550 Ok(color) => Some(Color::Rgb(color)),
551 Err(err) => return Err(ParseErrorKind::HexColor(err)),
552 },
553
554 _ => {
555 let Some(mut color_idx) = strip_prefix(token, b"color") else {
556 return Ok(None);
557 };
558 if color_idx.is_empty() {
559 return Err(ParseErrorKind::UnfinishedColor);
560 }
561
562 if color_idx[0] == b'(' {
564 if !matches!(color_idx.last(), Some(b')')) {
565 return Err(ParseErrorKind::UnfinishedColor);
566 }
567 (_, color_idx) = color_idx.split_at(1);
568 (color_idx, _) = color_idx.split_at(color_idx.len() - 1);
569 }
570
571 let index = const_try!(Self::parse_index(color_idx));
572 Some(Color::Ansi256(Ansi256Color(index)))
573 }
574 })
575 }
576
577 const fn gobble_whitespace(&mut self) {
578 while !self.is_eof() {
579 let ch = self.current_byte();
580 if ch.is_ascii_whitespace() {
581 self.advance_byte();
582 } else {
583 break;
584 }
585 }
586 }
587
588 const fn gobble_punct(&mut self) -> bool {
589 if !self.is_eof() && matches!(self.current_byte(), b',' | b';') {
590 self.advance_byte();
591 true
592 } else {
593 false
594 }
595 }
596
597 const fn take_token(&mut self) -> ops::Range<usize> {
598 const fn is_delimiter(ch: u8) -> bool {
599 ch.is_ascii_whitespace() || matches!(ch, b',' | b';' | b']')
600 }
601
602 let start_pos = self.pos();
603 while !self.is_eof() && !is_delimiter(self.current_byte()) {
604 self.advance_byte();
605 }
606 start_pos..self.pos()
607 }
608}
609
610impl<const TEXT_CAP: usize, const SPAN_CAP: usize> StackStyled<TEXT_CAP, SPAN_CAP> {
611 pub(crate) const fn parse(raw: &str) -> Result<Self, ParseError> {
612 let mut parser = RichParser::new(raw);
613 let mut text = StackStr::new();
614 let mut spans = Stack::new(StyledSpan::DUMMY);
615
616 while let ops::ControlFlow::Continue(mut span) = const_try!(parser.step()) {
617 let mut span_extended = false;
618 let plaintext_start = if let Some(last_span) = spans.last_mut() {
619 if is_same_style(&last_span.style, &span.style) {
620 last_span.extend_len(span.len.get());
621 span_extended = true;
622 }
623 last_span.end()
624 } else {
625 0
626 };
627
628 let text_range = span.start..span.end();
629 span.start = plaintext_start;
632
633 if !span_extended && spans.push(span).is_err() {
634 return Err(parser.cursor.error(ParseErrorKind::SpanOverflow));
635 }
636
637 let text_chunk = parser.cursor.range(&text_range);
638 let mut i = 0;
639 while i < text_chunk.len() {
640 if text.push(text_chunk[i]).is_err() {
641 return Err(parser.cursor.error(ParseErrorKind::TextOverflow));
642 }
643 i += 1;
644 }
645 }
646 Ok(Self { text, spans })
647 }
648}
649
650impl FromStr for StyledString {
651 type Err = ParseError;
652
653 fn from_str(raw: &str) -> Result<Self, Self::Err> {
654 let mut parser = RichParser::new(raw);
655 let mut builder = Self::builder();
656
657 while let ops::ControlFlow::Continue(span) = parser.step()? {
658 builder.push_style(span.style);
659 builder.push_text(&raw[span.start..span.end()]);
660 }
661 Ok(builder.build())
662 }
663}
664
665#[derive(Debug, Clone, Copy)]
683pub struct RichStyle<'a>(pub &'a Style);
684
685impl RichStyle<'_> {
686 pub const fn parse(raw: &str, current_style: &Style) -> Result<Style, ParseError> {
690 StrCursor::new(raw).parse_style(current_style, false)
691 }
692
693 pub(crate) fn tokens(self) -> Vec<Cow<'static, str>> {
694 let mut tokens = Vec::new();
695
696 let effects = self.0.get_effects();
697 if effects.contains(Effects::BOLD) {
698 tokens.push("bold".into());
699 }
700 if effects.contains(Effects::ITALIC) {
701 tokens.push("italic".into());
702 }
703 if effects.contains(Effects::DIMMED) {
704 tokens.push("dim".into());
705 }
706 if effects.contains(Effects::UNDERLINE) {
707 tokens.push("underline".into());
708 }
709 if effects.contains(Effects::STRIKETHROUGH) {
710 tokens.push("strike".into());
711 }
712 if effects.contains(Effects::INVERT) {
713 tokens.push("invert".into());
714 }
715 if effects.contains(Effects::BLINK) {
716 tokens.push("blink".into());
717 }
718 if effects.contains(Effects::HIDDEN) {
719 tokens.push("hidden".into());
720 }
721
722 if let Some(color) = self.0.get_fg_color() {
723 tokens.push(write_color(color).into());
724 }
725
726 if let Some(color) = self.0.get_bg_color() {
727 tokens.push("on".into());
728 tokens.push(write_color(color).into());
729 }
730 tokens
731 }
732}
733
734impl fmt::Display for RichStyle<'_> {
735 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
736 let mut tokens = self.tokens();
737 if tokens.is_empty() {
738 tokens.push("/".into());
739 }
740
741 for (i, token) in tokens.iter().enumerate() {
742 write!(formatter, "{token}")?;
743 if i + 1 < tokens.len() {
744 formatter.write_str(" ")?;
745 }
746 }
747 Ok(())
748 }
749}
750
751fn write_color(color: Color) -> String {
752 use core::fmt::Write as _;
753
754 let mut buffer = String::new();
755 match color {
756 Color::Ansi(color) => write!(
757 &mut buffer,
758 "{base}{bright}",
759 base = ansi_color_str(color),
760 bright = if color.is_bright() { "!" } else { "" }
761 )
762 .unwrap(),
763 Color::Ansi256(Ansi256Color(idx)) => write!(&mut buffer, "color{idx}").unwrap(),
764 Color::Rgb(color) => {
765 buffer = rgb_color_to_hex(color);
766 }
767 }
768 buffer
769}
770
771fn ansi_color_str(color: AnsiColor) -> &'static str {
772 match color.bright(false) {
773 AnsiColor::Black => "black",
774 AnsiColor::Red => "red",
775 AnsiColor::Green => "green",
776 AnsiColor::Yellow => "yellow",
777 AnsiColor::Blue => "blue",
778 AnsiColor::Magenta => "magenta",
779 AnsiColor::Cyan => "cyan",
780 AnsiColor::White => "white",
781 _ => unreachable!(),
782 }
783}
784
785#[derive(Debug)]
788pub(crate) struct EscapedText<'a> {
789 inner: &'a str,
790 escape_chars: bool,
792}
793
794impl<'a> EscapedText<'a> {
795 pub(crate) fn new(inner: &'a str, escape_chars: bool) -> Self {
796 Self {
797 inner,
798 escape_chars,
799 }
800 }
801}
802
803impl fmt::Display for EscapedText<'_> {
804 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
805 let mut remainder = self.inner;
806 while let Some(mut pos) = remainder.find("[[") {
807 pos += 2;
809 while remainder.as_bytes().get(pos).copied() == Some(b'[') {
810 pos += 1;
811 }
812
813 let head;
814 (head, remainder) = remainder.split_at(pos);
815 let escaped;
816 write!(
817 formatter,
818 "{}[[*]]",
819 if self.escape_chars {
820 escaped = head.escape_debug();
821 &escaped as &dyn fmt::Display
822 } else {
823 &head
824 }
825 )?;
826 }
827
828 let escaped;
829 write!(
830 formatter,
831 "{}",
832 if self.escape_chars {
833 escaped = remainder.escape_debug();
834 &escaped as &dyn fmt::Display
835 } else {
836 &remainder
837 }
838 )
839 }
840}