1use std::{
2 collections::VecDeque,
3 fmt::{self, Write},
4};
5
6#[derive(Debug, Clone, Copy)]
7pub enum CommandType {
8 Uwu,
9 Tilde,
10 Plus,
11 Normie,
12 Hash,
13 Huh,
14 Neither,
15}
16
17impl CommandType {
18 pub fn write_command(&self, f: &mut fmt::Formatter<'_>, command: &str) -> fmt::Result {
19 if matches!(self, Self::Uwu) {
20 f.write_str(command)?;
21 return f.write_char('~');
22 }
23 match self {
24 Self::Tilde => f.write_char('~')?,
25 Self::Plus => f.write_char('+')?,
26 Self::Normie => f.write_char('!')?,
27 Self::Hash => f.write_char('#')?,
28 Self::Huh => f.write_char('?')?,
29 _ => {}
30 };
31 f.write_str(command)
32 }
33}
34
35#[derive(Debug, Clone)]
36pub struct CommandToken {
37 pub tpe: CommandType,
38}
39
40#[derive(Debug, Clone)]
41pub struct CommandExpr {
42 pub name: String,
43 pub args: VecDeque<String>,
44 pub tpe: CommandType,
45}
46
47impl fmt::Display for CommandExpr {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(&self.name)?;
50 for arg in &self.args {
51 f.write_str(":")?;
52 if arg.chars().all(|ch| ch.is_alphanumeric()) {
53 f.write_str(arg)?;
54 } else {
55 write!(f, "{arg:?}")?;
56 }
57 }
58 f.write_str("~")?;
59 Ok(())
60 }
61}
62
63impl CommandExpr {
64 pub fn parse(word: &str) -> Option<Self> {
65 let (word, tpe) = (word.strip_suffix('~').map(|w| (w, CommandType::Uwu)))
66 .or(word.strip_prefix('~').map(|w| (w, CommandType::Tilde)))
67 .or(word.strip_prefix('+').map(|w| (w, CommandType::Plus)))
68 .or(word.strip_prefix('!').map(|w| (w, CommandType::Normie)))
69 .or(word.strip_prefix('#').map(|w| (w, CommandType::Hash)))
70 .or(word.strip_prefix('?').map(|w| (w, CommandType::Huh)))
71 .unwrap_or((word, CommandType::Neither));
72
73 let mut parts = split_balanced(word, &[':']).into_iter();
74 let mut name = parts.next().unwrap();
75
76 if name.is_empty() || name.starts_with('-') || name.ends_with('-') {
77 return None;
78 }
79
80 let mut args: VecDeque<_> = parts.map(|s| unwrap_string_literals(&s)).collect();
81
82 if (matches!(tpe, CommandType::Neither) && args.is_empty()) {
83 return None;
84 }
85
86 if let Some((_, prefix, number)) = lazy_regex::regex_captures!(r"^(.*?)(\d+s?)$", &name) {
87 args.push_front(number.to_owned());
88 name = prefix.to_owned();
89 }
90
91 Some(Self { name, args, tpe })
92 }
93}
94
95#[derive(Debug, Clone)]
96pub struct CommandMessage {
97 pub parallel: Vec<Vec<CommandExpr>>,
98 pub pure: bool,
100}
101
102impl fmt::Display for CommandMessage {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 for (i, group) in self.parallel.iter().enumerate() {
105 if i > 0 {
106 f.write_str(" | ")?;
107 }
108 for (j, command) in group.iter().enumerate() {
109 if j > 0 {
110 f.write_str(" ")?;
111 }
112 write!(f, "{command}")?;
113 }
114 }
115 Ok(())
116 }
117}
118
119impl CommandMessage {
120 pub fn parse(content: &str) -> Self {
121 let mut pure = true;
122 Self {
123 parallel: split_balanced(content.trim(), &['|', '/']) .into_iter()
125 .map(|group| {
126 split_balanced(&group, &[' ', ','])
127 .iter()
128 .filter_map(|s| {
129 let s = s.trim().trim_matches('\u{e0000}'); if s.is_empty() {
131 return None;
132 }
133 let parsed = CommandExpr::parse(s);
134 pure &= parsed.is_some();
135 parsed
136 })
137 .collect()
138 })
139 .filter(|g: &Vec<_>| !g.is_empty())
140 .collect::<Vec<_>>(),
141 pure,
142 }
143 }
144
145 pub fn is_empty(&self) -> bool {
146 self.parallel.iter().all(|seq| seq.is_empty())
147 }
148}
149
150fn unwrap_string_literals(input: &str) -> String {
151 let stripped = match input.strip_prefix('"') {
152 Some(tail) => tail.strip_suffix('"'),
153 None => input
154 .strip_prefix('{')
155 .and_then(|tail| tail.strip_suffix('}'))
156 .map(|s| s.trim()),
157 };
158 let Some(input) = stripped else {
159 return input.to_owned();
160 };
161
162 let mut result = String::with_capacity(input.len());
163 let mut chars = input.chars();
164 while let Some(ch) = chars.next() {
165 match ch {
166 '\\' => match chars.next() {
167 Some('n') => result.push('\n'),
168 Some('t') => result.push('\t'),
170 Some(ch) => result.push(ch),
171 _ => (),
172 },
173 ch => result.push(ch),
174 }
175 }
176 result
177}
178
179fn split_balanced(input: &str, seps: &[char]) -> Vec<String> {
181 let mut result = Vec::new();
182 let mut current = String::new();
183 let mut in_string = false;
184 let mut brace_depth = 0;
185 let mut chars = input.chars();
186 while let Some(ch) = chars.next() {
187 if (in_string || brace_depth != 0) && ch == '\\' {
188 if let Some(ch) = chars.next() {
189 current.push('\\');
190 current.push(ch);
191 }
192 continue;
193 }
194 match ch {
195 '"' if brace_depth == 0 => in_string = !in_string,
196 '{' if !in_string => brace_depth += 1,
197 '}' if !in_string => brace_depth -= 1,
198 _ => {}
199 }
200 if !in_string && brace_depth == 0 && seps.contains(&ch) {
201 result.push(std::mem::take(&mut current));
202 } else {
203 current.push(ch);
204 }
205 }
206 result.push(current);
207 result
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn parsing() {
216 let message = "Hello, this is left~ and right:.3~ and mouse:123:321~ | and then test~ test2~ and ~nope";
217 let parsed = CommandMessage::parse(message);
218
219 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test:2~ nope~"#); }
221
222 #[test]
223 fn cringing() {
224 let message = "Hello, this is +left and +right:.3 and +mouse:123:321 | and then +test +test2 and +wut~";
225 let parsed = CommandMessage::parse(message);
226
227 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test:2~ +wut~"#);
228 }
229
230 #[test]
231 fn normieing() {
232 let message = "Hello, this is !left and !right:.3 and !mouse:123:321 | and then !test !test2 and !wut~";
233 let parsed = CommandMessage::parse(message);
234
235 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test:2~ !wut~"#);
236 }
237
238 #[test]
239 fn neithering() {
240 let message =
241 "Hello, this is left and right:.3 and mouse:123:321 | and then test test2 and wut";
242 let parsed = CommandMessage::parse(message);
243
244 insta::assert_snapshot!(parsed, @r#"right:".3"~ mouse:123:321~"#);
245 }
246
247 #[test]
248 fn strings() {
249 let message = r#"print:"hello space"~ +hah:"and | pipe" | ~nope:123 | and-also-escapes:" \"incredible\", lol"~ "#;
250
251 let parsed = CommandMessage::parse(message);
252
253 insta::assert_snapshot!(parsed, @r#"print:"hello space"~ hah:"and | pipe"~ | nope:123~ | and-also-escapes:" \"incredible\", lol"~"#);
254 }
255
256 #[test]
257 fn braces() {
258 let message = r#"print:{ hello space }~ +hah:{ and | pipe } | ~nope:123 | and-also-escapes:{ { incredible }, lol }~ "#;
259
260 let parsed = CommandMessage::parse(message);
261
262 insta::assert_snapshot!(parsed, @r#"print:"hello space"~ hah:"and | pipe"~ | nope:123~ | and-also-escapes:"{ incredible }, lol"~"#);
263 }
264
265 #[test]
266 fn number_arg() {
267 let message = r#"test123~ wait123s~ nope432h~"#;
268
269 let parsed = CommandMessage::parse(message);
270
271 insta::assert_snapshot!(parsed, @"test:123~ wait:123s~ nope432h~");
272 }
273
274 #[test]
275 fn pure_cmd() {
276 assert!(CommandMessage::parse("U3s~ | w1s~ l600~").pure);
277 }
278
279 #[test]
280 fn seventv_spam_suffix() {
281 assert!(CommandMessage::parse("+lh ").pure);
282 }
283
284 #[test]
285 fn empty_arg() {
286 let parsed = CommandMessage::parse("command::second-arg~");
287
288 insta::assert_snapshot!(parsed, @"command::\"second-arg\"~");
289 }
290}