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 name = parts.next().unwrap();
75
76 if name.is_empty() {
77 return None;
78 }
79
80 let args: VecDeque<_> = parts.map(|s| unwrap_string_literals(&s)).collect();
81
82 if args.back().is_some_and(|s| s.is_empty()) {
84 return None;
85 }
86
87 if (matches!(tpe, CommandType::Neither) && args.is_empty()) {
89 return None;
90 }
91
92 Some(Self { name, args, tpe })
93 }
94}
95
96#[derive(Debug, Clone)]
97pub struct CommandMessage {
98 pub parallel: Vec<Vec<CommandExpr>>,
99 pub pure: bool,
101}
102
103impl fmt::Display for CommandMessage {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 for (i, group) in self.parallel.iter().enumerate() {
106 if i > 0 {
107 f.write_str(" | ")?;
108 }
109 for (j, command) in group.iter().enumerate() {
110 if j > 0 {
111 f.write_str(" ")?;
112 }
113 write!(f, "{command}")?;
114 }
115 }
116 Ok(())
117 }
118}
119
120impl CommandMessage {
121 pub fn parse(content: &str) -> Self {
122 let mut pure = true;
123 Self {
124 parallel: split_balanced(content.trim(), &['|', '/']) .into_iter()
126 .map(|group| {
127 split_balanced(&group, &[' ', ','])
128 .iter()
129 .filter_map(|s| {
130 let s = s.trim().trim_matches('\u{e0000}'); if s.is_empty() {
132 return None;
133 }
134 let parsed = CommandExpr::parse(s);
135 pure &= parsed.is_some();
136 parsed
137 })
138 .collect()
139 })
140 .filter(|g: &Vec<_>| !g.is_empty())
141 .collect::<Vec<_>>(),
142 pure,
143 }
144 }
145
146 pub fn is_empty(&self) -> bool {
147 self.parallel.iter().all(|seq| seq.is_empty())
148 }
149}
150
151fn unwrap_string_literals(input: &str) -> String {
152 let stripped = match input.strip_prefix('"') {
153 Some(tail) => tail.strip_suffix('"'),
154 None => input
155 .strip_prefix('{')
156 .and_then(|tail| tail.strip_suffix('}'))
157 .map(|s| s.trim()),
158 };
159 let Some(input) = stripped else {
160 return input.to_owned();
161 };
162
163 let mut result = String::with_capacity(input.len());
164 let mut chars = input.chars();
165 while let Some(ch) = chars.next() {
166 match ch {
167 '\\' => match chars.next() {
168 Some('n') => result.push('\n'),
169 Some('t') => result.push('\t'),
171 Some(ch) => result.push(ch),
172 _ => (),
173 },
174 ch => result.push(ch),
175 }
176 }
177 result
178}
179
180fn split_balanced(input: &str, seps: &[char]) -> Vec<String> {
182 let mut result = Vec::new();
183 let mut current = String::new();
184 let mut in_string = false;
185 let mut brace_depth = 0;
186 let mut paren_depth = 0;
187
188 let mut chars = input.chars();
189 while let Some(ch) = chars.next() {
190 if (in_string || brace_depth != 0) && ch == '\\' {
191 if let Some(ch) = chars.next() {
192 current.push('\\');
193 current.push(ch);
194 }
195 continue;
196 }
197 match ch {
198 '"' if brace_depth == 0 => in_string = !in_string,
199 '{' if !in_string => brace_depth += 1,
200 '}' if !in_string => brace_depth -= 1,
201 '(' if !in_string => paren_depth += 1,
202 ')' if !in_string => paren_depth -= 1,
203 _ => {}
204 }
205 if !in_string && brace_depth == 0 && paren_depth == 0 && seps.contains(&ch) {
206 result.push(std::mem::take(&mut current));
207 } else {
208 current.push(ch);
209 }
210 }
211 result.push(current);
212 result
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn parsing() {
221 let message = "Hello, this is left~ and right:.3~ and mouse:123:321~ | and then test~ test2~ and ~nope";
222 let parsed = CommandMessage::parse(message);
223
224 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test2~ nope~"#);
225 }
227
228 #[test]
229 fn cringing() {
230 let message = "Hello, this is +left and +right:.3 and +mouse:123:321 | and then +test +test2 and +wut~";
231 let parsed = CommandMessage::parse(message);
232
233 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test2~ +wut~"#);
234 }
235
236 #[test]
237 fn normieing() {
238 let message = "Hello, this is !left and !right:.3 and !mouse:123:321 | and then !test !test2 and !wut~";
239 let parsed = CommandMessage::parse(message);
240
241 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test2~ !wut~"#);
242 }
243
244 #[test]
245 fn neithering() {
246 let message =
247 "Hello, this is left and right:.3 and mouse:123:321 | and then test test2 and wut";
248 let parsed = CommandMessage::parse(message);
249
250 insta::assert_snapshot!(parsed, @r#"right:".3"~ mouse:123:321~"#);
251 }
252
253 #[test]
254 fn strings() {
255 let message = r#"print:"hello space"~ +hah:"and | pipe" | ~nope:123 | and-also-escapes:" \"incredible\", lol"~ "#;
256
257 let parsed = CommandMessage::parse(message);
258
259 insta::assert_snapshot!(parsed, @r#"print:"hello space"~ hah:"and | pipe"~ | nope:123~ | and-also-escapes:" \"incredible\", lol"~"#);
260 }
261
262 #[test]
263 fn braces() {
264 let message = r#"print:{ hello space }~ +hah:{ and | pipe } | ~nope:123 | and-also-escapes:{ { incredible }, lol }~ "#;
265
266 let parsed = CommandMessage::parse(message);
267
268 insta::assert_snapshot!(parsed, @r#"print:"hello space"~ hah:"and | pipe"~ | nope:123~ | and-also-escapes:"{ incredible }, lol"~"#);
269 }
270
271 #[test]
272 fn pure_cmd() {
273 assert!(CommandMessage::parse("U3s~ | w1s~ l600~").pure);
274 }
275
276 #[test]
277 fn seventv_spam_suffix() {
278 assert!(CommandMessage::parse("+lh ").pure);
279 }
280
281 #[test]
282 fn empty_arg() {
283 let parsed = CommandMessage::parse("command::second-arg~");
284
285 insta::assert_snapshot!(parsed, @"command::\"second-arg\"~");
286 }
287
288 #[test]
289 fn funky() {
290 let parsed = CommandMessage::parse(" test:some-text-with-{ braces and shit }-in-it ");
291
292 insta::assert_snapshot!(parsed, @r#"test:"some-text-with-{ braces and shit }-in-it"~"#);
293 }
294
295 #[test]
296 fn percent_full_arg() {
297 let parsed = CommandMessage::parse(" w:%(1:default) ");
298
299 insta::assert_snapshot!(parsed, @r#"w:"%(1:default)"~"#);
300 }
301}