1use std::borrow::Cow;
3
4enum ParserState {
5 Text,
6 ColorCode,
7 Foreground1(char),
8 Foreground2,
9 Comma,
10 Background1(char),
11}
12struct Parser {
13 state: ParserState,
14}
15
16pub trait FormattedStringExt<'a> {
18 fn is_formatted(&self) -> bool;
20
21 fn strip_formatting(self) -> Cow<'a, str>;
23}
24
25const FORMAT_CHARACTERS: &[char] = &[
26 '\x02', '\x1F', '\x16', '\x0F', '\x03', ];
32
33impl<'a> FormattedStringExt<'a> for &'a str {
34 fn is_formatted(&self) -> bool {
35 self.contains(FORMAT_CHARACTERS)
36 }
37
38 fn strip_formatting(self) -> Cow<'a, str> {
39 if !self.is_formatted() {
40 return Cow::Borrowed(self);
41 }
42 let mut s = String::from(self);
43 strip_formatting(&mut s);
44 Cow::Owned(s)
45 }
46}
47
48fn strip_formatting(buf: &mut String) {
49 let mut parser = Parser::new();
50 buf.retain(|cur| parser.next(cur));
51}
52
53impl Parser {
54 fn new() -> Self {
55 Parser {
56 state: ParserState::Text,
57 }
58 }
59
60 fn next(&mut self, cur: char) -> bool {
61 use self::ParserState::*;
62 match self.state {
63 Text | Foreground1(_) | Foreground2 if cur == '\x03' => {
64 self.state = ColorCode;
65 false
66 }
67 Text => !FORMAT_CHARACTERS.contains(&cur),
68 ColorCode if cur.is_ascii_digit() => {
69 self.state = Foreground1(cur);
70 false
71 }
72 Foreground1('0') if cur.is_ascii_digit() => {
73 self.state = Foreground2;
75 false
76 }
77 Foreground1('1') if cur.is_digit(6) => {
78 self.state = Foreground2;
80 false
81 }
82 Foreground1(_) if cur.is_digit(6) => {
83 self.state = Text;
84 true
85 }
86 Foreground1(_) if cur == ',' => {
87 self.state = Comma;
88 false
89 }
90 Foreground2 if cur == ',' => {
91 self.state = Comma;
92 false
93 }
94 Comma if (cur.is_ascii_digit()) => {
95 self.state = Background1(cur);
96 false
97 }
98 Background1(prev) if cur.is_digit(6) => {
99 self.state = Text;
101 prev != '1'
102 }
103 _ => {
104 self.state = Text;
105 !FORMAT_CHARACTERS.contains(&cur)
106 }
107 }
108 }
109}
110
111impl FormattedStringExt<'static> for String {
112 fn is_formatted(&self) -> bool {
113 self.as_str().is_formatted()
114 }
115 fn strip_formatting(mut self) -> Cow<'static, str> {
116 if !self.is_formatted() {
117 return Cow::Owned(self);
118 }
119 strip_formatting(&mut self);
120 Cow::Owned(self)
121 }
122}
123
124#[cfg(test)]
125mod test {
126 use crate::colors::FormattedStringExt;
127 use std::borrow::Cow;
128
129 macro_rules! test_formatted_string_ext {
130 { $( $name:ident ( $($line:tt)* ), )* } => {
131 $(
132 mod $name {
133 use super::*;
134 test_formatted_string_ext!(@ $($line)*);
135 }
136 )*
137 };
138 (@ $text:expr, should stripped into $expected:expr) => {
139 #[test]
140 fn test_formatted() {
141 assert!($text.is_formatted());
142 }
143 #[test]
144 fn test_strip() {
145 assert_eq!($text.strip_formatting(), $expected);
146 }
147 };
148 (@ $text:expr, is not formatted) => {
149 #[test]
150 fn test_formatted() {
151 assert!(!$text.is_formatted());
152 }
153 #[test]
154 fn test_strip() {
155 assert_eq!($text.strip_formatting(), $text);
156 }
157 }
158 }
159
160 test_formatted_string_ext! {
161 blank("", is not formatted),
162 blank2(" ", is not formatted),
163 blank3("\t\r\n", is not formatted),
164 bold("l\x02ol", should stripped into "lol"),
165 bold_from_string(String::from("l\x02ol"), should stripped into "lol"),
166 bold_hangul("우왕\x02굳", should stripped into "우왕굳"),
167 fg_color("l\x033ol", should stripped into "lol"),
168 fg_color2("l\x0312ol", should stripped into "lol"),
169 fg_color3("l\x0302ol", should stripped into "lol"),
170 fg_color4("l\x030ol", should stripped into "lol"),
171 fg_color5("l\x0309ol", should stripped into "lol"),
172 fg_bg_11("l\x031,2ol", should stripped into "lol"),
173 fg_bg_21("l\x0312,3ol", should stripped into "lol"),
174 fg_bg_12("l\x031,12ol", should stripped into "lol"),
175 fg_bg_22("l\x0312,13ol", should stripped into "lol"),
176 fg_bold("l\x0309\x02ol", should stripped into "lol"),
177 string_with_multiple_colors("hoo\x034r\x033a\x0312y", should stripped into "hooray"),
178 string_with_digit_after_color("\x0344\x0355\x0366", should stripped into "456"),
179 string_with_multiple_2digit_colors("hoo\x0310r\x0311a\x0312y", should stripped into "hooray"),
180 string_with_digit_after_2digit_color("\x031212\x031111\x031010", should stripped into "121110"),
181 thinking("🤔...", is not formatted),
182 unformatted("a plain text", is not formatted),
183 }
184
185 #[test]
186 fn test_strip_no_allocation_for_unformatted_text() {
187 if let Cow::Borrowed(formatted) = "plain text".strip_formatting() {
188 assert_eq!(formatted, "plain text");
189 } else {
190 panic!("allocation detected");
191 }
192 }
193}