From beb16ab9cf038ccaab97bfaa5b25db859e3bdf8f Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Fri, 12 Apr 2024 05:35:18 +0900
Subject: [PATCH] refactor (backend): port nyaify to backend-rs

---
 Cargo.lock                                    |  5 +-
 Cargo.toml                                    |  5 +-
 packages/backend-rs/Cargo.toml                |  1 +
 packages/backend-rs/index.js                  |  3 +-
 packages/backend-rs/src/lib.rs                |  1 +
 packages/backend-rs/src/misc/mod.rs           |  1 +
 packages/backend-rs/src/misc/nyaify.rs        | 96 +++++++++++++++++++
 packages/backend/src/misc/nyaify.ts           | 33 -------
 .../backend/src/models/repositories/note.ts   |  2 +-
 9 files changed, 108 insertions(+), 39 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/mod.rs
 create mode 100644 packages/backend-rs/src/misc/nyaify.rs
 delete mode 100644 packages/backend/src/misc/nyaify.ts

diff --git a/Cargo.lock b/Cargo.lock
index 4d33664b5f..3b7e70c186 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -162,9 +162,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.79"
+version = "0.1.80"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681"
+checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -204,6 +204,7 @@ dependencies = [
  "parse-display",
  "pretty_assertions",
  "rand",
+ "regex",
  "schemars",
  "sea-orm",
  "serde",
diff --git a/Cargo.toml b/Cargo.toml
index 2c208fe845..993fa06924 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,7 @@ napi = { version = "2.16.2", default-features = false }
 napi-derive = "2.16.2"
 napi-build = "2.1.2"
 
-async-trait = "0.1.79"
+async-trait = "0.1.80"
 basen = "0.1.0"
 cfg-if = "1.0.0"
 chrono = "0.4.37"
@@ -20,8 +20,9 @@ once_cell = "1.19.0"
 parse-display = "0.9.0"
 pretty_assertions = "1.4.0"
 proc-macro2 = "1.0.79"
-quote = "1.0.35"
+quote = "1.0.36"
 rand = "0.8.5"
+regex = "1.10.4"
 schemars = "0.8.16"
 sea-orm = "0.12.15"
 serde = "1.0.197"
diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml
index 12762ec101..48bdebc470 100644
--- a/packages/backend-rs/Cargo.toml
+++ b/packages/backend-rs/Cargo.toml
@@ -26,6 +26,7 @@ jsonschema = { workspace = true }
 once_cell = { workspace = true }
 parse-display = { workspace = true }
 rand = { workspace = true }
+regex = { workspace = true }
 schemars = { workspace = true, features = ["chrono"] }
 sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
 serde = { workspace = true, features = ["derive"] }
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 723a6a7e48..7b66fdb6de 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,8 +310,9 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr, IdConvertType, convertId } = nativeBinding
+const { nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr, IdConvertType, convertId } = nativeBinding
 
+module.exports.nyaify = nyaify
 module.exports.AntennaSrcEnum = AntennaSrcEnum
 module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
 module.exports.NoteVisibilityEnum = NoteVisibilityEnum
diff --git a/packages/backend-rs/src/lib.rs b/packages/backend-rs/src/lib.rs
index fcb2323380..06a09e64ac 100644
--- a/packages/backend-rs/src/lib.rs
+++ b/packages/backend-rs/src/lib.rs
@@ -2,6 +2,7 @@ pub use macro_rs::napi as export;
 
 pub mod database;
 pub mod macros;
