pikadick/commands/uwuify.rs
1use crate::checks::ENABLED_CHECK;
2use rand::prelude::SliceRandom;
3use serenity::{
4 client::Context,
5 framework::standard::{
6 macros::command,
7 Args,
8 CommandResult,
9 },
10 model::channel::Message,
11};
12
13const FACES: &[&str] = &["(・`ω´・)", ";;w;;", "owo", "UwU", ">w<", "^w^"];
14
15#[command]
16#[description("UwUify as phrase")]
17#[usage("\"<phrase>\"")]
18#[example("\"Hello World!\"")]
19#[min_args(1)]
20#[max_args(1)]
21#[checks(Enabled)]
22#[bucket("default")]
23pub async fn uwuify(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
24 let phrase = args.single_quoted::<String>()?;
25 msg.channel_id.say(&ctx.http, uwuify_str(&phrase)).await?;
26 Ok(())
27}
28
29/// A rust-optimized version of:
30/// ```javascript
31/// /// Taken from: https://honk.moe/tools/owo.html
32/// var faces = ["(・`ω´・)", ";;w;;", "owo", "UwU", ">w<", "^w^"];
33/// function OwoifyText(){
34/// v = document.getElementById("textarea").value
35/// v = v.replace(/(?:r|l)/g, "w");
36/// v = v.replace(/(?:R|L)/g, "W");
37/// v = v.replace(/n([aeiou])/g, 'ny$1');
38/// v = v.replace(/N([aeiou])/g, 'Ny$1');
39/// v = v.replace(/N([AEIOU])/g, 'Ny$1');
40/// v = v.replace(/ove/g, "uv");
41/// v = v.replace(/\!+/g, " " + faces[Math.floor(Math.random() * faces.length)] + " ");
42/// document.getElementById("textarea").value = v
43/// };
44/// ```
45/// This version doesn't use regexes and completes the uwufication in 1 iteration with a lookahead buffer of 2 elements.
46/// NOTE: It may be buggy due to its complexity and discrepancies with the js version should be reported on the issue tracker.
47pub fn uwuify_str(input: &str) -> String {
48 let mut iter = input.chars().peekable();
49 let mut buf = None;
50 let mut output = String::with_capacity(input.len());
51
52 // Buf has 1 cap so it must be empty in each match arm since we try to fetch a value from it here.
53 // We can then treat peek/next as the first value from here on.
54 while let Some(c) = buf.take().or_else(|| iter.next()) {
55 match c {
56 'r' | 'l' => {
57 output.push('w');
58 }
59 'R' | 'L' => {
60 output.push('W');
61 }
62 'n' => {
63 if let Some(c) = iter.peek().copied() {
64 if matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') {
65 let c = iter.next().unwrap();
66
67 output.reserve(3);
68 output.push_str("ny");
69 output.push(c);
70 } else {
71 output.push('n');
72 }
73 } else {
74 output.push('n');
75 }
76 }
77 'N' => {
78 if let Some(c) = iter.peek().copied().map(|c| c.to_ascii_lowercase()) {
79 if matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') {
80 let c = iter.next().unwrap();
81
82 output.reserve(3);
83 output.push_str("Ny");
84 output.push(c);
85 } else {
86 output.push('N');
87 }
88 } else {
89 output.push('N');
90 }
91 }
92 'o' => {
93 if let Some(c) = iter.peek().copied() {
94 if c == 'v' {
95 let _ = iter.next().unwrap();
96
97 if let Some(c) = iter.peek().copied() {
98 if c == 'e' {
99 let _ = iter.next().unwrap();
100 output.push_str("ove");
101 } else {
102 output.push('o');
103 buf = Some('v');
104 // e is still in iter peek buffer
105 }
106 } else {
107 output.push('o');
108 buf = Some('v');
109 }
110 } else {
111 output.push('o');
112 // v is in iter peek buffer
113 }
114 } else {
115 output.push('o');
116 }
117 }
118 '!' => {
119 while let Some('!') = iter.next() {
120 // Consume all '!'
121 // TODO: Some variants add a face per '!',
122 // it might make sense to add a feature that does that here
123 }
124
125 let face = FACES.choose(&mut rand::thread_rng()).expect("Valid Face");
126 output.reserve(face.len() + 2);
127
128 output.push(' ');
129 output.push_str(face);
130 output.push(' ');
131 }
132 c => output.push(c),
133 }
134 }
135
136 output
137}