diff --git a/app/src/main/java/cc/microblock/hook/Pangu.kt b/app/src/main/java/cc/microblock/hook/Pangu.kt
new file mode 100644
index 0000000000..b946191573
--- /dev/null
+++ b/app/src/main/java/cc/microblock/hook/Pangu.kt
@@ -0,0 +1,187 @@
+/*
+ * QAuxiliary - An Xposed module for QQ/TIM
+ * Copyright (C) 2019-2023 QAuxiliary developers
+ * https://github.com/cinit/QAuxiliary
+ *
+ * This software is non-free but opensource software: you can redistribute it
+ * and/or modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version and our eula as published
+ * by QAuxiliary contributors.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * and eula along with this software. If not, see
+ *
+ * .
+ */
+
+package cc.microblock.hook
+
+import com.github.kyuubiran.ezxhelper.utils.hookBefore
+import io.github.qauxv.base.annotation.FunctionHookEntry
+import io.github.qauxv.base.annotation.UiItemAgentEntry
+import io.github.qauxv.dsl.FunctionEntryRouter
+import io.github.qauxv.hook.CommonSwitchFunctionHook
+import io.github.qauxv.util.Log
+import io.github.qauxv.util.dexkit.AIOTextElementCtor
+import io.github.qauxv.util.dexkit.DexKit
+import xyz.nextalone.util.get
+import xyz.nextalone.util.set
+import java.util.regex.Pattern
+
+val CJK = "\u2e80-\u2eff\u2f00-\u2fdf\u3040-\u309f\u30a0-\u30fa\u30fc-\u30ff\u3100-\u312f\u3200-\u32ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff"
+
+val ANY_CJK = Pattern.compile("[$CJK]")
+
+val CONVERT_TO_FULLWIDTH_CJK_SYMBOLS_CJK = Pattern.compile("([$CJK])[ ]*(\\:+|\\.)[ ]*([$CJK])")
+val CONVERT_TO_FULLWIDTH_CJK_SYMBOLS = Pattern.compile("([$CJK])[ ]*([~\\!;,\\?]+)[ ]*")
+val DOTS_CJK = Pattern.compile("([\\.]{2,}|\\u2026)([$CJK])")
+val FIX_CJK_COLON_ANS = Pattern.compile("([$CJK])\\:([A-Z0-9\\(\\)])")
+
+val CJK_QUOTE = Pattern.compile("([$CJK])([\\`\"\\u05f4])")
+val QUOTE_CJK = Pattern.compile("([\\`\"\\u05f4])([$CJK])")
+val FIX_QUOTE_ANY_QUOTE = Pattern.compile("([`\"\\u05f4]+)[ ]*(.+?)[ ]*([`\"\\u05f4]+)")
+
+val CJK_SINGLE_QUOTE_BUT_POSSESSIVE = Pattern.compile("([$CJK])('[^s])")
+val SINGLE_QUOTE_CJK = Pattern.compile("(')([$CJK])")
+val FIX_POSSESSIVE_SINGLE_QUOTE = Pattern.compile("([A-Za-z0-9${CJK}])( )('s)")
+
+val HASH_ANS_CJK_HASH = Pattern.compile("([$CJK])(#)([$CJK]+)(#)([$CJK])")
+val CJK_HASH = Pattern.compile("([$CJK])(#([^ ]))")
+val HASH_CJK = Pattern.compile("(([^ ])#)([$CJK])")
+
+val CJK_OPERATOR_ANS = Pattern.compile("([$CJK])([\\+\\-\\*\\/=&\\|<>])([A-Za-z0-9])")
+val ANS_OPERATOR_CJK = Pattern.compile("([A-Za-z0-9])([\\+\\-\\*\\/=&\\|<>])([$CJK])")
+
+val FIX_SLASH_AS = Pattern.compile("([/]) ([a-z\\-\\_\\./]+)")
+val FIX_SLASH_AS_SLASH = Pattern.compile("([/\\.])([A-Za-z\\-\\_\\./]+) ([/])")
+
+val CJK_LEFT_BRACKET = Pattern.compile("([$CJK])([\\(\\[\\{<>\\u201c])")
+val RIGHT_BRACKET_CJK = Pattern.compile("([\\)\\]\\}>\\u201d])([$CJK])")
+val FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET = Pattern.compile("([\\(\\[\\{<\\u201c]+)[ ]*(.+?)[ ]*([\\)\\]\\}>\u201d]+)")
+val ANS_CJK_LEFT_BRACKET_ANY_RIGHT_BRACKET = Pattern.compile("([A-Za-z0-9${CJK}])[ ]*([\\u201c])([A-Za-z0-9${CJK}\\-_ ]+)([\\u201d])")
+val LEFT_BRACKET_ANY_RIGHT_BRACKET_ANS_CJK = Pattern.compile("([\\u201c])([A-Za-z0-9${CJK}\\-_ ]+)([\\u201d])[ ]*([A-Za-z0-9${CJK}])")
+
+val AN_LEFT_BRACKET = Pattern.compile("([A-Za-z0-9])([\\(\\[\\{])")
+val RIGHT_BRACKET_AN = Pattern.compile("([\\)\\]\\}])([A-Za-z0-9])")
+
+val CJK_ANS = Pattern.compile("([$CJK])([A-Za-z\\u0370-\\u03ff0-9@\$%\\^&\\*\\-\\+\\\\=\\|/\\u00a1-\\u00ff\\u2150-\\u218f\\u2700—\\u27bf])")
+val ANS_CJK = Pattern.compile("([A-Za-z\\u0370-\\u03ff0-9~\\\$%\\^&\\*\\-\\+\\\\=\\|/!;:,\\.\\?\\u00a1-\\u00ff\\u2150-\\u218f\\u2700—\\u27bf])([$CJK])")
+
+val S_A = Pattern.compile("(%)([A-Za-z])")
+
+val MIDDLE_DOT = Pattern.compile("([ ]*)([\\u00b7\\u2022\\u2027])([ ]*)")
+
+fun convertToFullwidth(symbols: String): String {
+ return symbols
+ .replace("~", "~")
+ .replace("!", "!")
+ .replace(";", ";")
+ .replace(":", ":")
+ .replace(",", ",")
+ .replace(".", "。")
+ .replace("?", "?")
+}
+
+// This is an incomplete implementation of https://github.com/vinta/pangu.js/blob/master/src/shared/core.js
+// https://github.com/vinta/pangu.js/blob/6107055384b99e6f30a49f5d1b85aa0b78251dc2/src/shared/core.js#L118-L126
+// These lines are implemented, work in the playground, but are commented in the actual code, as they are causing compile errors
+// As I'm not so familiar with Kotlin, if someone is able to figure it out and make it a complete impl, I'll appreciate it.
+fun spacing(text: String): String {
+ if (text.length <= 1 || !ANY_CJK.matcher(text).find()) {
+ return text
+ }
+
+ var newText = text
+
+/*
+
+// Corresponding lines: https://github.com/vinta/pangu.js/blob/6107055384b99e6f30a49f5d1b85aa0b78251dc2/src/shared/core.js#L118-L126
+
+ newText = CONVERT_TO_FULLWIDTH_CJK_SYMBOLS_CJK.matcher(newText).replaceAll { matchResult ->
+ val leftCjk = matchResult.group(1)
+ val symbols = matchResult.group(2)
+ val rightCjk = matchResult.group(3)
+ val fullwidthSymbols = convertToFullwidth(symbols)
+ "$leftCjk$fullwidthSymbols$rightCjk"
+ }
+
+ newText = CONVERT_TO_FULLWIDTH_CJK_SYMBOLS.matcher(newText).replaceAll { matchResult ->
+ val cjk = matchResult.group(1)
+ val symbols = matchResult.group(2)
+ val fullwidthSymbols = convertToFullwidth(symbols)
+ "$cjk$fullwidthSymbols"
+ }
+*/
+
+ newText = DOTS_CJK.matcher(newText).replaceAll("$1 $2")
+ newText = FIX_CJK_COLON_ANS.matcher(newText).replaceAll("$1:$2")
+
+ newText = CJK_QUOTE.matcher(newText).replaceAll("$1 $2")
+ newText = QUOTE_CJK.matcher(newText).replaceAll("$1 $2")
+ newText = FIX_QUOTE_ANY_QUOTE.matcher(newText).replaceAll("$1$2$3")
+
+ newText = CJK_SINGLE_QUOTE_BUT_POSSESSIVE.matcher(newText).replaceAll("$1 $2")
+ newText = SINGLE_QUOTE_CJK.matcher(newText).replaceAll("$1 $2")
+ newText = FIX_POSSESSIVE_SINGLE_QUOTE.matcher(newText).replaceAll("$1's")
+
+ newText = HASH_ANS_CJK_HASH.matcher(newText).replaceAll("$1 $2$3$4 $5")
+ newText = CJK_HASH.matcher(newText).replaceAll("$1 $2")
+ newText = HASH_CJK.matcher(newText).replaceAll("$1 $3")
+
+ newText = CJK_OPERATOR_ANS.matcher(newText).replaceAll("$1 $2 $3")
+ newText = ANS_OPERATOR_CJK.matcher(newText).replaceAll("$1 $2 $3")
+
+ newText = FIX_SLASH_AS.matcher(newText).replaceAll("$1$2")
+ newText = FIX_SLASH_AS_SLASH.matcher(newText).replaceAll("$1$2$3")
+
+ newText = CJK_LEFT_BRACKET.matcher(newText).replaceAll("$1 $2")
+ newText = RIGHT_BRACKET_CJK.matcher(newText).replaceAll("$1 $2")
+ newText = FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET.matcher(newText).replaceAll("$1$2$3")
+ newText = ANS_CJK_LEFT_BRACKET_ANY_RIGHT_BRACKET.matcher(newText).replaceAll("$1 $2$3$4")
+ newText = LEFT_BRACKET_ANY_RIGHT_BRACKET_ANS_CJK.matcher(newText).replaceAll("$1$2$3 $4")
+
+ newText = AN_LEFT_BRACKET.matcher(newText).replaceAll("$1 $2")
+ newText = RIGHT_BRACKET_AN.matcher(newText).replaceAll("$1 $2")
+
+ newText = CJK_ANS.matcher(newText).replaceAll("$1 $2")
+ newText = ANS_CJK.matcher(newText).replaceAll("$1 $2")
+
+ newText = S_A.matcher(newText).replaceAll("$1 $2")
+
+ newText = MIDDLE_DOT.matcher(newText).replaceAll("・")
+
+ return newText
+}
+
+
+@FunctionHookEntry
+@UiItemAgentEntry
+object SendPangu : CommonSwitchFunctionHook("sendMsgPangu",arrayOf(AIOTextElementCtor)) {
+ override val name = "发送消息自动 Pangu.kt"
+ override val description = "自动在中英文间加上空格,以美化排版\n若消息以空格开头,则不会被 Pangu 化"
+
+ override val uiItemLocation = FunctionEntryRouter.Locations.Simplify.CHAT_OTHER
+
+ override fun initOnce(): Boolean {
+ DexKit.requireMethodFromCache(AIOTextElementCtor)
+ .hookBefore {
+ val content = it.args[0].get("a") as String;
+ if(!content.startsWith(" "))
+ it.args[0].set("a", SendPangu.processPangu(content))
+ else
+ it.args[0].set("a", content.substring(1))
+ }
+
+ return true;
+ }
+
+ private fun processPangu(message: String): String {
+ return spacing(message);
+ }
+}
diff --git a/app/src/main/java/io/github/qauxv/util/dexkit/DexKitTarget.kt b/app/src/main/java/io/github/qauxv/util/dexkit/DexKitTarget.kt
index 0bde29ae4f..8ff9bdc336 100644
--- a/app/src/main/java/io/github/qauxv/util/dexkit/DexKitTarget.kt
+++ b/app/src/main/java/io/github/qauxv/util/dexkit/DexKitTarget.kt
@@ -769,3 +769,10 @@ data object Multiforward_Avatar_setListener_NT : DexKitTarget.UsingDexkit() {
override val declaringClass = "com.tencent.mobileqq.aio.msglist.holder.component.avatar.AIOAvatarContentComponent"
override val filter = DexKitFilter.allowAll
}
+
+data object AIOTextElementCtor: DexKitTarget.UsingStr() {
+ override val findMethod: Boolean = true;
+ override val declaringClass = "com.tencent.mobileqq.aio.msg.AIOMsgElement.AIOTextElementCtor"
+ override val traitString = arrayOf("textElement")
+ override val filter = DexKitFilter.strInClsName("com/tencent/mobileqq/aio/msg")
+}