#![warn(missing_docs)]
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, Input};
use std::io::Write;
use thiserror::Error;
mod builder;
mod dict;
pub use builder::*;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Uwu {
pub lowercase: bool,
pub expressions: bool,
pub w_replace: bool,
pub stutter: bool,
pub stutter_chance: u8,
pub emojis: bool,
pub emojis_chance: u8,
}
impl Default for Uwu {
fn default() -> Self {
Self {
lowercase: true,
expressions: true,
w_replace: true,
stutter: true,
stutter_chance: 4,
emojis: true,
emojis_chance: 1,
}
}
}
impl Uwu {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> UwuBuilder {
UwuBuilder::new()
}
pub fn uwuify<S: AsRef<str>>(&self, input: S) -> Result<String, UwuError> {
let mut input = input.as_ref().to_owned();
if self.lowercase {
input = input.to_ascii_lowercase();
}
let mut buf = input.into_bytes();
buf.insert(0, b' ');
buf.push(b' ');
if self.expressions {
buf = Self::do_expressions(buf)?;
}
if self.w_replace {
buf = Self::do_w_replace(buf)?;
}
if self.stutter {
buf = self.do_stutter(buf)?;
}
if self.emojis {
buf = self.do_emojis(buf)?;
}
if let Some(last) = buf.last() {
if *last == b' ' {
buf.pop();
}
}
if let Some(first) = buf.first() {
if *first == b' ' {
buf.remove(0);
}
}
let output = String::from_utf8_lossy(&buf).to_string();
Ok(output)
}
fn do_expressions(input: Vec<u8>) -> Result<Vec<u8>, UwuError> {
let mut buf = Vec::with_capacity(input.len());
let matcher = AhoCorasick::new(dict::EXPRESSIONS)?;
matcher.try_stream_replace_all(
input.as_slice(),
&mut buf,
dict::EXPRESSIONS_REPLACE.as_slice(),
)?;
Ok(buf)
}
fn do_w_replace(mut input: Vec<u8>) -> Result<Vec<u8>, UwuError> {
input.iter_mut().for_each(|byte| {
if matches!(byte, b'l' | b'r') {
*byte = b'w';
}
});
Ok(input)
}
fn do_stutter(&self, input: Vec<u8>) -> Result<Vec<u8>, UwuError> {
if input.len() < 2 {
return Ok(input);
}
let mut buf: Vec<u8> = Vec::with_capacity(input.len());
let mut rng = Uwu::create_rng();
let mut prev_idx = 0;
for mut idx in 0..input.len() - 1 {
if input[idx] == b' '
&& input[idx + 1].is_ascii_alphabetic()
&& rng.u8(0..self.stutter_chance) == 0
{
idx += 1;
let section = &input[prev_idx..idx];
let ch = input[idx];
buf.write_all(section)?;
buf.write_all(&[ch])?;
buf.write_all(&[b'-'])?;
prev_idx = idx;
}
}
buf.write_all(&input[prev_idx..])?;
Ok(buf)
}
fn do_emojis(&self, input: Vec<u8>) -> Result<Vec<u8>, UwuError> {
let matcher = AhoCorasickBuilder::new().build(dict::PUNCTUATION)?;
let matches = matcher
.try_find_iter(Input::new(&input))?
.map(|mat| mat.end())
.collect::<Vec<usize>>();
if matches.is_empty() {
return Ok(input);
}
let mut buf = Vec::with_capacity(input.len());
let mut rng = Uwu::create_rng();
let mut prev_idx = 0;
for idx in matches {
if rng.u8(0..self.emojis_chance) != 0 {
continue;
}
let section = &input[prev_idx..idx];
let emoji = rng.choice(dict::EMOJIS).unwrap_or("uwu");
buf.write_all(section)?;
buf.write_all(emoji.as_bytes())?;
prev_idx = idx;
}
buf.write_all(&input[prev_idx..])?;
Ok(buf)
}
fn create_rng() -> fastrand::Rng {
let seed = 75777521; fastrand::Rng::with_seed(seed)
}
}
#[derive(Error, Debug)]
pub enum UwuError {
#[error("string matcher build error: {0}")]
StringMatcherBuild(#[from] aho_corasick::BuildError),
#[error("string matcher match error: {0}")]
StringMatcherMatch(#[from] aho_corasick::MatchError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error(transparent)]
Unknown(#[from] Box<dyn std::error::Error + Send>),
}
pub fn uwuify<S: AsRef<str>>(input: S) -> Result<String, UwuError> {
let uwu = Uwu::new();
uwu.uwuify(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assert_uwuify() {
fn convert(input: &str) -> String {
Uwu::new().uwuify(input).unwrap()
}
assert_eq!(convert("very nice"), "vewy nyice");
assert_eq!(convert("very elegant solution"), "vewy ewegant s-sowution");
assert_eq!(convert("master! suki!"), "mastew! o.O suki! ^•ﻌ•^");
assert_eq!(
convert("my master, I have a question"),
"my mastew, o.O i-i have a-a qwuestion"
);
assert_eq!(
convert("The quick brown fox jumps over the lazy dog"),
"the qwuick b-bwown fox j-jumps ovew the wazy dog"
);
}
#[test]
fn assert_rng() {
fn calc_avg<F>(mut func: F, rounds: usize) -> f64
where
F: FnMut(&mut fastrand::Rng) -> bool,
{
let mut rng = fastrand::Rng::with_seed(75777521); let mut positives = 0;
for _ in 0..rounds {
if func(&mut rng) {
positives += 1;
}
}
positives as f64 / rounds as f64
}
assert_eq!(calc_avg(|rng| rng.bool(), 1000), 0.499);
assert_eq!(calc_avg(|rng| rng.u8(0..1) == 0, 1000), 1.0);
assert_eq!(calc_avg(|rng| rng.u8(0..2) == 0, 1000), 0.492);
assert_eq!(calc_avg(|rng| rng.u8(0..3) == 0, 1000), 0.319);
assert_eq!(calc_avg(|rng| rng.u8(0..4) == 0, 1000), 0.231);
assert_eq!(calc_avg(|rng| rng.u8(0..5) == 0, 1000), 0.186); }
}