irc_async/proto/
colors.rs1use 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_digit(10) => {
69 self.state = Foreground1(cur);
70 false
71 }
72 Foreground1('1') if cur.is_digit(6) => {
73 self.state = Foreground2;
75 false
76 }
77 Foreground1(_) if cur.is_digit(6) => {
78 self.state = Text;
79 true
80 }
81 Foreground1(_) if cur == ',' => {
82 self.state = Comma;
83 false
84 }
85 Foreground2 if cur == ',' => {
86 self.state = Comma;
87 false
88 }
89 Comma if (cur.is_digit(10)) => {
90 self.state = Background1(cur);
91 false
92 }
93 Background1(prev) if cur.is_digit(6) => {
94 self.state = Text;
96 prev != '1'
97 }
98 _ => {
99 self.state = Text;
100 true
101 }
102 }
103 }
104}
105
106impl FormattedStringExt<'static> for String {
107 fn is_formatted(&self) -> bool {
108 self.as_str().is_formatted()
109 }
110 fn strip_formatting(mut self) -> Cow<'static, str> {
111 if !self.is_formatted() {
112 return Cow::Owned(self);
113 }
114 strip_formatting(&mut self);
115 Cow::Owned(self)
116 }
117}
118
119#[cfg(test)]
120mod test {
121 use crate::proto::colors::FormattedStringExt;
122 use std::borrow::Cow;
123
124 macro_rules! test_formatted_string_ext {
125 { $( $name:ident ( $($line:tt)* ), )* } => {
126 $(
127 mod $name {
128 use super::*;
129 test_formatted_string_ext!(@ $($line)*);
130 }
131 )*
132 };
133 (@ $text:expr, should stripped into $expected:expr) => {
134 #[test]
135 fn test_formatted() {
136 assert!($text.is_formatted());
137 }
138 #[test]
139 fn test_strip() {
140 assert_eq!($text.strip_formatting(), $expected);
141 }
142 };
143 (@ $text:expr, is not formatted) => {
144 #[test]
145 fn test_formatted() {
146 assert!(!$text.is_formatted());
147 }
148 #[test]
149 fn test_strip() {
150 assert_eq!($text.strip_formatting(), $text);
151 }
152 }
153 }
154
155 test_formatted_string_ext! {
156 blank("", is not formatted),
157 blank2(" ", is not formatted),
158 blank3("\t\r\n", is not formatted),
159 bold("l\x02ol", should stripped into "lol"),
160 bold_from_string(String::from("l\x02ol"), should stripped into "lol"),
161 bold_hangul("우왕\x02굳", should stripped into "우왕굳"),
162 fg_color("l\x033ol", should stripped into "lol"),
163 fg_color2("l\x0312ol", should stripped into "lol"),
164 fg_bg_11("l\x031,2ol", should stripped into "lol"),
165 fg_bg_21("l\x0312,3ol", should stripped into "lol"),
166 fg_bg_12("l\x031,12ol", should stripped into "lol"),
167 fg_bg_22("l\x0312,13ol", should stripped into "lol"),
168 string_with_multiple_colors("hoo\x034r\x033a\x0312y", should stripped into "hooray"),
169 string_with_digit_after_color("\x0344\x0355\x0366", should stripped into "456"),
170 string_with_multiple_2digit_colors("hoo\x0310r\x0311a\x0312y", should stripped into "hooray"),
171 string_with_digit_after_2digit_color("\x031212\x031111\x031010", should stripped into "121110"),
172 thinking("🤔...", is not formatted),
173 unformatted("a plain text", is not formatted),
174 }
175
176 #[test]
177 fn test_strip_no_allocation_for_unformatted_text() {
178 if let Cow::Borrowed(formatted) = "plain text".strip_formatting() {
179 assert_eq!(formatted, "plain text");
180 } else {
181 panic!("allocation detected");
182 }
183 }
184}