+pub mod misc;
 pub mod model;
 pub mod util;
 
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
new file mode 100644
index 0000000000..6c5d7c4f2e
--- /dev/null
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -0,0 +1 @@
+pub mod nyaify;
diff --git a/packages/backend-rs/src/misc/nyaify.rs b/packages/backend-rs/src/misc/nyaify.rs
new file mode 100644
index 0000000000..9ce25b8b4a
--- /dev/null
+++ b/packages/backend-rs/src/misc/nyaify.rs
@@ -0,0 +1,96 @@
+use once_cell::sync::Lazy;
+use regex::{Captures, Regex};
+
+#[cfg_attr(feature = "napi", crate::export)]
+pub fn nyaify(text: &str, lang: Option<&str>) -> String {
+    let mut to_return = text.to_owned();
+
+    {
+        static RE: Lazy<Regex> =
+            Lazy::new(|| Regex::new(r"(?i-u)(non)([bcdfghjklmnpqrstvwxyz])").unwrap());
+        to_return = RE
+            .replace_all(&to_return, |caps: &Captures<'_>| {
+                format!(
+                    "{}{}",
+                    match &caps[1] {
+                        "non" => "nyan",
+                        "Non" => "Nyan",
+                        "NON" => "NYAN",
+                        _ => &caps[1],
+                    },
+                    &caps[2]
+                )
+            })
+            .to_string();
+    }
+
+    {
+        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"다([..。…??!!\s]|$)").unwrap());
+        to_return = RE.replace_all(&to_return, r"다냥$1").to_string();
+    }
+
+    {
+        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"야([??\s]|$)").unwrap());
+        to_return = RE.replace_all(&to_return, r"냥$1").to_string();
+    }
+
+    {
+        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"([나-낳])").unwrap());
+        to_return = RE
+            .replace_all(&to_return, |caps: &Captures<'_>| {
+                format!(
+                    "{}",
+                    char::from_u32(
+                        caps[0].chars().next().unwrap() as u32 + 56 /* = '냐' - '나' */
+                    )
+                    .unwrap()
+                )
+            })
+            .to_string();
+    }
+
+    if lang.is_some() && lang.unwrap().starts_with("zh") {
+        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[妙庙描渺瞄秒苗藐廟]").unwrap());
+        to_return = RE.replace_all(&to_return, "喵").to_string();
+    }
+
+    let simple_rules = [
+        ("な", "にゃ"),
+        ("ナ", "ニャ"),
+        ("ナ", "ニャ"),
+        ("na", "nya"),
+        ("NA", "NYA"),
+        ("Na", "Nya"),
+        ("morning", "mornyan"),
+        ("Morning", "Mornyan"),
+        ("MORNING", "MORNYAN"),
+        ("everyone", "everynyan"),
+        ("Everyone", "Everynyan"),
+        ("EVERYONE", "EVERYNYAN"),
+        ("να", "νια"),
+        ("ΝΑ", "ΝΙΑ"),
+        ("Να", "Νια"),
+    ];
+
+    simple_rules.into_iter().for_each(|(from, to)| {
+        to_return = to_return.replace(from, to);
+    });
+
+    to_return
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::nyaify;
+
+    #[test]
+    fn can_nyaify() {
+        assert_eq!(nyaify("Hello everyone!", Some("en")), "Hello everynyan!");
+        assert_eq!(nyaify("Nonbinary people", None), "Nyanbinyary people");
+        assert_eq!(nyaify("1分鐘是60秒", Some("zh-TW")), "1分鐘是60喵");
+        assert_eq!(nyaify("1分間は60秒です", Some("ja-JP")), "1分間は60秒です");
+        assert_eq!(nyaify("あなたは誰ですか", None), "あにゃたは誰ですか");
+        assert_eq!(nyaify("Ναυτικός", Some("el-GR")), "Νιαυτικός");
+        assert_eq!(nyaify("일어나다", None), "일어냐다냥");
+    }
+}
diff --git a/packages/backend/src/misc/nyaify.ts b/packages/backend/src/misc/nyaify.ts
deleted file mode 100644
index 5829461779..0000000000
--- a/packages/backend/src/misc/nyaify.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export function nyaify(text: string, lang?: string): string {
-	text = text
-		// ja-JP
-		.replaceAll("な", "にゃ")
-		.replaceAll("ナ", "ニャ")
-		.replaceAll("ナ", "ニャ")
-		// en-US
-		.replaceAll("na", "nya")
-		.replaceAll("Na", "Nya")
-		.replaceAll("NA", "NYA")
-		.replace(/(?<=morn)ing/gi, (x) => (x === "ING" ? "YAN" : "yan"))
-		.replace(/(?<=every)one/gi, (x) => (x === "ONE" ? "NYAN" : "nyan"))
-		.replace(/non(?=[bcdfghjklmnpqrstvwxyz])/gi, (x) =>
-			x === "NON" ? "NYAN" : "nyan",
-		)
-		// ko-KR
-		.replace(/[나-낳]/g, (match) =>
-			String.fromCharCode(
-				match.charCodeAt(0)! + "냐".charCodeAt(0) - "나".charCodeAt(0),
-			),
-		)
-		.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, "다냥")
-		.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, "냥")
-		// el-GR
-		.replaceAll("να", "νια")
-		.replaceAll("ΝΑ", "ΝΙΑ")
-		.replaceAll("Να", "Νια");
-
-	// zh-CN, zh-TW
-	if (lang === "zh") text = text.replace(/(妙|庙|描|渺|瞄|秒|苗|藐|廟)/g, "喵");
-
-	return text;
-}
diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index 2921f68be3..ed6ecc4a5c 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -12,7 +12,7 @@ import {
 	Channels,
 } from "../index.js";
 import type { Packed } from "@/misc/schema.js";
-import { nyaify } from "@/misc/nyaify.js";
+import { nyaify } from "backend-rs";
 import { awaitAll } from "@/prelude/await-all.js";
 import { convertReactions, decodeReaction } from "@/misc/reaction-lib.js";
 import type { NoteReaction } from "@/models/entities/note-reaction.js";