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 || args.iter().any(|arg| arg.is_empty())
84 {
85 return None;
86 }
87
88 if let Some((_, prefix, number)) = lazy_regex::regex_captures!(r"^(.*?)(\d+s?)$", &name) {
89 args.push_front(number.to_owned());
90 name = prefix.to_owned();
91 }
92
93 Some(Self { name, args, tpe })
94 }
95}
96
97#[derive(Debug, Clone)]
98pub struct CommandMessage {
99 pub parallel: Vec<Vec<CommandExpr>>,
100 pub pure: bool,
102}
103
104impl fmt::Display for CommandMessage {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 for (i, group) in self.parallel.iter().enumerate() {
107 if i > 0 {
108 f.write_str(" | ")?;
109 }
110 for (j, command) in group.iter().enumerate() {
111 if j > 0 {
112 f.write_str(" ")?;
113 }
114 write!(f, "{command}")?;
115 }
116 }
117 Ok(())
118 }
119}
120
121impl CommandMessage {
122 pub fn parse(content: &str) -> Self {
123 let mut pure = true;
124 Self {
125 parallel: split_balanced(content.trim(), &['|', '/']) .into_iter()
127 .map(|group| {
128 split_balanced(&group, &[' '])
129 .iter()
130 .filter_map(|s| {
131 if s.trim().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 chars = input.chars();
187 while let Some(ch) = chars.next() {
188 if (in_string || brace_depth != 0) && ch == '\\' {
189 if let Some(ch) = chars.next() {
190 current.push('\\');
191 current.push(ch);
192 }
193 continue;
194 }
195 match ch {
196 '"' if brace_depth == 0 => in_string = !in_string,
197 '{' if !in_string => brace_depth += 1,
198 '}' if !in_string => brace_depth -= 1,
199 _ => {}
200 }
201 if !in_string && brace_depth == 0 && seps.contains(&ch) {
202 result.push(std::mem::take(&mut current));
203 } else {
204 current.push(ch);
205 }
206 }
207 result.push(current);
208 result
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn parsing() {
217 let message = "Hello, this is left~ and right:.3~ and mouse:123:321~ | and then test~ test2~ and ~nope";
218 let parsed = CommandMessage::parse(message);
219
220 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test:2~ nope~"#); }
222
223 #[test]
224 fn cringing() {
225 let message = "Hello, this is +left and +right:.3 and +mouse:123:321 | and then +test +test2 and +wut~";
226 let parsed = CommandMessage::parse(message);
227
228 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test:2~ +wut~"#);
229 }
230
231 #[test]
232 fn normieing() {
233 let message = "Hello, this is !left and !right:.3 and !mouse:123:321 | and then !test !test2 and !wut~";
234 let parsed = CommandMessage::parse(message);
235
236 insta::assert_snapshot!(parsed, @r#"left~ right:".3"~ mouse:123:321~ | test~ test:2~ !wut~"#);
237 }
238
239 #[test]
240 fn neithering() {
241 let message =
242 "Hello, this is left and right:.3 and mouse:123:321 | and then test test2 and wut";
243 let parsed = CommandMessage::parse(message);
244
245 insta::assert_snapshot!(parsed, @r#"right:".3"~ mouse:123:321~"#);
246 }
247
248 #[test]
249 fn strings() {
250 let message = r#"print:"hello space"~ +hah:"and | pipe" | ~nope:123 | and-also-escapes:" \"incredible\", lol"~ "#;
251
252 let parsed = CommandMessage::parse(message);
253
254 insta::assert_snapshot!(parsed, @r#"print:"hello space"~ hah:"and | pipe"~ | nope:123~ | and-also-escapes:" \"incredible\", lol"~"#);
255 }
256
257 #[test]
258 fn braces() {
259 let message = r#"print:{ hello space }~ +hah:{ and | pipe } | ~nope:123 | and-also-escapes:{ { incredible }, lol }~ "#;
260
261 let parsed = CommandMessage::parse(message);
262
263 insta::assert_snapshot!(parsed, @r#"print:"hello space"~ hah:"and | pipe"~ | nope:123~ | and-also-escapes:"{ incredible }, lol"~"#);
264 }
265
266 #[test]
267 fn number_arg() {
268 let message = r#"test123~ wait123s~ nope432h~"#;
269
270 let parsed = CommandMessage::parse(message);
271
272 insta::assert_snapshot!(parsed, @"test:123~ wait:123s~ nope432h~");
273 }
274
275 #[test]
276 fn pure_cmd() {
277 let message = r#"U3s~ | w1s~ l600~"#;
278
279 let parsed = CommandMessage::parse(message);
280
281 assert!(parsed.pure);
282 }
283}