1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use crate::checks::ENABLED_CHECK;
use rand::prelude::SliceRandom;
use serenity::{
    client::Context,
    framework::standard::{
        macros::command,
        Args,
        CommandResult,
    },
    model::channel::Message,
};

const FACES: &[&str] = &["(・`ω´・)", ";;w;;", "owo", "UwU", ">w<", "^w^"];

#[command]
#[description("UwUify as phrase")]
#[usage("\"<phrase>\"")]
#[example("\"Hello World!\"")]
#[min_args(1)]
#[max_args(1)]
#[checks(Enabled)]
#[bucket("default")]
pub async fn uwuify(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
    let phrase = args.single_quoted::<String>()?;
    msg.channel_id.say(&ctx.http, uwuify_str(&phrase)).await?;
    Ok(())
}

/// A rust-optimized version of:
/// ```javascript
/// /// Taken from: https://honk.moe/tools/owo.html
/// var faces = ["(・`ω´・)", ";;w;;", "owo", "UwU", ">w<", "^w^"];
/// function OwoifyText(){
///     v = document.getElementById("textarea").value
///     v = v.replace(/(?:r|l)/g, "w");
///     v = v.replace(/(?:R|L)/g, "W");
///     v = v.replace(/n([aeiou])/g, 'ny$1');
///     v = v.replace(/N([aeiou])/g, 'Ny$1');
///     v = v.replace(/N([AEIOU])/g, 'Ny$1');
///     v = v.replace(/ove/g, "uv");
///     v = v.replace(/\!+/g, " " + faces[Math.floor(Math.random() * faces.length)] + " ");
///     document.getElementById("textarea").value = v
///  };
/// ```
/// This version doesn't use regexes and completes the uwufication in 1 iteration with a lookahead buffer of 2 elements.
/// NOTE: It may be buggy due to its complexity and discrepancies with the js version should be reported on the issue tracker.
pub fn uwuify_str(input: &str) -> String {
    let mut iter = input.chars().peekable();
    let mut buf = None;
    let mut output = String::with_capacity(input.len());

    // Buf has 1 cap so it must be empty in each match arm since we try to fetch a value from it here.
    // We can then treat peek/next as the first value from here on.
    while let Some(c) = buf.take().or_else(|| iter.next()) {
        match c {
            'r' | 'l' => {
                output.push('w');
            }
            'R' | 'L' => {
                output.push('W');
            }
            'n' => {
                if let Some(c) = iter.peek().copied() {
                    if matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') {
                        let c = iter.next().unwrap();

                        output.reserve(3);
                        output.push_str("ny");
                        output.push(c);
                    } else {
                        output.push('n');
                    }
                } else {
                    output.push('n');
                }
            }
            'N' => {
                if let Some(c) = iter.peek().copied().map(|c| c.to_ascii_lowercase()) {
                    if matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') {
                        let c = iter.next().unwrap();

                        output.reserve(3);
                        output.push_str("Ny");
                        output.push(c);
                    } else {
                        output.push('N');
                    }
                } else {
                    output.push('N');
                }
            }
            'o' => {
                if let Some(c) = iter.peek().copied() {
                    if c == 'v' {
                        let _ = iter.next().unwrap();

                        if let Some(c) = iter.peek().copied() {
                            if c == 'e' {
                                let _ = iter.next().unwrap();
                                output.push_str("ove");
                            } else {
                                output.push('o');
                                buf = Some('v');
                                // e is still in iter peek buffer
                            }
                        } else {
                            output.push('o');
                            buf = Some('v');
                        }
                    } else {
                        output.push('o');
                        // v is in iter peek buffer
                    }
                } else {
                    output.push('o');
                }
            }
            '!' => {
                while let Some('!') = iter.next() {
                    // Consume all '!'
                    // TODO: Some variants add a face per '!',
                    // it might make sense to add a feature that does that here
                }

                let face = FACES.choose(&mut rand::thread_rng()).expect("Valid Face");
                output.reserve(face.len() + 2);

                output.push(' ');
                output.push_str(face);
                output.push(' ');
            }
            c => output.push(c),
        }
    }

    output
}