6.6.1 - Alpha2 - 支持 Node.js 拼音模块及结巴分词

This commit is contained in:
SuperMonster003
2024-12-24 20:24:32 +08:00
parent 7c62902429
commit 9a4b581f07
53 changed files with 38238 additions and 212 deletions

View File

@@ -4,6 +4,7 @@
"released_date": "2024/12/09",
"feature": [
"pinyin 模块, 用于汉语拼音转换 (参阅 项目文档 > [汉语拼音](https://docs.autojs6.com/#/pinyin))",
"pinyin4j 模块, 用于汉语拼音转换 (参阅 项目文档 > [汉语拼音](https://docs.autojs6.com/#/pinyin4j))",
"UiObject#isSimilar 及 UiObjectCollection#isSimilar 方法, 用于确定控件或控件集合是否相似"
],
"fix": [
@@ -15,9 +16,13 @@
],
"improvement": [
"脚本项目识别在 project.json 损坏情况下尽可能还原关键信息",
"打包单文件时自动生成的包名后缀支持将简体中文自动转换为拼音",
"打包单文件时自动生成的包名后缀支持将简体中文转换为拼音 (支持多音字)",
"UiSelector#findOnce 及 UiSelector#find 方法支持负数参数",
"UI 元素及 className 相关选择器支持更多的包名前缀省略形式 (如 RecyclerView, Snackbar 等)"
],
"dependency": [
"附加 Jieba Analysis 版本 1.0.3-SNAPSHOT (modified)",
"升级 Gradle 版本 8.11.1 -> 8.12"
]
},
"v6.6.0": {

View File

@@ -198,9 +198,13 @@ dependencies /* Unclassified */ {
implementation("com.github.getActivity:Toaster:12.6")
implementation("com.github.getActivity:EasyWindow:10.3")
// pinyin4j
// Pinyin4j
implementation("com.belerweb:pinyin4j:2.5.0")
// Jieba Analysis (zh-CN: 结巴分词)
// implementation("com.huaban:jieba-analysis:1.0.2")
implementation(project(":jieba-analysis"))
}
dependencies /* MIME */ {
@@ -741,6 +745,12 @@ extra {
versions.handleIfNeeded(project, flavorNameApp, listOf(buildTypeDebug, buildTypeRelease))
}
gradle.beforeProject {
extensions.extraProperties["compileSdk"] = versions.sdkVersionCompile
extensions.extraProperties["minSdk"] = versions.sdkVersionMin
extensions.extraProperties["targetSdk"] = versions.sdkVersionTarget
}
class Sign(filePath: String) {
var isValid = false
@@ -828,6 +838,10 @@ class Versions(filePath: String) {
javaVersionInfoSuffix += " [consistent]"
}
gradle.beforeProject {
extensions.extraProperties["javaVersion"] = niceVersionInt
}
JavaVersion.toVersion(niceVersionInt)
}

View File

@@ -1,5 +1,6 @@
package org.autojs.autojs.engine
import android.annotation.SuppressLint
import android.util.Log
import android.view.View
import org.autojs.autojs.core.ui.ViewExtras
@@ -179,7 +180,16 @@ open class RhinoJavaScriptEngine(private val scriptRuntime: ScriptRuntime, priva
initStandardObjects(context, false)
}
fun enterContext(): Context = RhinoAndroidHelper(androidContext).enterContext().also { setupContext(it) }
@SuppressLint("VisibleForTests")
fun enterContext(): Context {
val rhinoAndroidHelper = RhinoAndroidHelper(androidContext)
val enterContext = try {
rhinoAndroidHelper.enterContext()
} catch (e: SecurityException) {
rhinoAndroidHelper.contextFactory.enterContext()
}
return enterContext.also { setupContext(it) }
}
fun setupContext(context: Context) = context.apply {
if (this is AutoJsContext) {

View File

@@ -72,6 +72,7 @@ import org.autojs.autojs.runtime.api.augment.ocr.OcrPaddle
import org.autojs.autojs.runtime.api.augment.ocr.OcrRapid
import org.autojs.autojs.runtime.api.augment.opencc.OpenCC
import org.autojs.autojs.runtime.api.augment.pinyin.Pinyin
import org.autojs.autojs.runtime.api.augment.pinyin4j.Pinyin4j
import org.autojs.autojs.runtime.api.augment.plugins.Plugins
import org.autojs.autojs.runtime.api.augment.recorder.Recorder
import org.autojs.autojs.runtime.api.augment.s13n.S13n
@@ -763,6 +764,7 @@ class ScriptRuntime private constructor(builder: Builder) {
SQLite(this).augment(target, true)
NanoID.augment(target, true)
Pinyin.augment(target, true)
Pinyin4j.augment(target, true)
augmentedApp.defineProp(Autojs::class.java.simpleName.lowercase(), augmentedAutojs)
}

View File

@@ -24,7 +24,7 @@ class RootAutomatorNativeObject(scriptRuntime: ScriptRuntime, waitForReady: Any?
init {
super.exportAsJSClass(MAX_PROTOTYPE_ID, this, false)
defineProperty("__ra__", mRootAutomatorObject, DONTENUM or READONLY or PERMANENT)
defineProperty("__ra__", mRootAutomatorObject, READONLY or DONTENUM or PERMANENT)
}
override fun has(name: String?): Boolean {

View File

@@ -0,0 +1,517 @@
package org.autojs.autojs.runtime.api.augment.pinyin
object Dict {
/* 带声调字符. */
val PHONETIC_SYMBOL = mapOf(
"ā" to "a1",
"á" to "a2",
"ǎ" to "a3",
"à" to "a4",
"ē" to "e1",
"é" to "e2",
"ě" to "e3",
"è" to "e4",
"ō" to "o1",
"ó" to "o2",
"ǒ" to "o3",
"ò" to "o4",
"ī" to "i1",
"í" to "i2",
"ǐ" to "i3",
"ì" to "i4",
"ū" to "u1",
"ú" to "u2",
"ǔ" to "u3",
"ù" to "u4",
"ü" to "v0",
"ǘ" to "v2",
"ǚ" to "v3",
"ǜ" to "v4",
"ń" to "n2",
"ň" to "n3",
"" to "m2",
)
/* 百家姓. */
/* https://zh.wikipedia.org/wiki/%E7%99%BE%E5%AE%B6%E5%A7%93 */
val SURNAME = mapOf(
"赵" to listOf(listOf("zhào")),
"钱" to listOf(listOf("qián")),
"孙" to listOf(listOf("sūn")),
"李" to listOf(listOf("lǐ")),
"周" to listOf(listOf("zhōu")),
"吴" to listOf(listOf("wú")),
"郑" to listOf(listOf("zhèng")),
"王" to listOf(listOf("wáng")),
"冯" to listOf(listOf("féng")),
"陈" to listOf(listOf("chén")),
"褚" to listOf(listOf("chǔ")),
"卫" to listOf(listOf("wèi")),
"蒋" to listOf(listOf("jiǎng")),
"沈" to listOf(listOf("shěn")),
"韩" to listOf(listOf("hán")),
"杨" to listOf(listOf("yáng")),
"朱" to listOf(listOf("zhū")),
"秦" to listOf(listOf("qín")),
"尤" to listOf(listOf("yóu")),
"许" to listOf(listOf("xǔ")),
"何" to listOf(listOf("hé")),
"吕" to listOf(listOf("lǚ")),
"施" to listOf(listOf("shī")),
"张" to listOf(listOf("zhāng")),
"孔" to listOf(listOf("kǒng")),
"曹" to listOf(listOf("cáo")),
"严" to listOf(listOf("yán")),
"华" to listOf(listOf("huà")),
"金" to listOf(listOf("jīn")),
"魏" to listOf(listOf("wèi")),
"陶" to listOf(listOf("táo")),
"姜" to listOf(listOf("jiāng")),
"戚" to listOf(listOf("qī")),
"谢" to listOf(listOf("xiè")),
"邹" to listOf(listOf("zōu")),
"喻" to listOf(listOf("yù")),
"柏" to listOf(listOf("bǎi")),
"水" to listOf(listOf("shuǐ")),
"窦" to listOf(listOf("dòu")),
"章" to listOf(listOf("zhāng")),
"云" to listOf(listOf("yún")),
"苏" to listOf(listOf("sū")),
"潘" to listOf(listOf("pān")),
"葛" to listOf(listOf("gě")),
"奚" to listOf(listOf("xī")),
"范" to listOf(listOf("fàn")),
"彭" to listOf(listOf("péng")),
"郎" to listOf(listOf("láng")),
"鲁" to listOf(listOf("lǔ")),
"韦" to listOf(listOf("wéi")),
"昌" to listOf(listOf("chāng")),
"马" to listOf(listOf("mǎ")),
"苗" to listOf(listOf("miáo")),
"凤" to listOf(listOf("fèng")),
"花" to listOf(listOf("huā")),
"方" to listOf(listOf("fāng")),
"俞" to listOf(listOf("yú")),
"任" to listOf(listOf("rèn")),
"袁" to listOf(listOf("yuán")),
"柳" to listOf(listOf("liǔ")),
"酆" to listOf(listOf("fēng")),
"鲍" to listOf(listOf("bào")),
"史" to listOf(listOf("shǐ")),
"唐" to listOf(listOf("táng")),
"费" to listOf(listOf("fèi")),
"廉" to listOf(listOf("lián")),
"岑" to listOf(listOf("cén")),
"薛" to listOf(listOf("xuē")),
"雷" to listOf(listOf("léi")),
"贺" to listOf(listOf("hè")),
"倪" to listOf(listOf("ní")),
"汤" to listOf(listOf("tāng")),
"滕" to listOf(listOf("téng")),
"殷" to listOf(listOf("yīn")),
"罗" to listOf(listOf("luó")),
"毕" to listOf(listOf("bì")),
"郝" to listOf(listOf("hǎo")),
"邬" to listOf(listOf("wū")),
"安" to listOf(listOf("ān")),
"常" to listOf(listOf("cháng")),
"乐" to listOf(listOf("yuè")),
"于" to listOf(listOf("yú")),
"时" to listOf(listOf("shí")),
"傅" to listOf(listOf("fù")),
"皮" to listOf(listOf("pí")),
"卞" to listOf(listOf("biàn")),
"齐" to listOf(listOf("qí")),
"康" to listOf(listOf("kāng")),
"伍" to listOf(listOf("wǔ")),
"余" to listOf(listOf("yú")),
"元" to listOf(listOf("yuán")),
"卜" to listOf(listOf("bǔ")),
"顾" to listOf(listOf("gù")),
"孟" to listOf(listOf("mèng")),
"平" to listOf(listOf("píng")),
"黄" to listOf(listOf("huáng")),
"和" to listOf(listOf("hé")),
"穆" to listOf(listOf("mù")),
"萧" to listOf(listOf("xiāo")),
"尹" to listOf(listOf("yǐn")),
"姚" to listOf(listOf("yáo")),
"邵" to listOf(listOf("shào")),
"湛" to listOf(listOf("zhàn")),
"汪" to listOf(listOf("wāng")),
"祁" to listOf(listOf("qí")),
"毛" to listOf(listOf("máo")),
"禹" to listOf(listOf("yǔ")),
"狄" to listOf(listOf("dí")),
"米" to listOf(listOf("mǐ")),
"贝" to listOf(listOf("bèi")),
"明" to listOf(listOf("míng")),
"臧" to listOf(listOf("zāng")),
"计" to listOf(listOf("jì")),
"伏" to listOf(listOf("fú")),
"成" to listOf(listOf("chéng")),
"戴" to listOf(listOf("dài")),
"谈" to listOf(listOf("tán")),
"宋" to listOf(listOf("sòng")),
"茅" to listOf(listOf("máo")),
"庞" to listOf(listOf("páng")),
"熊" to listOf(listOf("xióng")),
"纪" to listOf(listOf("jì")),
"舒" to listOf(listOf("shū")),
"屈" to listOf(listOf("qū")),
"项" to listOf(listOf("xiàng")),
"祝" to listOf(listOf("zhù")),
"董" to listOf(listOf("dǒng")),
"梁" to listOf(listOf("liáng")),
"杜" to listOf(listOf("dù")),
"阮" to listOf(listOf("ruǎn")),
"蓝" to listOf(listOf("lán")),
"闵" to listOf(listOf("mǐn")),
"席" to listOf(listOf("xí")),
"季" to listOf(listOf("jì")),
"麻" to listOf(listOf("má")),
"强" to listOf(listOf("qiáng")),
"贾" to listOf(listOf("jiǎ")),
"路" to listOf(listOf("lù")),
"娄" to listOf(listOf("lóu")),
"危" to listOf(listOf("wēi")),
"江" to listOf(listOf("jiāng")),
"童" to listOf(listOf("tóng")),
"颜" to listOf(listOf("yán")),
"郭" to listOf(listOf("guō")),
"梅" to listOf(listOf("méi")),
"盛" to listOf(listOf("shèng")),
"林" to listOf(listOf("lín")),
"刁" to listOf(listOf("diāo")),
"钟" to listOf(listOf("zhōng")),
"徐" to listOf(listOf("xú")),
"邱" to listOf(listOf("qiū")),
"骆" to listOf(listOf("luò")),
"高" to listOf(listOf("gāo")),
"夏" to listOf(listOf("xià")),
"蔡" to listOf(listOf("cài")),
"田" to listOf(listOf("tián")),
"樊" to listOf(listOf("fán")),
"胡" to listOf(listOf("hú")),
"凌" to listOf(listOf("líng")),
"霍" to listOf(listOf("huò")),
"虞" to listOf(listOf("yú")),
"万" to listOf(listOf("wàn")),
"支" to listOf(listOf("zhī")),
"柯" to listOf(listOf("kē")),
"昝" to listOf(listOf("zǎn")),
"管" to listOf(listOf("guǎn")),
"卢" to listOf(listOf("lú")),
"莫" to listOf(listOf("mò")),
"经" to listOf(listOf("jīng")),
"房" to listOf(listOf("fáng")),
"裘" to listOf(listOf("qiú")),
"缪" to listOf(listOf("miào")),
"干" to listOf(listOf("gān")),
"解" to listOf(listOf("xiè")),
"应" to listOf(listOf("yīng")),
"宗" to listOf(listOf("zōng")),
"丁" to listOf(listOf("dīng")),
"宣" to listOf(listOf("xuān")),
"贲" to listOf(listOf("bēn")),
"邓" to listOf(listOf("dèng")),
"郁" to listOf(listOf("yù")),
"单" to listOf(listOf("shàn")),
"杭" to listOf(listOf("háng")),
"洪" to listOf(listOf("hóng")),
"包" to listOf(listOf("bāo")),
"诸" to listOf(listOf("zhū")),
"左" to listOf(listOf("zuǒ")),
"石" to listOf(listOf("shí")),
"崔" to listOf(listOf("cuī")),
"吉" to listOf(listOf("jí")),
"钮" to listOf(listOf("niǔ")),
"龚" to listOf(listOf("gōng")),
"程" to listOf(listOf("chéng")),
"嵇" to listOf(listOf("jī")),
"邢" to listOf(listOf("xíng")),
"滑" to listOf(listOf("huá")),
"裴" to listOf(listOf("péi")),
"陆" to listOf(listOf("lù")),
"荣" to listOf(listOf("róng")),
"翁" to listOf(listOf("wēng")),
"荀" to listOf(listOf("xún")),
"羊" to listOf(listOf("yáng")),
"於" to listOf(listOf("yū")),
"惠" to listOf(listOf("huì")),
"甄" to listOf(listOf("zhēn")),
"曲" to listOf(listOf("qū")),
"家" to listOf(listOf("jiā")),
"封" to listOf(listOf("fēng")),
"芮" to listOf(listOf("ruì")),
"羿" to listOf(listOf("yì")),
"储" to listOf(listOf("chǔ")),
"靳" to listOf(listOf("jìn")),
"汲" to listOf(listOf("jí")),
"邴" to listOf(listOf("bǐng")),
"糜" to listOf(listOf("mí")),
"松" to listOf(listOf("sōng")),
"井" to listOf(listOf("jǐng")),
"段" to listOf(listOf("duàn")),
"富" to listOf(listOf("fù")),
"巫" to listOf(listOf("wū")),
"乌" to listOf(listOf("wū")),
"焦" to listOf(listOf("jiāo")),
"巴" to listOf(listOf("bā")),
"弓" to listOf(listOf("gōng")),
"牧" to listOf(listOf("mù")),
"隗" to listOf(listOf("kuí")),
"山" to listOf(listOf("shān")),
"谷" to listOf(listOf("gǔ")),
"车" to listOf(listOf("chē")),
"侯" to listOf(listOf("hóu")),
"宓" to listOf(listOf("mì")),
"蓬" to listOf(listOf("péng")),
"全" to listOf(listOf("quán")),
"郗" to listOf(listOf("xī")),
"班" to listOf(listOf("bān")),
"仰" to listOf(listOf("yǎng")),
"秋" to listOf(listOf("qiū")),
"仲" to listOf(listOf("zhòng")),
"伊" to listOf(listOf("yī")),
"宫" to listOf(listOf("gōng")),
"宁" to listOf(listOf("nìng")),
"仇" to listOf(listOf("qiú")),
"栾" to listOf(listOf("luán")),
"暴" to listOf(listOf("bào")),
"甘" to listOf(listOf("gān")),
"钭" to listOf(listOf("tǒu")),
"厉" to listOf(listOf("lì")),
"戎" to listOf(listOf("róng")),
"祖" to listOf(listOf("zǔ")),
"武" to listOf(listOf("wǔ")),
"符" to listOf(listOf("fú")),
"刘" to listOf(listOf("liú")),
"景" to listOf(listOf("jǐng")),
"詹" to listOf(listOf("zhān")),
"束" to listOf(listOf("shù")),
"龙" to listOf(listOf("lóng")),
"叶" to listOf(listOf("yè")),
"幸" to listOf(listOf("xìng")),
"司" to listOf(listOf("sī")),
"韶" to listOf(listOf("sháo")),
"郜" to listOf(listOf("gào")),
"黎" to listOf(listOf("lí")),
"蓟" to listOf(listOf("jì")),
"薄" to listOf(listOf("bó")),
"印" to listOf(listOf("yìn")),
"宿" to listOf(listOf("sù")),
"白" to listOf(listOf("bái")),
"怀" to listOf(listOf("huái")),
"蒲" to listOf(listOf("pú")),
"邰" to listOf(listOf("tái")),
"从" to listOf(listOf("cóng")),
"鄂" to listOf(listOf("è")),
"索" to listOf(listOf("suǒ")),
"咸" to listOf(listOf("xián")),
"籍" to listOf(listOf("jí")),
"赖" to listOf(listOf("lài")),
"卓" to listOf(listOf("zhuó")),
"蔺" to listOf(listOf("lìn")),
"屠" to listOf(listOf("tú")),
"蒙" to listOf(listOf("méng")),
"池" to listOf(listOf("chí")),
"乔" to listOf(listOf("qiáo")),
"阴" to listOf(listOf("yīn")),
"鬱" to listOf(listOf("yù")),
"胥" to listOf(listOf("xū")),
"能" to listOf(listOf("nài")),
"苍" to listOf(listOf("cāng")),
"双" to listOf(listOf("shuāng")),
"闻" to listOf(listOf("wén")),
"莘" to listOf(listOf("shēn")),
"党" to listOf(listOf("dǎng")),
"翟" to listOf(listOf("zhái")),
"谭" to listOf(listOf("tán")),
"贡" to listOf(listOf("gòng")),
"劳" to listOf(listOf("láo")),
"逄" to listOf(listOf("páng")),
"姬" to listOf(listOf("jī")),
"申" to listOf(listOf("shēn")),
"扶" to listOf(listOf("fú")),
"堵" to listOf(listOf("dǔ")),
"冉" to listOf(listOf("rǎn")),
"宰" to listOf(listOf("zǎi")),
"郦" to listOf(listOf("lì")),
"雍" to listOf(listOf("yōng")),
"郤" to listOf(listOf("xì")),
"璩" to listOf(listOf("qú")),
"桑" to listOf(listOf("sāng")),
"桂" to listOf(listOf("guì")),
"濮" to listOf(listOf("pú")),
"牛" to listOf(listOf("niú")),
"寿" to listOf(listOf("shòu")),
"通" to listOf(listOf("tōng")),
"边" to listOf(listOf("biān")),
"扈" to listOf(listOf("hù")),
"燕" to listOf(listOf("yān")),
"冀" to listOf(listOf("jì")),
"郏" to listOf(listOf("jiá")),
"浦" to listOf(listOf("pǔ")),
"尚" to listOf(listOf("shàng")),
"农" to listOf(listOf("nóng")),
"温" to listOf(listOf("wēn")),
"别" to listOf(listOf("bié")),
"庄" to listOf(listOf("zhuāng")),
"晏" to listOf(listOf("yàn")),
"柴" to listOf(listOf("chái")),
"瞿" to listOf(listOf("qú")),
"阎" to listOf(listOf("yán")),
"充" to listOf(listOf("chōng")),
"慕" to listOf(listOf("mù")),
"连" to listOf(listOf("lián")),
"茹" to listOf(listOf("rú")),
"习" to listOf(listOf("xí")),
"宦" to listOf(listOf("huàn")),
"艾" to listOf(listOf("ài")),
"鱼" to listOf(listOf("yú")),
"容" to listOf(listOf("róng")),
"向" to listOf(listOf("xiàng")),
"古" to listOf(listOf("gǔ")),
"易" to listOf(listOf("yì")),
"慎" to listOf(listOf("shèn")),
"戈" to listOf(listOf("gē")),
"廖" to listOf(listOf("liào")),
"庾" to listOf(listOf("yǔ")),
"终" to listOf(listOf("zhōng")),
"暨" to listOf(listOf("jì")),
"居" to listOf(listOf("jū")),
"衡" to listOf(listOf("héng")),
"步" to listOf(listOf("bù")),
"都" to listOf(listOf("dū")),
"耿" to listOf(listOf("gěng")),
"满" to listOf(listOf("mǎn")),
"弘" to listOf(listOf("hóng")),
"匡" to listOf(listOf("kuāng")),
"国" to listOf(listOf("guó")),
"文" to listOf(listOf("wén")),
"寇" to listOf(listOf("kòu")),
"广" to listOf(listOf("guǎng")),
"禄" to listOf(listOf("lù")),
"阙" to listOf(listOf("quē")),
"东" to listOf(listOf("dōng")),
"欧" to listOf(listOf("ōu")),
"殳" to listOf(listOf("shū")),
"沃" to listOf(listOf("wò")),
"利" to listOf(listOf("lì")),
"蔚" to listOf(listOf("wèi")),
"越" to listOf(listOf("yuè")),
"夔" to listOf(listOf("kuí")),
"隆" to listOf(listOf("lóng")),
"师" to listOf(listOf("shī")),
"巩" to listOf(listOf("gǒng")),
"厍" to listOf(listOf("shè")),
"聂" to listOf(listOf("niè")),
"晁" to listOf(listOf("cháo")),
"勾" to listOf(listOf("gōu")),
"敖" to listOf(listOf("áo")),
"融" to listOf(listOf("róng")),
"冷" to listOf(listOf("lěng")),
"訾" to listOf(listOf("zǐ")),
"辛" to listOf(listOf("xīn")),
"阚" to listOf(listOf("kàn")),
"那" to listOf(listOf("nā")),
"简" to listOf(listOf("jiǎn")),
"饶" to listOf(listOf("ráo")),
"空" to listOf(listOf("kōng")),
"曾" to listOf(listOf("zēng")),
"母" to listOf(listOf("mǔ")),
"沙" to listOf(listOf("shā")),
"乜" to listOf(listOf("niè")),
"养" to listOf(listOf("yǎng")),
"鞠" to listOf(listOf("jū")),
"须" to listOf(listOf("xū")),
"丰" to listOf(listOf("fēng")),
"巢" to listOf(listOf("cháo")),
"关" to listOf(listOf("guān")),
"蒯" to listOf(listOf("kuǎi")),
"相" to listOf(listOf("xiàng")),
"查" to listOf(listOf("zhā")),
"后" to listOf(listOf("hòu")),
"荆" to listOf(listOf("jīng")),
"红" to listOf(listOf("hóng")),
"游" to listOf(listOf("yóu")),
"竺" to listOf(listOf("zhú")),
"权" to listOf(listOf("quán")),
"逯" to listOf(listOf("lù")),
"盖" to listOf(listOf("gài")),
"益" to listOf(listOf("yì")),
"桓" to listOf(listOf("huán")),
"公" to listOf(listOf("gōng")),
"牟" to listOf(listOf("móu")),
"哈" to listOf(listOf("hǎ")),
"言" to listOf(listOf("yán")),
"福" to listOf(listOf("fú")),
)
/* 复姓. */
val COMPOUND_SURNAME = mapOf(
"万俟" to listOf(listOf("mò"), listOf("qí")),
"上官" to listOf(listOf("shàng"), listOf("guān")),
"东方" to listOf(listOf("dōng"), listOf("fāng")),
"东郭" to listOf(listOf("dōng"), listOf("guō")),
"东门" to listOf(listOf("dōng"), listOf("mén")),
"乐正" to listOf(listOf("yuè"), listOf("zhèng")),
"亓官" to listOf(listOf("qí"), listOf("guān")),
"仉督" to listOf(listOf("zhǎng"), listOf("dū")),
"令狐" to listOf(listOf("líng"), listOf("hú")),
"仲孙" to listOf(listOf("zhòng"), listOf("sūn")),
"公冶" to listOf(listOf("gōng"), listOf("yě")),
"公孙" to listOf(listOf("gōng"), listOf("sūn")),
"公羊" to listOf(listOf("gōng"), listOf("yáng")),
"公良" to listOf(listOf("gōng"), listOf("liáng")),
"公西" to listOf(listOf("gōng"), listOf("xī")),
"单于" to listOf(listOf("chán"), listOf("yú")),
"南宫" to listOf(listOf("nán"), listOf("gōng")),
"南门" to listOf(listOf("nán"), listOf("mén")),
"司寇" to listOf(listOf("sī"), listOf("kòu")),
"司徒" to listOf(listOf("sī"), listOf("tú")),
"司空" to listOf(listOf("sī"), listOf("kōng")),
"司马" to listOf(listOf("sī"), listOf("mǎ")),
"呼延" to listOf(listOf("hū"), listOf("yán")),
"壤驷" to listOf(listOf("rǎng"), listOf("sì")),
"夏侯" to listOf(listOf("xià"), listOf("hóu")),
"太叔" to listOf(listOf("tài"), listOf("shū")),
"夹谷" to listOf(listOf("jiá"), listOf("gǔ")),
"子车" to listOf(listOf("zǐ"), listOf("jū")),
"宇文" to listOf(listOf("yǔ"), listOf("wén")),
"宗政" to listOf(listOf("zōng"), listOf("zhèng")),
"宰父" to listOf(listOf("zǎi"), listOf("fǔ")),
"尉迟" to listOf(listOf("yù"), listOf("chí")),
"左丘" to listOf(listOf("zuǒ"), listOf("qiū")),
"巫马" to listOf(listOf("wū"), listOf("mǎ")),
"慕容" to listOf(listOf("mù"), listOf("róng")),
"拓跋" to listOf(listOf("tuò"), listOf("bá")),
"梁丘" to listOf(listOf("liáng"), listOf("qiū")),
"榖梁" to listOf(listOf("gǔ"), listOf("liáng")),
"欧阳" to listOf(listOf("ōu"), listOf("yáng")),
"段干" to listOf(listOf("duàn"), listOf("gān")),
"淳于" to listOf(listOf("chún"), listOf("yú")),
"漆雕" to listOf(listOf("qī"), listOf("diāo")),
"澹台" to listOf(listOf("tán"), listOf("tái")),
"濮阳" to listOf(listOf("pú"), listOf("yáng")),
"申屠" to listOf(listOf("shēn"), listOf("tú")),
"百里" to listOf(listOf("bǎi"), listOf("lǐ")),
"皇甫" to listOf(listOf("huáng"), listOf("pǔ")),
"端木" to listOf(listOf("duān"), listOf("mù")),
"第五" to listOf(listOf("dì"), listOf("wǔ")),
"羊舌" to listOf(listOf("yáng"), listOf("shé")),
"西门" to listOf(listOf("xī"), listOf("mén")),
"诸葛" to listOf(listOf("zhū"), listOf("gě")),
"赫连" to listOf(listOf("hè"), listOf("lián")),
"轩辕" to listOf(listOf("xuān"), listOf("yuán")),
"钟离" to listOf(listOf("zhōng"), listOf("lí")),
"长孙" to listOf(listOf("zhǎng"), listOf("sūn")),
"闻人" to listOf(listOf("wén"), listOf("rén")),
"闾丘" to listOf(listOf("lǘ"), listOf("qiū")),
"颛孙" to listOf(listOf("zhuān"), listOf("sūn")),
"鲜于" to listOf(listOf("xiān"), listOf("yú")),
)
}

View File

@@ -1,178 +1,263 @@
package org.autojs.autojs.runtime.api.augment.pinyin
import net.sourceforge.pinyin4j.PinyinHelper
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType.LOWERCASE
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType.UPPERCASE
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType.WITHOUT_TONE
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType.WITH_TONE_MARK
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType.WITH_TONE_NUMBER
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType.WITH_U_AND_COLON
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType.WITH_U_UNICODE
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType.WITH_V
import com.huaban.analysis.jieba.CharsDictionaryDatabase
import com.huaban.analysis.jieba.JiebaSegmenter
import com.huaban.analysis.jieba.PhrasesDictionaryDatabase
import org.autojs.autojs.annotation.RhinoSingletonFunctionInterface
import org.autojs.autojs.extension.ArrayExtensions.toNativeArray
import org.autojs.autojs.extension.ScriptableExtensions.defineProp
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
import org.autojs.autojs.runtime.api.augment.Augmentable
import org.autojs.autojs.runtime.api.augment.Invokable
import org.autojs.autojs.runtime.api.augment.pinyin.PinyinCore.DefaultOptions
import org.autojs.autojs.runtime.api.augment.pinyin.PinyinCore.PinyinMode
import org.autojs.autojs.runtime.api.augment.pinyin.PinyinCore.PinyinStyle
import org.autojs.autojs.runtime.api.augment.pinyin.PinyinCore.parseStyle
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
import org.autojs.autojs.util.RhinoUtils.coerceString
import org.autojs.autojs.util.RhinoUtils.coerceStringUppercase
import org.autojs.autojs.util.RhinoUtils.newNativeObject
import org.autojs.autojs.util.RhinoUtils.withRhinoContext
import org.mozilla.javascript.BoundFunction
import org.mozilla.javascript.Context
import org.mozilla.javascript.NativeArray
import org.mozilla.javascript.NativeJavaMethod
import org.mozilla.javascript.NativeObject
import org.mozilla.javascript.Scriptable
import org.mozilla.javascript.ScriptableObject.DONTENUM
import org.mozilla.javascript.ScriptableObject.PERMANENT
import org.mozilla.javascript.ScriptableObject.READONLY
object Pinyin : Augmentable(), Invokable {
private const val DEFAULT_SEPARATOR = ""
private val charsDictDatabase by lazy { CharsDictionaryDatabase.getInstance(globalContext).database }
private val phrasesDictDatabase by lazy { PhrasesDictionaryDatabase.getInstance(globalContext).database }
private const val DEFAULT_CASE_TYPE_STRING = "LOWERCASE"
private val DEFAULT_CASE_TYPE = LOWERCASE
private const val DEFAULT_TONE_TYPE_STRING = "WITHOUT_TONE"
private val DEFAULT_TONE_TYPE = WITHOUT_TONE
private const val DEFAULT_V_CHAR_TYPE_STRING = "WITH_V"
private val DEFAULT_V_CHAR_TYPE = WITH_V
private val pinyinToneMap = mapOf(
// 单韵母
"a1" to "ā", "a2" to "á", "a3" to "ǎ", "a4" to "à", "a0" to "a",
"o1" to "ō", "o2" to "ó", "o3" to "ǒ", "o4" to "ò", "o0" to "o",
"e1" to "ē", "e2" to "é", "e3" to "ě", "e4" to "è", "e0" to "e",
"i1" to "ī", "i2" to "í", "i3" to "ǐ", "i4" to "ì", "i0" to "i",
"u1" to "ū", "u2" to "ú", "u3" to "ǔ", "u4" to "ù", "u0" to "u",
"ü1" to "ǖ", "ü2" to "ǘ", "ü3" to "ǚ", "ü4" to "ǜ", "ü0" to "ü",
"v1" to "ǖ", "v2" to "ǘ", "v3" to "ǚ", "v4" to "ǜ", "v0" to "ü",
"u:1" to "ǖ", "u:2" to "ǘ", "u:3" to "ǚ", "u:4" to "ǜ", "u:0" to "ü",
// 复韵母 - 普通
"ai1" to "āi", "ai2" to "ái", "ai3" to "ǎi", "ai4" to "ài", "ai0" to "ai",
"ei1" to "ēi", "ei2" to "éi", "ei3" to "ěi", "ei4" to "èi", "ei0" to "ei",
"ui1" to "uī", "ui2" to "uí", "ui3" to "uǐ", "ui4" to "uì", "ui0" to "ui",
"ao1" to "āo", "ao2" to "áo", "ao3" to "ǎo", "ao4" to "ào", "ao0" to "ao",
"ou1" to "ōu", "ou2" to "óu", "ou3" to "ǒu", "ou4" to "òu", "ou0" to "ou",
"iu1" to "iū", "iu2" to "iú", "iu3" to "iǔ", "iu4" to "iù", "iu0" to "iu",
"ie1" to "iē", "ie2" to "ié", "ie3" to "iě", "ie4" to "iè", "ie0" to "ie",
"üe1" to "üē", "üe2" to "üé", "üe3" to "üě", "üe4" to "üè", "üe0" to "üe",
"u:e1" to "üē", "u:e2" to "üé", "u:e3" to "üě", "u:e4" to "üè", "u:e0" to "üe",
"ve1" to "üē", "ve2" to "üé", "ve3" to "üě", "ve4" to "üè", "ve0" to "üe",
// 复韵母 - 特殊
"er1" to "ēr", "er2" to "ér", "er3" to "ěr", "er4" to "èr", "er0" to "er",
// 复韵母 - 前鼻韵母
"an1" to "ān", "an2" to "án", "an3" to "ǎn", "an4" to "àn", "an0" to "an",
"en1" to "ēn", "en2" to "én", "en3" to "ěn", "en4" to "èn", "en0" to "en",
"in1" to "īn", "in2" to "ín", "in3" to "ǐn", "in4" to "ìn", "in0" to "in",
"un1" to "ūn", "un2" to "ún", "un3" to "ǔn", "un4" to "ùn", "un0" to "un",
"ün1" to "ǖn", "ün2" to "ǘn", "ün3" to "ǚn", "ün4" to "ǜn", "ün0" to "ün",
"u:n1" to "ǖn", "u:n2" to "ǘn", "u:n3" to "ǚn", "u:n4" to "ǜn", "u:n0" to "ün",
"vn1" to "ǖn", "vn2" to "ǘn", "vn3" to "ǚn", "vn4" to "ǜn", "vn0" to "ün",
// 复韵母 - 后鼻韵母
"ang1" to "āng", "ang2" to "áng", "ang3" to "ǎng", "ang4" to "àng", "ang0" to "ang",
"eng1" to "ēng", "eng2" to "éng", "eng3" to "ěng", "eng4" to "èng", "eng0" to "eng",
"ing1" to "īng", "ing2" to "íng", "ing3" to "ǐng", "ing4" to "ìng", "ing0" to "ing",
"ong1" to "ōng", "ong2" to "óng", "ong3" to "ǒng", "ong4" to "òng", "ong0" to "ong"
@Suppress("SpellCheckingInspection")
override val selfAssignmentProperties = listOf(
"STYLE_NORMAL" to PinyinStyle.NORMAL,
"STYLE_TONE" to PinyinStyle.TONE,
"STYLE_TONE2" to PinyinStyle.TONE2,
"STYLE_TO3NE" to PinyinStyle.TO3NE,
"STYLE_INITIALS" to PinyinStyle.INITIALS,
"STYLE_FIRST_LETTER" to PinyinStyle.FIRST_LETTER,
"MODE_NORMAL" to PinyinMode.NORMAL,
"MODE_SURNAME" to PinyinMode.SURNAME,
"MODE_PLACENAME" to PinyinMode.PLACE_NAME,
"MODE_PLACE_NAME" to PinyinMode.PLACE_NAME,
)
override val selfAssignmentFunctions = listOf(
::`as`.name,
::of.name,
::convert.name,
::simple.name,
::compare.name,
::compact.name,
::fromCodePoint.name,
::fromPhrase.name,
)
@RhinoSingletonFunctionInterface
override fun invoke(args: Array<out Any?>): String = ensureArgumentsLengthInRange(args, 1..2) { argList ->
of(argList)
override fun invoke(args: Array<out Any?>): NativeArray = ensureArgumentsLengthInRange(args, 1..2) { argList ->
convert(argList)
}
@RhinoSingletonFunctionInterface
fun of(args: Array<out Any?>): String = ensureArgumentsLengthInRange(args, 1..2) { argList ->
val (string, options) = argList
val str = coerceString(string, "").takeIf { it.isNotEmpty() } ?: return@ensureArgumentsLengthInRange ""
var separator = DEFAULT_SEPARATOR
val opt = when (options) {
is String -> newNativeObject().also { separator = options }
is NativeObject -> options
else -> newNativeObject()
}
separator = opt.inquire(listOf("separator", "sep"), ::coerceString, separator)
val caseType = when (val case = opt.inquire(listOf("case", "caseType"))) {
is HanyuPinyinCaseType -> case
else -> when (coerceStringUppercase(case, DEFAULT_CASE_TYPE_STRING)) {
"LOWERCASE", "LOW", "L", "0" -> LOWERCASE
"UPPERCASE", "UP", "U", "1" -> UPPERCASE
else -> throw IllegalArgumentException("Invalid case type: ${Context.toString(case)}")
fun convert(args: Array<out Any?>): NativeArray = ensureArgumentsLengthInRange(args, 1..2) { argList ->
val (hansArg, options) = argList
val hans = coerceString(hansArg, "")
val opt = options as? NativeObject ?: newNativeObject()
convertRhino(hans, opt).map { it.toNativeArray() }.toNativeArray().also { result ->
val funcName = "compact"
withRhinoContext { cx ->
val thisObj = Context.javaToJS(Util, result) as Scriptable
val boundFunction = BoundFunction(
/* cx = */ cx,
/* scope = */ result,
/* targetFunction = */ NativeJavaMethod(Util::class.java.methods.find { it.name == funcName }, funcName),
/* boundThis = */ thisObj,
/* boundArgs = */ arrayOf(result),
)
result.defineProp(funcName, boundFunction, READONLY or DONTENUM or PERMANENT)
}
}
val toneType = when (val tone = opt.inquire(listOf("tone", "toneType"))) {
is HanyuPinyinToneType -> tone
else -> when (coerceStringUppercase(tone, DEFAULT_TONE_TYPE_STRING)) {
"WITH_TONE_NUMBER", "WITH_NUMBER", "NUMBER", "NUM" -> WITH_TONE_NUMBER
"WITHOUT_TONE", "NO_TONE", "NO", "FALSE", "0" -> WITHOUT_TONE
"WITH_TONE_MARK", "WITH_MARK", "MARK", "TRUE", "1" -> WITH_TONE_MARK
else -> throw IllegalArgumentException("Invalid tone type: ${Context.toString(tone)}")
}
}
val vCharType = when (val vChar = opt.inquire(listOf("v", "vChar", "vCharType"))) {
is HanyuPinyinVCharType -> vChar
else -> when (coerceStringUppercase(vChar, if (toneType == WITH_TONE_MARK) "WITH_U_UNICODE" else DEFAULT_V_CHAR_TYPE_STRING)) {
"WITH_U_AND_COLON", "U_AND_COLON", "U_COLON", "U:" -> WITH_U_AND_COLON
"WITH_V", "V" -> WITH_V
"WITH_U_UNICODE", "U_UNICODE", "UNICODE", "Ü" -> WITH_U_UNICODE
else -> throw IllegalArgumentException("Invalid v char type: ${Context.toString(vChar)}")
}
}
ofRhino(str, separator, caseType, toneType, vCharType)
}
@JvmStatic
@JvmOverloads
fun ofRhino(
str: String,
separator: String = DEFAULT_SEPARATOR,
caseType: HanyuPinyinCaseType = DEFAULT_CASE_TYPE,
toneType: HanyuPinyinToneType = DEFAULT_TONE_TYPE,
vCharType: HanyuPinyinVCharType = DEFAULT_V_CHAR_TYPE,
): String {
val format = HanyuPinyinOutputFormat().also {
it.caseType = caseType
it.toneType = toneType
it.vCharType = vCharType
}
val pinyinResult = StringBuilder()
str.forEach { ch ->
val pinyin = when (ch) {
in '\u4E00'..'\u9FA5' -> {
PinyinHelper.toHanyuPinyinStringArray(ch, format)?.firstOrNull() ?: ""
fun convertRhino(hans: String, opt: NativeObject): List<List<String>> = when {
hans.isEmpty() -> listOf()
else -> {
val phrases: List<String> = when {
opt.inquire("segment", ::coerceBoolean, DefaultOptions.SEGMENT) -> {
segment(hans)
}
else -> ch.toString()
else -> groupChineseAndNonChinese(hans)
}
pinyinResult.append(pinyin).append(separator)
}
var nonHans = ""
return pinyinResult.toString().trim()
when (PinyinMode.SURNAME.value) {
opt.inquire("mode", { o, def -> PinyinCore.parseMode(o) ?: def }, DefaultOptions.MODE.value) -> {
PinyinCore.surnamePinyin(hans, opt)
}
else -> {
val pinyinList = mutableListOf<List<String>>()
for (words in phrases) {
val firstCharCode = words.codePointAt(0)
if (fromCodePointInternal(firstCharCode) != null) {
if (nonHans.isNotEmpty()) {
pinyinList.add(listOf(nonHans))
nonHans = ""
}
val newPinyinList = when (words.length) {
1 -> PinyinCore.convert(words, opt)
else -> phrasePinyin(words, opt)
}
if (opt.inquire("group", ::coerceBoolean, false)) {
pinyinList.add(groupPhrases(newPinyinList))
} else {
pinyinList.addAll(newPinyinList)
}
} else {
nonHans += words
}
}
if (nonHans.isNotEmpty()) {
pinyinList.add(listOf(nonHans))
}
pinyinList
}
}
}
}
@RhinoSingletonFunctionInterface
fun `as`(args: Array<out Any?>): String = ensureArgumentsOnlyOne(args) { o ->
convertToTonePinyin(coerceString(o, ""))
fun compare(args: Array<out Any?>): String = ensureArgumentsLengthInRange(args, 1..2) { argList ->
""
}
private fun convertToTonePinyin(input: String): String {
val regex = Regex("(üe|u:e|ve|ue|ang|eng|ing|ong|ai|ei|ui|ao|ou|iu|ie|er|an|en|in|un|ün|u:n|vn|[aoeiuüv])([0-4])")
return regex.replace(input) { matchResult ->
val pinyinBase = matchResult.groupValues[1]
val toneNumber = matchResult.groupValues[2]
pinyinToneMap[pinyinBase + toneNumber] ?: matchResult.value
@RhinoSingletonFunctionInterface
fun compact(args: Array<out Any?>): String = ensureArgumentsLengthInRange(args, 1..2) { argList ->
""
}
@RhinoSingletonFunctionInterface
fun simple(args: Array<out Any?>): String = ensureArgumentsLengthInRange(args, 1..3) { argList ->
val (str, enableNumericTone, enableSegment) = argList
simpleRhino(coerceString(str, ""), coerceBoolean(enableNumericTone, false), coerceBoolean(enableSegment, false))
}
@JvmOverloads
fun simpleRhino(s: String, enableNumericTone: Boolean = false, enableSegment: Boolean = false): String {
return convertRhino(s, newNativeObject().also {
it.defineProp("style", if (enableNumericTone) PinyinStyle.TONE2 else PinyinStyle.NORMAL)
it.defineProp("segment", enableSegment)
}).map { it.firstOrNull() ?: emptyList<String>() }.joinToString("")
}
@RhinoSingletonFunctionInterface
fun fromCodePoint(args: Array<out Any?>): String? = ensureArgumentsOnlyOne(args) { o ->
fromCodePointInternal(coerceIntNumber(o))
}
@RhinoSingletonFunctionInterface
fun fromPhrase(args: Array<out Any?>): NativeArray = ensureArgumentsOnlyOne(args) { o ->
fromPhraseInternal(coerceString(o)).map { it.toNativeArray() }.toNativeArray()
}
internal fun fromCodePointInternal(codePoint: Int): String? = charsDictDatabase.let { db ->
db.query(
"Dict",
arrayOf("Pinyin"),
"CodePoint = ?",
arrayOf(codePoint.toString()),
null,
null,
null
).use { cursor ->
when {
cursor.moveToFirst() -> cursor.getString(cursor.getColumnIndexOrThrow("Pinyin"))
else -> null
}
}
}
private fun fromPhraseInternal(phrase: String): List<List<String>> = phrasesDictDatabase.let { db ->
val cursor = db.query(
"PhrasesDict",
arrayOf("Pinyin", "OrderIndex"),
"Word = ?",
arrayOf(phrase),
null,
null,
"OrderIndex ASC",
)
val result = mutableListOf<MutableList<String>>()
cursor.use {
while (it.moveToNext()) {
val pinyin = it.getString(it.getColumnIndexOrThrow("Pinyin"))
val orderIndex = it.getInt(it.getColumnIndexOrThrow("OrderIndex"))
while (result.size < orderIndex) {
result.add(mutableListOf())
}
result[orderIndex - 1].add(pinyin)
}
}
result
}
private fun segment(hans: String): List<String> {
return JiebaSegmenter(globalContext).cutSmall(hans, 4)
}
private fun phrasePinyin(phrase: String, options: NativeObject): List<List<String>> {
val phrasePinyinList = fromPhraseInternal(phrase)
return when {
phrasePinyinList.isNotEmpty() -> {
val style = options.inquire("style", { o, def -> parseStyle(o) ?: def }, DefaultOptions.STYLE.value)
phrasePinyinList.map { item ->
if (options.inquire("heteronym", ::coerceBoolean, DefaultOptions.HETERONYM)) {
item.map { pyItem -> PinyinCore.toFixed(pyItem, style) }.toMutableList()
} else {
mutableListOf(PinyinCore.toFixed(item[0], style))
}
}.toMutableList()
}
else -> PinyinCore.convert(phrase, options)
}
}
private fun groupPhrases(phrases: List<List<String>>): List<String> {
return when (phrases.size) {
1 -> phrases[0]
else -> Util.combo(phrases)
}
}
private fun groupChineseAndNonChinese(input: String): List<String> {
if (input.isEmpty()) return emptyList()
val result = mutableListOf<String>()
val chineseRegex = "\\p{IsHan}".toRegex() // 匹配中文字符的正则
var tempGroup = StringBuilder()
var isLastChinese = chineseRegex.matches(input[0].toString()) // 判断第一个字符是否中文
for (char in input) {
val isChinese = chineseRegex.matches(char.toString())
// 如果字符类型(中文或非中文)发生切换,保存当前分组
if (isChinese != isLastChinese) {
result.add(tempGroup.toString()) // 添加当前分组
tempGroup = StringBuilder() // 重置分组
}
tempGroup.append(char) // 将字符添加到当前分组
isLastChinese = isChinese // 更新当前字符类型
}
// 添加最后一个分组
if (tempGroup.isNotEmpty()) {
result.add(tempGroup.toString())
}
return result
}
}

View File

@@ -0,0 +1,282 @@
package org.autojs.autojs.runtime.api.augment.pinyin
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
import org.autojs.autojs.extension.AnyExtensions.jsUnwrapped
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
import org.autojs.autojs.util.RhinoUtils.coerceString
import org.autojs.autojs.util.RhinoUtils.newNativeObject
import org.mozilla.javascript.NativeObject
import java.util.regex.Pattern
object PinyinCore {
private val SURNAME = Dict.SURNAME
private val COMPOUND_SURNAME = Dict.COMPOUND_SURNAME
// 声母表
private val INITIALS = arrayOf(
"b", "p", "m", "f", "d", "t", "n", "l",
"g", "k", "h", "j", "q", "x",
"zh", "ch", "sh", "r", "z", "c", "s",
)
// 韵母表
private val FINALS = arrayOf(
"a", "o", "e", "i", "u", "v",
"ai", "ei", "ui", "ao", "ou", "iu", "ie", "ve", "er",
"an", "en", "in", "un", "vn",
"ang", "eng", "ing", "ong",
)
// 带声调字符
private val PHONETIC_SYMBOL = Dict.PHONETIC_SYMBOL
private val RE_PHONETIC_SYMBOL = Pattern.compile("([${PHONETIC_SYMBOL.keys.joinToString("")}])").toRegex()
private val RE_TONE2 = Regex("([aeoiuvnm])([0-4])$")
/**
* @param hansArg 要转为拼音的目标字符串 (汉字).
* @param options 可选, 用于指定拼音风格, 是否启用多音字.
* @return 返回的拼音列表.
*/
fun convert(hansArg: String, options: NativeObject = newNativeObject()): MutableList<List<String>> {
val pinyinList = mutableListOf<List<String>>()
val hans = coerceString(hansArg, "").trim()
if (hans.isNotEmpty()) {
var nonHans = ""
when (PinyinMode.SURNAME.value) {
options.inquire("mode", { o, def -> parseMode(o) ?: def }, DefaultOptions.MODE.value) -> {
pinyinList.addAll(surnamePinyin(hans, options))
}
else -> hans.forEach { char ->
when {
Pinyin.fromCodePointInternal(char.code) != null -> {
// 拼音转换
if (nonHans.isNotEmpty()) {
pinyinList.add(listOf(nonHans))
nonHans = ""
}
pinyinList.add(singlePinyin(char.toString(), options))
}
else -> nonHans += char
}
}
}
if (nonHans.isNotEmpty()) pinyinList.add(listOf(nonHans))
}
return pinyinList
}
/**
* 比较两个汉字转成拼音后的排序顺序, 可以用作默认的拼音排序算法.
*
* @param hanA 汉字字符串 A.
* @param hanB 汉字字符串 B.
* @return -1, 0 或 1.
*/
fun compare(hanA: String, hanB: String): Int {
return coerceString(convert(hanA)).compareTo(coerceString(convert(hanB)))
}
fun compact(arr: List<List<String>>) = Util.compact(arr)
/**
* 单字拼音转换.
*
* @param han 单个汉字.
* @param options 选项.
* @return 返回拼音列表, 多音字会有多个拼音项.
*/
private fun singlePinyin(han: String, options: NativeObject): List<String> {
require(han.isNotEmpty()) { "Argument han must not be empty" }
val hanCode = han.first().code
val consulted = Pinyin.fromCodePointInternal(hanCode)
val pinyinList = consulted?.split(",") ?: return listOf(han.first().toString())
val style = options.inquire("style", { o, def -> parseStyle(o) ?: def }, DefaultOptions.STYLE.value)
return when {
options.inquire("heteronym", ::coerceBoolean, DefaultOptions.HETERONYM) -> {
pinyinList.map { toFixed(it, style) }.distinct()
}
else -> listOf(toFixed(pinyinList.first(), style))
}
}
/**
* 单姓处理, 将单个姓氏转换为拼音.
*
* @param hans 姓氏字符串.
* @param options 转换选项 (包括风格、多音字选项等).
* @return 转换后的拼音列表.
*/
private fun singleSurname(hans: String, options: NativeObject): List<List<String>> {
val result = mutableListOf<List<String>>()
// 遍历所有汉字
hans.forEach { char ->
val word = char.toString()
when {
// 如果是已知的姓氏, 从预定义数据中获取拼音, 并格式化
SURNAME.containsKey(word) -> SURNAME[word]?.map { toFixedList(it, options) }?.let { surnamePinyin ->
result.addAll(surnamePinyin)
}
// 如果是未知姓氏, 则按一般规则转换为拼音
else -> result.add(singlePinyin(word, options))
}
}
return result
}
/**
* 姓名转换为拼音.
*/
fun surnamePinyin(hans: String, options: NativeObject): List<List<String>> {
return compoundSurname(hans, options)
}
/**
* 复姓处理, 将复姓转换为拼音.
*
* @param hans 包含复姓的字符串.
* @param options 转换选项 (包含拼音风格等).
* @return 转换后的拼音列表.
*/
private fun compoundSurname(hans: String, options: NativeObject): List<List<String>> {
val len = hans.length
var prefixIndex = 0
val result = mutableListOf<List<String>>()
var i = 0
while (i < len) {
// 获取两个连续字符, 检测是否是复姓
val twoWords = hans.substring(i, (i + 2).coerceAtMost(len))
if (!COMPOUND_SURNAME.containsKey(twoWords)) {
i++
continue
}
// 如果前缀内容存在 (非复姓部分), 处理非复姓部分
if (prefixIndex < i) {
val nonCompoundPart = hans.substring(prefixIndex, i)
result.addAll(singleSurname(nonCompoundPart, options)) // 调用单姓处理函数
}
// 处理复姓部分, 并格式化为指定风格
COMPOUND_SURNAME[twoWords]?.map { toFixedList(it, options) }?.let { compoundPart ->
result.addAll(compoundPart)
}
// 跳过复姓的两个字
i += 2
prefixIndex = i
}
// 处理复姓最后剩下的部分 (即非复姓部分)
if (prefixIndex < len) {
val remaining = hans.substring(prefixIndex, len)
result.addAll(singleSurname(remaining, options))
}
return result
}
/**
* 格式化拼音风格.
*
* @param pinyin TONE 风格的拼音.
* @param style 目标转换的拼音风格.
* @return 转换后的拼音.
*/
internal fun toFixed(pinyin: String, style: Int): String = when (style) {
PinyinStyle.NORMAL.value -> pinyin.replace(RE_PHONETIC_SYMBOL) { matchResult ->
PHONETIC_SYMBOL[matchResult.groupValues[1]]?.replace(RE_TONE2, "$1") ?: matchResult.value
}
PinyinStyle.INITIALS.value -> initials(pinyin)
PinyinStyle.FIRST_LETTER.value -> pinyin.first().toString()
PinyinStyle.TONE.value -> pinyin
PinyinStyle.TONE2.value -> {
var tone = ""
val py = pinyin.replace(RE_PHONETIC_SYMBOL) { matchResult ->
PHONETIC_SYMBOL[matchResult.groupValues[1]]?.let { s ->
s.replace(RE_TONE2, "$2").also { tone = it }
s.replace(RE_TONE2, "$1")
} ?: matchResult.value
}
py + tone
}
PinyinStyle.TO3NE.value -> pinyin.replace(RE_PHONETIC_SYMBOL) { matchResult ->
PHONETIC_SYMBOL[matchResult.groupValues[1]] ?: matchResult.value
}
else -> pinyin
}
private fun toFixedList(items: List<String>, options: NativeObject): List<String> {
val style = options.inquire("style", { o, def -> parseStyle(o) ?: def }, DefaultOptions.STYLE.value)
return items.map { toFixed(it, style) }
}
/**
* 格式化拼音为声母 (Initials) 形式.
*/
private fun initials(pinyin: String) = INITIALS.find { pinyin.startsWith(it) } ?: ""
internal fun parseMode(o: Any): Int? {
val obj = o.jsUnwrapped()
return when {
obj.isJsNullish() -> null
obj is PinyinMode -> obj.value
obj is Int -> {
require(PinyinMode.entries.any { it.value == obj }) { "Invalid mode constant: $obj" }
obj
}
else -> coerceString(obj, "").uppercase().let { name ->
PinyinMode.entries.find { it.name == name }?.value ?: throw IllegalArgumentException("Invalid mode name: $obj")
}
}
}
internal fun parseStyle(o: Any): Int? {
val obj = o.jsUnwrapped()
return when {
obj.isJsNullish() -> null
obj is PinyinStyle -> obj.value
obj is Int -> {
require(PinyinStyle.entries.any { it.value == obj }) { "Invalid style constant: $obj" }
obj
}
else -> coerceString(obj, "").uppercase().let { name ->
PinyinStyle.entries.find { it.name == name }?.value ?: throw IllegalArgumentException("Invalid style name: $obj")
}
}
}
enum class PinyinStyle(val value: Int) {
NORMAL(0), // 普通风格, 不带声调
TONE(1), // 标准风格, 声调在韵母上
TONE2(2), // 数字形式标记声调
TO3NE(5), // 数字方式, 声母后加声调
INITIALS(3), // 仅保留声母
FIRST_LETTER(4), // 仅保留首字母
}
@Suppress("SpellCheckingInspection")
enum class PinyinMode(val value: Int) {
NORMAL(0), // 普通模式
SURNAME(1), // 姓氏模式
PLACE_NAME(2), // 地名模式
PLACENAME(2), // 地名模式 (兼容)
}
object DefaultOptions {
val MODE = PinyinMode.NORMAL // 默认模式
val STYLE = PinyinStyle.TONE // 默认风格
const val SEGMENT: Boolean = false // 是否分词
const val HETERONYM: Boolean = false // 是否允许多音字
}
}

View File

@@ -0,0 +1,89 @@
package org.autojs.autojs.runtime.api.augment.pinyin
import org.autojs.autojs.util.RhinoUtils.coerceString
/**
* 工具类 Util, 用于实现拼音转换时辅助功能.
*/
@Suppress("SpellCheckingInspection")
object Util {
/**
* 合并二维字符串数组为一维字符串数组.
*
* @param arr 二维数组, 如 [["zhāo", "cháo"], ["yáng"], ["dōng"], ["shēng"]]
* @return 合并后的字符串数组, 一维结果, 如 ["zhāoyáng", "cháoyáng", "dōng", "shēng"]
*/
fun combo(arr: List<List<String>>): List<String> = when {
arr.size <= 1 -> arr.firstOrNull() ?: emptyList()
else -> {
var result = combo2array(arr[0], arr[1])
for (i in 2 until arr.size) {
result = combo2array(result, arr[i])
}
result
}
}
/**
* 组合两个拼音数组为一维字符串数组.
*
* @param a1 第一个拼音数组, 如 ["zhāo", "cháo"]
* @param a2 第二个拼音数组, 如 ["yáng"]
* @return 组合后的一维字符串数组, 如 ["zhāoyáng", "cháoyáng"]
*/
private fun combo2array(a1: List<String>, a2: List<String>): List<String> {
return when {
a1.isEmpty() -> a2
a2.isEmpty() -> a1
else -> a1.flatMap { item1 -> a2.map { item2 -> item1 + item2 } }
}
}
/**
* 合并多个拼音数组, 形成一个新的二维数组.
* 每两个拼音元素形成一个元组数组.
*
* @param arr 二维数组(每组拼音数组)
* @return 合并后的二维拼音数组, 如 [[hai, qian], [huan, qian]]
*/
fun compact(arr: List<List<String>>): List<List<String>> = when {
arr.isEmpty() -> emptyList()
arr.size == 1 -> arr.map { listOf(it).flatten() }
else -> {
var result = compact2array(arr[0], arr[1])
for (i in 2 until arr.size) {
result = compact2array(result, arr[i])
}
result
}
}
/**
* 组合两个拼音数组, 形成一个新的二维数组.
*
* @param a1 第一个拼音数组 eg: ["hai", "huan"] / [["hai"], ["huan"]]
* @param a2 第二个拼音数组 eg: ["qian"]
* @return 组合后的二维数组, 如 [["hai", "qian"], ["huan", "qian"]]
*/
private fun compact2array(a1: List<Any>, a2: List<String>): List<List<String>> = when {
a1.isEmpty() -> listOf(a2)
a2.isEmpty() -> a1.map { item1 ->
when (item1) {
is List<*> -> item1.map { coerceString(it) }
else -> listOf(item1.toString())
}
}
else -> {
val result = mutableListOf<List<String>>()
a1.forEach { item1 ->
val list1 = when (item1) {
is List<*> -> item1.map { coerceString(it) }
else -> listOf(coerceString(item1))
}
a2.mapTo(result) { list1 + it }
}
result
}
}
}

View File

@@ -0,0 +1,178 @@
package org.autojs.autojs.runtime.api.augment.pinyin4j
import net.sourceforge.pinyin4j.PinyinHelper
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType.LOWERCASE
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType.UPPERCASE
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType.WITHOUT_TONE
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType.WITH_TONE_MARK
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType.WITH_TONE_NUMBER
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType.WITH_U_AND_COLON
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType.WITH_U_UNICODE
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType.WITH_V
import org.autojs.autojs.annotation.RhinoSingletonFunctionInterface
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
import org.autojs.autojs.runtime.api.augment.Augmentable
import org.autojs.autojs.runtime.api.augment.Invokable
import org.autojs.autojs.util.RhinoUtils.coerceString
import org.autojs.autojs.util.RhinoUtils.coerceStringUppercase
import org.autojs.autojs.util.RhinoUtils.newNativeObject
import org.mozilla.javascript.Context
import org.mozilla.javascript.NativeObject
object Pinyin4j : Augmentable(), Invokable {
private const val DEFAULT_SEPARATOR = ""
private const val DEFAULT_CASE_TYPE_STRING = "LOWERCASE"
private val DEFAULT_CASE_TYPE = LOWERCASE
private const val DEFAULT_TONE_TYPE_STRING = "WITHOUT_TONE"
private val DEFAULT_TONE_TYPE = WITHOUT_TONE
private const val DEFAULT_V_CHAR_TYPE_STRING = "WITH_V"
private val DEFAULT_V_CHAR_TYPE = WITH_V
private val pinyinToneMap = mapOf(
// 单韵母
"a1" to "ā", "a2" to "á", "a3" to "ǎ", "a4" to "à", "a0" to "a",
"o1" to "ō", "o2" to "ó", "o3" to "ǒ", "o4" to "ò", "o0" to "o",
"e1" to "ē", "e2" to "é", "e3" to "ě", "e4" to "è", "e0" to "e",
"i1" to "ī", "i2" to "í", "i3" to "ǐ", "i4" to "ì", "i0" to "i",
"u1" to "ū", "u2" to "ú", "u3" to "ǔ", "u4" to "ù", "u0" to "u",
"ü1" to "ǖ", "ü2" to "ǘ", "ü3" to "ǚ", "ü4" to "ǜ", "ü0" to "ü",
"v1" to "ǖ", "v2" to "ǘ", "v3" to "ǚ", "v4" to "ǜ", "v0" to "ü",
"u:1" to "ǖ", "u:2" to "ǘ", "u:3" to "ǚ", "u:4" to "ǜ", "u:0" to "ü",
// 复韵母 - 普通
"ai1" to "āi", "ai2" to "ái", "ai3" to "ǎi", "ai4" to "ài", "ai0" to "ai",
"ei1" to "ēi", "ei2" to "éi", "ei3" to "ěi", "ei4" to "èi", "ei0" to "ei",
"ui1" to "uī", "ui2" to "uí", "ui3" to "uǐ", "ui4" to "uì", "ui0" to "ui",
"ao1" to "āo", "ao2" to "áo", "ao3" to "ǎo", "ao4" to "ào", "ao0" to "ao",
"ou1" to "ōu", "ou2" to "óu", "ou3" to "ǒu", "ou4" to "òu", "ou0" to "ou",
"iu1" to "iū", "iu2" to "iú", "iu3" to "iǔ", "iu4" to "iù", "iu0" to "iu",
"ie1" to "iē", "ie2" to "ié", "ie3" to "iě", "ie4" to "iè", "ie0" to "ie",
"üe1" to "üē", "üe2" to "üé", "üe3" to "üě", "üe4" to "üè", "üe0" to "üe",
"u:e1" to "üē", "u:e2" to "üé", "u:e3" to "üě", "u:e4" to "üè", "u:e0" to "üe",
"ve1" to "üē", "ve2" to "üé", "ve3" to "üě", "ve4" to "üè", "ve0" to "üe",
// 复韵母 - 特殊
"er1" to "ēr", "er2" to "ér", "er3" to "ěr", "er4" to "èr", "er0" to "er",
// 复韵母 - 前鼻韵母
"an1" to "ān", "an2" to "án", "an3" to "ǎn", "an4" to "àn", "an0" to "an",
"en1" to "ēn", "en2" to "én", "en3" to "ěn", "en4" to "èn", "en0" to "en",
"in1" to "īn", "in2" to "ín", "in3" to "ǐn", "in4" to "ìn", "in0" to "in",
"un1" to "ūn", "un2" to "ún", "un3" to "ǔn", "un4" to "ùn", "un0" to "un",
"ün1" to "ǖn", "ün2" to "ǘn", "ün3" to "ǚn", "ün4" to "ǜn", "ün0" to "ün",
"u:n1" to "ǖn", "u:n2" to "ǘn", "u:n3" to "ǚn", "u:n4" to "ǜn", "u:n0" to "ün",
"vn1" to "ǖn", "vn2" to "ǘn", "vn3" to "ǚn", "vn4" to "ǜn", "vn0" to "ün",
// 复韵母 - 后鼻韵母
"ang1" to "āng", "ang2" to "áng", "ang3" to "ǎng", "ang4" to "àng", "ang0" to "ang",
"eng1" to "ēng", "eng2" to "éng", "eng3" to "ěng", "eng4" to "èng", "eng0" to "eng",
"ing1" to "īng", "ing2" to "íng", "ing3" to "ǐng", "ing4" to "ìng", "ing0" to "ing",
"ong1" to "ōng", "ong2" to "óng", "ong3" to "ǒng", "ong4" to "òng", "ong0" to "ong"
)
override val selfAssignmentFunctions = listOf(
::`as`.name,
::of.name,
)
@RhinoSingletonFunctionInterface
override fun invoke(args: Array<out Any?>): String = ensureArgumentsLengthInRange(args, 1..2) { argList ->
of(argList)
}
@RhinoSingletonFunctionInterface
fun of(args: Array<out Any?>): String = ensureArgumentsLengthInRange(args, 1..2) { argList ->
val (string, options) = argList
val str = coerceString(string, "").takeIf { it.isNotEmpty() } ?: return@ensureArgumentsLengthInRange ""
var separator = DEFAULT_SEPARATOR
val opt = when (options) {
is String -> newNativeObject().also { separator = options }
is NativeObject -> options
else -> newNativeObject()
}
separator = opt.inquire(listOf("separator", "sep"), ::coerceString, separator)
val caseType = when (val case = opt.inquire(listOf("case", "caseType"))) {
is HanyuPinyinCaseType -> case
else -> when (coerceStringUppercase(case, DEFAULT_CASE_TYPE_STRING)) {
"LOWERCASE", "LOW", "L", "0" -> LOWERCASE
"UPPERCASE", "UP", "U", "1" -> UPPERCASE
else -> throw IllegalArgumentException("Invalid case type: ${Context.toString(case)}")
}
}
val toneType = when (val tone = opt.inquire(listOf("tone", "toneType"))) {
is HanyuPinyinToneType -> tone
else -> when (coerceStringUppercase(tone, DEFAULT_TONE_TYPE_STRING)) {
"WITH_TONE_NUMBER", "WITH_NUMBER", "NUMBER", "NUM" -> WITH_TONE_NUMBER
"WITHOUT_TONE", "NO_TONE", "NO", "FALSE", "0" -> WITHOUT_TONE
"WITH_TONE_MARK", "WITH_MARK", "MARK", "TRUE", "1" -> WITH_TONE_MARK
else -> throw IllegalArgumentException("Invalid tone type: ${Context.toString(tone)}")
}
}
val vCharType = when (val vChar = opt.inquire(listOf("v", "vChar", "vCharType"))) {
is HanyuPinyinVCharType -> vChar
else -> when (coerceStringUppercase(vChar, if (toneType == WITH_TONE_MARK) "WITH_U_UNICODE" else DEFAULT_V_CHAR_TYPE_STRING)) {
"WITH_U_AND_COLON", "U_AND_COLON", "U_COLON", "U:" -> WITH_U_AND_COLON
"WITH_V", "V" -> WITH_V
"WITH_U_UNICODE", "U_UNICODE", "UNICODE", "Ü" -> WITH_U_UNICODE
else -> throw IllegalArgumentException("Invalid v char type: ${Context.toString(vChar)}")
}
}
ofRhino(str, separator, caseType, toneType, vCharType)
}
@JvmStatic
@JvmOverloads
fun ofRhino(
str: String,
separator: String = DEFAULT_SEPARATOR,
caseType: HanyuPinyinCaseType = DEFAULT_CASE_TYPE,
toneType: HanyuPinyinToneType = DEFAULT_TONE_TYPE,
vCharType: HanyuPinyinVCharType = DEFAULT_V_CHAR_TYPE,
): String {
val format = HanyuPinyinOutputFormat().also {
it.caseType = caseType
it.toneType = toneType
it.vCharType = vCharType
}
val pinyinResult = StringBuilder()
str.forEach { ch ->
val pinyin4j = when (ch) {
in '\u4E00'..'\u9FA5' -> {
PinyinHelper.toHanyuPinyinStringArray(ch, format)?.firstOrNull() ?: ""
}
else -> ch.toString()
}
pinyinResult.append(pinyin4j).append(separator)
}
return pinyinResult.toString().trim()
}
@RhinoSingletonFunctionInterface
fun `as`(args: Array<out Any?>): String = ensureArgumentsOnlyOne(args) { o ->
convertToTonePinyin(coerceString(o, ""))
}
private fun convertToTonePinyin(input: String): String {
val regex = Regex("(üe|u:e|ve|ue|ang|eng|ing|ong|ai|ei|ui|ao|ou|iu|ie|er|an|en|in|un|ün|u:n|vn|[aoeiuüv])([0-4])")
return regex.replace(input) { matchResult ->
val pinyinBase = matchResult.groupValues[1]
val toneNumber = matchResult.groupValues[2]
pinyinToneMap[pinyinBase + toneNumber] ?: matchResult.value
}
}
}

View File

@@ -161,7 +161,7 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
val key = coerceString(keyArg)
value?.let { mProperties[key] = it } ?: mProperties.remove(key)
}, NOT_CONSTRUCTABLE))
}, PERMANENT or READONLY)
}, READONLY or PERMANENT)
}
@Suppress("SameReturnValue")
@@ -599,11 +599,11 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
private fun initWidgetConstructor(ctor: BaseFunction) {
val prototype = ctor.prop("prototype") as? ScriptableObject ?: newNativeObject()
assignWidgetProperties(prototype)
ctor.defineProp("prototype", prototype, PERMANENT or DONTENUM)
ctor.defineProp("prototype", prototype, DONTENUM or PERMANENT)
}
private fun assignWidgetProperties(o: ScriptableObject) {
o.defineProperty("__attrs__", newNativeObject(), PERMANENT or DONTENUM)
o.defineProperty("__attrs__", newNativeObject(), DONTENUM or PERMANENT)
o.defineFunctionProperties(
arrayOf(
"renderInternal",

View File

@@ -277,7 +277,7 @@ object Util : Augmentable() {
else -> withRhinoContext { context ->
val tmp = object : BaseFunction() {
override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable?, args: Array<out Any?>) = newNativeObject().also {
it.defineProperty("constructor", d, READONLY or PERMANENT or DONTENUM)
it.defineProperty("constructor", d, READONLY or DONTENUM or PERMANENT)
}
}
tmp.construct(context, ImporterTopLevel(context), arrayOf()).also { instance ->
@@ -718,7 +718,7 @@ object Util : Augmentable() {
it.defineFunctionProperties(
arrayOf("toString"),
RegularFunction::class.java,
READONLY or PERMANENT or DONTENUM,
READONLY or DONTENUM or PERMANENT,
)
}
}

View File

@@ -19,6 +19,9 @@ import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager
import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.huaban.analysis.jieba.CharsDictionaryDatabase
import com.huaban.analysis.jieba.PhrasesDictionaryDatabase
import com.huaban.analysis.jieba.WordDictionaryDatabase
import org.autojs.autojs.AutoJs
import org.autojs.autojs.app.FragmentPagerAdapterBuilder
import org.autojs.autojs.app.FragmentPagerAdapterBuilder.StoredFragmentPagerAdapter
@@ -230,12 +233,20 @@ class MainActivity : BaseActivity(), DelegateHost, HostActivity {
fun exitCompletely() {
FloatyWindowManger.hideCircularMenuAndSaveState()
stopService(Intent(this, FloatyService::class.java))
stopService(Intent(this, ScreenCapturerForegroundService::class.java))
AutoJs.instance.scriptEngineService.stopAll()
WordDictionaryDatabase.getInstance(this).close()
CharsDictionaryDatabase.getInstance(this).close()
PhrasesDictionaryDatabase.getInstance(this).close()
mA11yTool.stopService(false)
mForeGroundService.stopIfNeeded()
WrappedShizuku.onDestroy()
Process.killProcess(Process.myPid())
}

View File

@@ -234,8 +234,8 @@ public class TaskListRecyclerView extends ThemeColorRecyclerView {
mTask = task;
firstCharIconBinding.firstChar
.setIcon(AutoFileSource.ENGINE.equals(mTask.getEngineName()) ? FileUtils.TYPE.AUTO.icon : FileUtils.TYPE.JAVASCRIPT.icon)
// .setIconTextThemeColor()
// .setStrokeThemeColor()
.setIconTextThemeColor()
.setStrokeThemeColor()
.setFillTransparent();
itemBinding.name.setText(task.getName());
itemBinding.taskListFilePath.setText(task.getDesc());

View File

@@ -56,7 +56,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.regex.Pattern;
@@ -115,8 +114,11 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
EditText mOutputPath;
EditText mAppName;
EditText mPackageName;
TextInputLayout mPackageNameParent;
EditText mVersionName;
TextInputLayout mVersionNameParent;
EditText mVersionCode;
TextInputLayout mVersionCodeParent;
ImageView mIcon;
LinearLayout mAppConfig;
@@ -181,11 +183,15 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
}
return false;
});
mPackageNameParent = binding.packageNameParent;
mVersionName = binding.versionName;
mVersionNameParent = binding.versionNameParent;
mVersionCode = binding.versionCode;
mVersionCodeParent = binding.versionCodeParent;
mIcon = binding.appIcon;
mIcon.setVisibility(View.INVISIBLE);
mIcon.setOnClickListener(v -> selectIcon());
mAppConfig = binding.appConfig;
@@ -261,11 +267,11 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
NotAskAgainDialog.Builder builder = new NotAskAgainDialog.Builder(context, key);
builder.title(R.string.text_prompt);
builder.content(getString(R.string.text_unable_to_build_apk_as_autojs6_does_not_include_selected_abi, abiText) + "\n\n" +
getString(R.string.text_the_following_solutions_can_be_referred_to) + ":\n\n" +
"- " + getString(R.string.text_download_and_install_autojs6_including_above_abi, abiText) + "\n" +
"- " + getString(R.string.text_download_and_install_autojs6_including_all_abis) + " [" + getString(R.string.text_recommended) + "]\n\n" +
getString(R.string.text_download_link_for_autojs6) + ":\n" +
getString(R.string.uri_autojs6_download_link));
getString(R.string.text_the_following_solutions_can_be_referred_to) + ":\n\n" +
"- " + getString(R.string.text_download_and_install_autojs6_including_above_abi, abiText) + "\n" +
"- " + getString(R.string.text_download_and_install_autojs6_including_all_abis) + " [" + getString(R.string.text_recommended) + "]\n\n" +
getString(R.string.text_download_link_for_autojs6) + ":\n" +
getString(R.string.uri_autojs6_download_link));
builder.positiveText(R.string.dialog_button_dismiss);
MaterialDialog dialog = builder.show();
if (dialog != null) {
@@ -360,6 +366,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
return list.stream().anyMatch(item -> item.equalsIgnoreCase(s));
}
@SuppressLint("CheckResult")
private void setupWithSourceFile(ScriptFile file) {
String dir = file.getParent();
if (dir != null && dir.startsWith(getFilesDir().getPath())) {
@@ -368,20 +375,42 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
mOutputPath.setText(dir);
mAppName.setText(file.getSimplifiedName());
String packageName = getString(R.string.format_default_package_name, generatePackageNameSuffix(file));
mPackageName.setText(packageName);
Observable.fromCallable(() -> {
String packageNameSuffix = generatePackageNameSuffix(file);
return getString(R.string.format_default_package_name, packageNameSuffix);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(packageName -> {
mPackageName.setText(packageName);
mPackageNameParent.setHint(R.string.text_package_name);
SimpleVersionInfo nextVersionInfo = AppUtils.generateNextVersionInfo(packageName);
if (nextVersionInfo != null) {
mVersionName.setText(nextVersionInfo.versionName);
mVersionCode.setText(nextVersionInfo.versionCodeString);
}
SimpleVersionInfo nextVersionInfo = AppUtils.generateNextVersionInfo(packageName);
if (nextVersionInfo != null) {
mVersionName.setText(nextVersionInfo.versionName);
mVersionCode.setText(nextVersionInfo.versionCodeString);
} else {
mVersionName.setText(R.string.default_build_apk_version_name);
mVersionCode.setText(R.string.default_build_apk_version_code);
}
mVersionNameParent.setHint(R.string.text_version_name);
mVersionCodeParent.setHint(R.string.text_version_code);
Drawable iconDrawable = AppUtils.getInstalledAppIcon(packageName);
if (iconDrawable != null) {
mIcon.setImageDrawable(iconDrawable);
mIsDefaultIcon = false;
}
Drawable iconDrawable = AppUtils.getInstalledAppIcon(packageName);
if (iconDrawable != null) {
mIcon.setImageDrawable(iconDrawable);
mIsDefaultIcon = false;
}
mIcon.setVisibility(View.VISIBLE);
}, throwable -> {
mPackageName.setText(getString(R.string.format_default_package_name, file.getSimplifiedName().toLowerCase(Language.getPrefLanguage().getLocale())));
mVersionName.setText(R.string.default_build_apk_version_name);
mVersionCode.setText(R.string.default_build_apk_version_code);
mPackageNameParent.setHint(R.string.text_package_name);
mVersionNameParent.setHint(R.string.text_version_name);
mVersionCodeParent.setHint(R.string.text_version_code);
mIcon.setVisibility(View.VISIBLE);
});
setSource(file);
}
@@ -389,7 +418,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
private static String generatePackageNameSuffix(ScriptFile file) {
String name = file.getSimplifiedName();
if (name.matches(".*[\\u4e00-\\u9fff].*")) {
name = Pinyin.ofRhino(name);
// name = Pinyin4j.ofRhino(name);
name = Pinyin.INSTANCE.simpleRhino(name, false, true);
}
name = name.replaceAll("\\W+", "_");
if (name.matches("^\\d.*")) {
@@ -461,11 +491,11 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
return checkNotEmpty(mOutputPath);
}
return checkNotEmpty(mSourcePath)
& checkNotEmpty(mOutputPath)
& checkNotEmpty(mAppName)
& checkNotEmpty(mVersionCode)
& checkNotEmpty(mVersionName)
& checkPackageNameValid(mPackageName);
& checkNotEmpty(mOutputPath)
& checkNotEmpty(mAppName)
& checkNotEmpty(mVersionCode)
& checkNotEmpty(mVersionName)
& checkPackageNameValid(mPackageName);
}
private boolean checkAbis() {
@@ -482,9 +512,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
private boolean checkPackageNameValid(EditText editText) {
Editable text = editText.getText();
String hint = Objects.requireNonNull(((TextInputLayout) editText.getParent().getParent()).getHint()).toString();
if (TextUtils.isEmpty(text)) {
editText.setError(hint + getString(R.string.text_should_not_be_empty));
editText.setError(getString(R.string.text_should_not_be_empty));
return false;
}
if (!REGEX_PACKAGE_NAME.matcher(text).matches()) {
@@ -495,15 +524,11 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
}
private boolean checkNotEmpty(EditText editText) {
if (!TextUtils.isEmpty(editText.getText()) || !editText.isShown())
return true;
// TODO by Stardust on Dec 8, 2017.
// ! More beautiful ways?
// ! zh-CN (translated by SuperMonster003 on Jul 29, 2024):
// ! 更优雅的方式?
String hint = Objects.requireNonNull(((TextInputLayout) editText.getParent().getParent()).getHint()).toString();
editText.setError(hint + getString(R.string.text_should_not_be_empty));
return false;
if (TextUtils.isEmpty(editText.getText()) && editText.isShown()) {
editText.setError(getString(R.string.text_should_not_be_empty));
return false;
}
return true;
}
private void showHintDialogIfNeeded() {
@@ -586,18 +611,18 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
private boolean isWideCharacter(char c) {
Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
return block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS ||
block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A ||
block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B ||
block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS ||
block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT ||
block == Character.UnicodeBlock.HIRAGANA ||
block == Character.UnicodeBlock.KATAKANA ||
block == Character.UnicodeBlock.HANGUL_SYLLABLES ||
block == Character.UnicodeBlock.HANGUL_JAMO ||
block == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO ||
block == Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS ||
block == Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS ||
block == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS;
block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A ||
block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B ||
block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS ||
block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT ||
block == Character.UnicodeBlock.HIRAGANA ||
block == Character.UnicodeBlock.KATAKANA ||
block == Character.UnicodeBlock.HANGUL_SYLLABLES ||
block == Character.UnicodeBlock.HANGUL_JAMO ||
block == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO ||
block == Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS ||
block == Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS ||
block == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS;
}
@SuppressWarnings("ResultOfMethodCallIgnored")

View File

@@ -2,6 +2,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -156,10 +157,11 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/package_name_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/text_package_name"
android:hint="@string/text_package_name_with_computing"
app:expandedHintEnabled="false">
<!-- @Hint by SuperMonster003 on Nov 18, 2024. -->
@@ -188,10 +190,11 @@
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/version_name_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/text_version_name"
android:hint="@string/text_version_name_with_computing"
app:expandedHintEnabled="false">
<androidx.appcompat.widget.AppCompatEditText
@@ -199,15 +202,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:text="1.0.0" />
tools:text="1.0.0" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/version_code_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/text_version_code"
android:hint="@string/text_version_code_with_computing"
app:expandedHintEnabled="false">
<androidx.appcompat.widget.AppCompatEditText
@@ -215,7 +219,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="1" />
tools:text="1" />
</com.google.android.material.textfield.TextInputLayout>

View File

@@ -811,5 +811,8 @@
<string name="text_write_secure_settings_description">إعدادات النظام الآمنة ، التي تحتوي على تفضيلات النظام التي يمكن أن تقرأها التطبيقات ولكن لا يُسمح لها بالكتابة.\nهذه هي لتفضيلات يجب على المستخدم تعديلها بشكل صريح من خلال واجهة المستخدم لتطبيق النظام.\nمع إذن إعدادات النظام الآمن ، يمكن للتطبيقات العادية تعديل الإعدادات الآمنة مباشرة (مثل خدمة إمكانية الوصول).</string>
<string name="text_write_system_settings">كتابة إعدادات النظام</string>
<string name="theme_color_default">تقصير</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">اسم الحزمة (جارٍ الحساب...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">اسم الإصدار (جارٍ الحساب...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">رمز الإصدار (جارٍ الحساب...)</string>
</resources>

View File

@@ -807,5 +807,8 @@
<string name="text_write_secure_settings_description">Secure system settings, containing system preferences that applications can read but are not allowed to write.\nThese are for preferences that the user must explicitly modify through the UI of a system app.\nWith secure system settings permission, normal applications can directly modify the secure settings (such as accessibility service).</string>
<string name="text_write_system_settings">Write system settings</string>
<string name="theme_color_default">Default</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">Package name (computing...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">Version name (computing...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">Version code (computing...)</string>
</resources>

View File

@@ -810,5 +810,8 @@
<string name="text_write_secure_settings_description">Ajustes de seguridad del sistema, que contienen preferencias del sistema que las aplicaciones pueden leer pero no pueden escribir.\nSe trata de preferencias que el usuario debe modificar explícitamente a través de la interfaz de usuario de una aplicación del sistema.\nCon el permiso de configuración segura del sistema, las aplicaciones normales pueden modificar directamente la configuración segura (como el servicio de accesibilidad).</string>
<string name="text_write_system_settings">Escribir la configuración del sistema</string>
<string name="theme_color_default">Por defecto</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">Nombre del paquete (Calculando...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">Nombre de la versión (Calculando...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">Código de la versión (Calculando...)</string>
</resources>

View File

@@ -810,5 +810,8 @@
<string name="text_write_secure_settings_description">Paramètres de sécurité du système, contenant les préférences du système que les applications peuvent lire mais ne sont pas autorisées à écrire.\nIl s\'agit des préférences que l\'utilisateur doit explicitement modifier par le biais de l\'interface utilisateur d\'une application système.\nAvec l\'autorisation de paramètres de sécurité du système, les applications normales peuvent directement modifier les paramètres de sécurité (comme le service d\'accessibilité).</string>
<string name="text_write_system_settings">Écrire les paramètres système</string>
<string name="theme_color_default">Default</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">Nom du paquet (en cours de calcul...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">Nom de la version (en cours de calcul...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">Code de version (en cours de calcul...)</string>
</resources>

View File

@@ -810,5 +810,8 @@
<string name="text_write_secure_settings_description">アプリケーションが読み取ることはできるが, 書き込むことはできないシステム環境設定を含む, 安全なシステム設定です.\nこれは, ユーザーがシステムアプリのUIを通じて明示的に変更する必要がある環境設定のためのものです.\nセキュアなシステム設定を許可すると, 通常のアプリケーションはセキュアな設定 (アクセシビリティサービスなど) を直接変更できるようになります</string>
<string name="text_write_system_settings">システム設定の書き込み</string>
<string name="theme_color_default">デフォルト</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">パッケージ名 (計算中...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">バージョン名 (計算中...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">バージョンコード (計算中...)</string>
</resources>

View File

@@ -811,5 +811,8 @@
<string name="text_write_secure_settings_description">애플리케이션이 읽을 수 있지만 쓸 수없는 시스템 환경 설정을 포함하는 보안 시스템 설정.\n이들은 사용자가 시스템 앱의 UI를 통해 명시 적으로 수정 해야하는 선호도입니다.\n보안 시스템 설정 권한을 사용하면 일반 애플리케이션이 보안 설정 (예: 접근성 서비스)을 직접 수정할 수 있습니다.</string>
<string name="text_write_system_settings">시스템 설정을 작성하십시오</string>
<string name="theme_color_default">기본</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">패키지 이름 (계산 중...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">버전 이름 (계산 중...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">버전 코드 (계산 중...)</string>
</resources>

View File

@@ -810,5 +810,8 @@
<string name="text_write_secure_settings_description">Настройки безопасности системы, содержащие системные предпочтения, которые приложения могут читать, но не имеют права записывать.\nОни предназначены для параметров, которые пользователь должен явно изменить через пользовательский интерфейс системного приложения.\nПри наличии разрешения на безопасные системные настройки обычные приложения могут напрямую изменять безопасные настройки (например, служба доступности).</string>
<string name="text_write_system_settings">Запись системных настроек</string>
<string name="theme_color_default">По умолчанию</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">Имя пакета (Вычисление...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">Название версии (Вычисление...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">Код версии (Вычисление...)</string>
</resources>

View File

@@ -809,5 +809,8 @@
<string name="text_write_secure_settings_description">安全設置包含應用程序可讀但不可寫入的設置選項, 這些選項只能由 UI 或系統級別應用修改.\n被授予 \"修改安全設置權限\" 後, 普通應用可直接修改上述安全設置 (例如無障礙服務).</string>
<string name="text_write_system_settings">修改系統設置</string>
<string name="theme_color_default">默認</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">包名 (計算中...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">版本名稱 (計算中...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">版本號 (計算中...)</string>
</resources>

View File

@@ -809,5 +809,8 @@
<string name="text_write_secure_settings_description">安全設定包含應用程式可讀但不可寫入的設定選項, 這些選項只能由 UI 或系統級別應用修改.\n被授予 \"修改安全設定許可權\" 後, 普通應用可直接修改上述安全設定 (例如無障礙服務).</string>
<string name="text_write_system_settings">修改系統設定</string>
<string name="theme_color_default">預設</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">包名 (計算中...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">版本名稱 (計算中...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">版本號 (計算中...)</string>
</resources>

View File

@@ -805,5 +805,8 @@
<string name="text_write_secure_settings_description">安全设置包含应用程序可读但不可写入的设置选项, 这些选项只能由 UI 或系统级别应用修改.\n被授予 \"修改安全设置权限\" 后, 普通应用可直接修改上述安全设置 (例如无障碍服务).</string>
<string name="text_write_system_settings">修改系统设置</string>
<string name="theme_color_default">默认</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">包名 (计算中...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">版本名称 (计算中...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">版本号 (计算中...)</string>
</resources>

View File

@@ -1030,5 +1030,10 @@
<string name="value_app_language_english" translatable="false">EN</string>
<string name="value_app_language_follow_system" translatable="false">Auto</string>
<string name="value_app_language_simplified_chinese" translatable="false">ZH_CN</string>
<string name="text_package_name_with_computing" tools:ignore="TypographyEllipsis">Package name (computing...)</string>
<string name="text_version_name_with_computing" tools:ignore="TypographyEllipsis">Version name (computing...)</string>
<string name="text_version_code_with_computing" tools:ignore="TypographyEllipsis">Version code (computing...)</string>
<string name="default_build_apk_version_name" translatable="false">1.0.0</string>
<string name="default_build_apk_version_code" translatable="false">1</string>
</resources>

View File

@@ -1,6 +1,6 @@
#Fri Nov 29 20:40:57 CST 2024
#Mon Dec 23 16:08:32 CST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

191
jieba-analysis/LICENSE Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

157
jieba-analysis/README.md Normal file
View File

@@ -0,0 +1,157 @@
结巴分词(java版) jieba-analysis
===============================
首先感谢jieba分词原作者[fxsjy](https://github.com/fxsjy),没有他的无私贡献,我们也不会结识到结巴
分词. 同时也感谢jieba分词java版本的实现团队[huaban](https://github.com/huaban),他们的努力使得Java也能直接做出效果很棒的分词。
不过由于huaban已经没有再对java版进行维护,所以我自己对项目进行了开发。除了结巴分词(java版)所保留的原项目针对搜索引擎分词的功能(cut~forindex~、cut~forsearch~),我加入了tfidf的关键词提取功能,并且实现的效果和python的jieba版本的效果一模一样!
(以下内容在基于[jieba-java版本README.md](https://github.com/huaban/jieba-analysis])的基础上,加入了对我新加入的tfidf关键词提取模块的相关说明)
***
简介
====
支持分词模式
------------
- Search模式,用于对用户查询词分词
- Index模式,用于对索引文档分词
特性
----
- 支持多种分词模式
- 全角统一转成半角
- 用户词典功能
- conf 目录有整理的搜狗细胞词库
- 因为性能原因,最新的快照版本去除词性标注,也希望有更好的 Pull
Request 可以提供该功能。
**新特性:tfidf算法提取关键词**
```{.java}
public static void main(String[] args)
{
String content="孩子上了幼儿园 安全防拐教育要做好";
int topN=5;
TFIDFAnalyzer tfidfAnalyzer=new TFIDFAnalyzer();
List<Keyword> list=tfidfAnalyzer.analyze(content,topN);
for(Keyword word:list)
System.out.println(word.getName()+":"+word.getTfidfvalue()+",");
// 防拐:0.1992,幼儿园:0.1434,做好:0.1065,教育:0.0946,安全:0.0924
}
```
<!-- 如何获取
========
- 当前稳定版本
``` {.xml}
<dependency>
<groupId>com.huaban</groupId>
<artifactId>jieba-analysis</artifactId>
<version>1.0.2</version>
</dependency>
```
- 当前快照版本
``` {.xml}
<dependency>
<groupId>com.huaban</groupId>
<artifactId>jieba-analysis</artifactId>
<version>1.0.3-SNAPSHOT</version>
</dependency>
``` -->
如何使用
========
- Demo
``` {.java}
@Test
public void testDemo() {
JiebaSegmenter segmenter = new JiebaSegmenter();
String[] sentences =
new String[] {"这是一个伸手不见五指的黑夜。我叫孙悟空,我爱北京,我爱Python和C++。", "我不喜欢日本和服。", "雷猴回归人间。",
"工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作", "结果婚的和尚未结过婚的"};
for (String sentence : sentences) {
System.out.println(segmenter.process(sentence, SegMode.INDEX).toString());
}
}
```
算法(wiki补充...)
=================
- \[ \] 基于 `trie` 树结构实现高效词图扫描
- \[ \] 生成所有切词可能的有向无环图 `DAG`
- \[ \] 采用动态规划算法计算最佳切词组合
- \[ \] 基于 `HMM` 模型,采用 `Viterbi` (维特比)算法实现未登录词识别
性能评估
========
- 测试机配置
``` {.screen}
Processor 2 Intel(R) Pentium(R) CPU G620 @ 2.60GHz
Memory:8GB
分词测试时机器开了许多应用(eclipse、emacs、chrome...),可能
会影响到测试速度
```
- *测试文本*
- 测试结果(单线程,对测试文本逐行分词,并循环调用上万次)
``` {.screen}
循环调用一万次
第一次测试结果:
time elapsed:12373, rate:2486.986533kb/s, words:917319.94/s
第二次测试结果:
time elapsed:12284, rate:2505.005241kb/s, words:923966.10/s
第三次测试结果:
time elapsed:12336, rate:2494.445880kb/s, words:920071.30/s
循环调用2万次
第一次测试结果:
time elapsed:22237, rate:2767.593144kb/s, words:1020821.12/s
第二次测试结果:
time elapsed:22435, rate:2743.167762kb/s, words:1011811.87/s
第三次测试结果:
time elapsed:22102, rate:2784.497726kb/s, words:1027056.34/s
统计结果:词典加载时间1.8s左右,分词效率每秒2Mb多,近100万词。
2 Processor Intel(R) Core(TM) i3-2100 CPU @ 3.10GHz
12G 测试效果
time elapsed:19597, rate:3140.428063kb/s, words:1158340.52/s
time elapsed:20122, rate:3058.491639kb/s, words:1128118.44/s
```
使用本库项目
============
- [analyzer-solr](https://github.com/sing1ee/analyzer-solr) @sing1ee
许可证
======
jieba(python版本)的许可证为MIT,jieba(java版本)的许可证为ApacheLicence
2.0
``` {.screen}
Copyright (C) 2013 Huaban Inc
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
```

View File

@@ -0,0 +1,59 @@
plugins {
id 'com.android.library' // Android Library 插件
id 'kotlin-android' // Kotlin Android 插件
}
android {
namespace = "com.huaban.jieba"
compileSdk = project.ext.compileSdk
defaultConfig {
minSdk = project.ext.minSdk
targetSdk = project.ext.targetSdk
group = 'com.huaban'
versionName = '1.0.3-SNAPSHOT (Optimized for AutoJs6)'
description = '结巴分词工具(jieba for java)'
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
sourceSets {
main {
// 主 Java/Kotlin 目录
java.srcDirs = ['src/main/java']
// 将资源文件夹指定为 assets 目录
assets.srcDirs = ['src/main/resources']
}
}
}
repositories {
mavenCentral()
google()
}
dependencies {
// implementation "org.jetbrains.kotlin:kotlin-stdlib:${project.ext.kotlinVersion}"
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'org.apache.commons:commons-lang3:3.16.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}

View File

@@ -0,0 +1,35 @@
package com.huaban.analysis.jieba
import java.util.regex.Pattern
object CharacterUtil {
var reSkip: Pattern = Pattern.compile("(\\d+\\.\\d+|[a-zA-Z0-9]+)")
private val connectors = charArrayOf('+', '#', '&', '.', '_', '-')
internal fun isChineseLetter(ch: Char) = ch.code in 0x4E00..0x9FA5
internal fun isEnglishLetter(ch: Char) = ch.code in 0x0041..0x005A || ch.code in 0x0061..0x007A
internal fun isDigit(ch: Char) = ch.code in 0x0030..0x0039
private fun isConnector(ch: Char) = connectors.contains(ch)
fun ccFind(ch: Char) = isChineseLetter(ch) || isEnglishLetter(ch) || isDigit(ch) || isConnector(ch)
/**
* 全角 to 半角,大写 to 小写
*
* @param input
* 输入字符
* @return 转换后的字符
*/
fun regularize(input: Char) = when (input.code) {
12288 -> 32.toChar()
in 65281..65374 -> (input.code - 65248).toChar()
in 'A'.code..'Z'.code -> input + 32
else -> input
}
}

View File

@@ -0,0 +1,19 @@
package com.huaban.analysis.jieba
import android.content.Context
open class CharsDictionaryDatabase private constructor(context: Context) : DictionaryDatabase(context) {
override val databaseName = "dict-chinese-chars.db"
companion object {
@Volatile
private var instance: CharsDictionaryDatabase? = null
fun getInstance(context: Context): CharsDictionaryDatabase = instance ?: synchronized(this) {
instance ?: CharsDictionaryDatabase(context.applicationContext).also { instance = it }
}
}
}

View File

@@ -0,0 +1,250 @@
package com.huaban.analysis.jieba
import java.util.*
import java.util.concurrent.ConcurrentHashMap
/**
* 词典树分段,表示词典树的一个分枝
*/
internal class DictSegment(
// 当前节点上存储的字符
private val nodeChar: Char,
) : Comparable<DictSegment> {
private val childrenSegment: MutableMap<Char, DictSegment> by lazy { ConcurrentHashMap() }
// 数组方式存储结构
private var childrenArray: Array<DictSegment?>? = null
// 当前节点存储的 Segment 数目
// storeSize <= ARRAY_LENGTH_LIMIT, 使用数组存储
// storeSize > ARRAY_LENGTH_LIMIT, 则使用 Map 存储
private var storeSize = 0
// 当前 DictSegment 状态
// 默认 0, 1 表示从根节点到当前节点的路径表示一个词
private var nodeState = 0
// 判断是否有下一个节点
private fun hasNextNode() = this.storeSize > 0
/**
* 匹配词段
*/
@JvmOverloads
fun match(charArray: CharArray, begin: Int = 0, length: Int = charArray.size, searchHit: Hit? = null): Hit {
var niceSearchHit = searchHit
if (niceSearchHit == null) {
// 如果hit为空,新建
niceSearchHit = Hit()
// 设置hit的其实文本位置
niceSearchHit.begin = begin
} else {
// 否则要将HIT状态重置
niceSearchHit.setUnmatch()
}
// 设置hit的当前处理位置
niceSearchHit.end = begin
val keyChar = charArray[begin]
var ds: DictSegment? = null
// 引用实例变量为本地变量,避免查询时遇到更新的同步问题
val segmentArray = this.childrenArray
val segmentMap: Map<Char, DictSegment> = this.childrenSegment
// STEP1 在节点中查找keyChar对应的DictSegment
if (segmentArray != null) {
// 在数组中查找
val keySegment = DictSegment(keyChar)
val position = Arrays.binarySearch(segmentArray, 0, this.storeSize, keySegment)
if (position >= 0) {
ds = segmentArray[position]
}
} else {
// 在map中查找
ds = segmentMap[keyChar]
}
// STEP2 找到DictSegment,判断词的匹配状态,是否继续递归,还是返回结果
if (ds != null) {
if (length > 1) {
// 词未匹配完,继续往下搜索
return ds.match(charArray, begin + 1, length - 1, niceSearchHit)
} else if (length == 1) {
// 搜索最后一个char
if (ds.nodeState == 1) {
// 添加HIT状态为完全匹配
niceSearchHit.setMatch()
}
if (ds.hasNextNode()) {
// 添加HIT状态为前缀匹配
niceSearchHit.setPrefix()
// 记录当前位置的DictSegment
niceSearchHit.matchedDictSegment = ds
}
return niceSearchHit
}
}
// STEP3 没有找到DictSegment, 将HIT设置为不匹配
return niceSearchHit
}
/**
* 加载填充词典片段
*/
private fun fillSegment(charArray: CharArray) {
this.fillSegment(charArray, 0, charArray.size, 1)
}
/**
* 屏蔽词典中的一个词
*/
@Suppress("unused")
fun disableSegment(charArray: CharArray) {
this.fillSegment(charArray, 0, charArray.size, 0)
}
/**
* 加载填充词典片段
*/
@Synchronized
private fun fillSegment(charArray: CharArray, begin: Int, length: Int, enabled: Int) {
// 获取字典表中的汉字对象
val beginChar = charArray[begin]
var keyChar = charMap[beginChar]
// 字典中没有该字,则将其添加入字典
if (keyChar == null) {
charMap[beginChar] = beginChar
keyChar = beginChar
}
// 搜索当前节点的存储,查询对应keyChar的keyChar,如果没有则创建
val ds = lookforSegment(keyChar, enabled)
if (ds != null) {
// 处理keyChar对应的segment
if (length > 1) {
// 词元还没有完全加入词典树
ds.fillSegment(charArray, begin + 1, length - 1, enabled)
} else if (length == 1) {
// 已经是词元的最后一个char,设置当前节点状态为enabled,
// enabled=1表明一个完整的词,enabled=0表示从词典中屏蔽当前词
ds.nodeState = enabled
}
}
}
/**
* 查找本节点下对应的keyChar的segment *
*
* @param keyChar
* @param create
* =1如果没有找到,则创建新的segment ; =0如果没有找到,不创建,返回null
* @return
*/
private fun lookforSegment(keyChar: Char, create: Int): DictSegment? {
var ds: DictSegment? = null
if (this.storeSize <= ARRAY_LENGTH_LIMIT) {
// 获取数组容器,如果数组未创建则创建数组
val segmentArray = getChildrenArray()
// 搜寻数组
val keySegment = DictSegment(keyChar)
val position = Arrays.binarySearch(segmentArray, 0, this.storeSize, keySegment)
if (position >= 0) {
ds = segmentArray[position]
}
// 遍历数组后没有找到对应的segment
if (ds == null && create == 1) {
ds = keySegment
if (this.storeSize < ARRAY_LENGTH_LIMIT) {
// 数组容量未满,使用数组存储
segmentArray[storeSize] = ds
// segment数目+1
storeSize++
Arrays.sort(segmentArray, 0, this.storeSize)
} else {
// 数组容量已满,切换Map存储
// 获取Map容器,如果Map未创建,则创建Map
val segmentMap = getChildrenMap()
// 将数组中的segment迁移到Map中
migrate(segmentArray, segmentMap)
// 存储新的segment
segmentMap[keyChar] = ds
// segment数目+1 , 必须在释放数组前执行storeSize++ , 确保极端情况下,不会取到空的数组
storeSize++
// 释放当前的数组引用
this.childrenArray = null
}
}
} else {
// 获取Map容器,如果Map未创建,则创建Map
val segmentMap = getChildrenMap()
// 搜索Map
ds = segmentMap[keyChar]
if (ds == null && create == 1) {
// 构造新的segment
ds = DictSegment(keyChar)
segmentMap[keyChar] = ds
// 当前节点存储segment数目+1
storeSize++
}
}
return ds
}
/**
* 获取数组容器 线程同步方法
*/
private fun getChildrenArray(): Array<DictSegment?> {
if (this.childrenArray == null) {
synchronized(this) {
if (this.childrenArray == null) {
this.childrenArray = arrayOfNulls(ARRAY_LENGTH_LIMIT)
}
}
}
return childrenArray!!
}
private fun getChildrenMap(): MutableMap<Char, DictSegment> {
return childrenSegment
}
/**
* 将数组中的segment迁移到Map中以支持高性能存储
*/
private fun migrate(segmentArray: Array<DictSegment?>, segmentMap: MutableMap<Char, DictSegment>) {
for (segment in segmentArray) {
if (segment != null) {
segmentMap[segment.nodeChar] = segment
}
}
}
/**
* 实现Comparable接口
*/
override fun compareTo(other: DictSegment): Int {
// 对当前节点存储的char进行比较
return nodeChar.compareTo(other.nodeChar)
}
fun fillSegments(words: List<String>) {
for (word in words) {
fillSegment(word.toCharArray())
}
}
companion object {
// 公用字典表,存储汉字
private val charMap: MutableMap<Char, Char> = HashMap(16, 0.95f)
// 数组大小上限
private const val ARRAY_LENGTH_LIMIT = 3
}
}

View File

@@ -0,0 +1,80 @@
package com.huaban.analysis.jieba
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.security.MessageDigest
import java.util.zip.GZIPInputStream
abstract class DictionaryDatabase internal constructor(context: Context): Closeable {
abstract val databaseName: String
private var shouldForciblyCopyDatabase = false
private val compressedDatabaseName: String
get() = "$databaseName.gzip"
private val md5Key: String
get() = "database_md5_$databaseName"
val database: SQLiteDatabase by lazy {
copyDatabaseWithCompressionAndMd5(context)
SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).absolutePath, null, SQLiteDatabase.OPEN_READONLY)
}
override fun close() = database.close()
private fun copyDatabaseWithCompressionAndMd5(context: Context) {
val dbFile = File(context.getDatabasePath(databaseName).path)
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val storedMd5 = prefs.getString(md5Key, null)
val currentMd5 by lazy { dbFile.md5() }
// 如果数据库文件存在,检查 MD5
if (dbFile.exists()) {
if (storedMd5 != null && storedMd5 == currentMd5) {
if (!shouldForciblyCopyDatabase) return // 文件有效,无需复制或解压
}
}
// 数据文件不存在或 MD5 不匹配,重新复制
dbFile.parentFile?.mkdirs()
context.assets.open(compressedDatabaseName).use { compressedInputStream ->
GZIPInputStream(compressedInputStream).use { gzipStream -> // 解压
FileOutputStream(dbFile).use { outputStream ->
gzipStream.copyTo(outputStream)
}
}
}
// 保存 MD5 值
prefs.edit().putString(md5Key, currentMd5).apply()
}
companion object {
private const val PREF_NAME = "dict_prefs"
}
/**
* 计算文件的 MD5
*/
private fun File.md5(): String = inputStream().use { it.md5() }
private fun InputStream.md5(): String {
val md = MessageDigest.getInstance("MD5")
val buffer = ByteArray(1024)
var read: Int
while (this.read(buffer).also { read = it } != -1) {
md.update(buffer, 0, read)
}
val digest = md.digest()
return digest.joinToString("") { "%02x".format(it) }
}
}

View File

@@ -0,0 +1,117 @@
/**
*
* IK 中文分词 版本 5.0
* IK Analyzer release 5.0
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* 源代码由林良益(linliangyi2005@gmail.com)提供
* 版权声明 2012,乌龙茶工作室
* provided by Linliangyi and copyright 2012 by Oolong studio
*
*/
package com.huaban.analysis.jieba;
/**
* 表示一次词典匹配的命中
*/
public class Hit {
//Hit不匹配
private static final int UNMATCH = 0x00000000;
//Hit完全匹配
private static final int MATCH = 0x00000001;
//Hit前缀匹配
private static final int PREFIX = 0x00000010;
//该HIT当前状态,默认未匹配
private int hitState = UNMATCH;
//记录词典匹配过程中,当前匹配到的词典分支节点
private DictSegment matchedDictSegment;
/*
* 词段开始位置
*/
private int begin;
/*
* 词段的结束位置
*/
private int end;
/**
* 判断是否完全匹配
*/
public boolean isMatch() {
return (this.hitState & MATCH) > 0;
}
/**
*
*/
public void setMatch() {
this.hitState = this.hitState | MATCH;
}
/**
* 判断是否是词的前缀
*/
public boolean isPrefix() {
return (this.hitState & PREFIX) > 0;
}
/**
*
*/
public void setPrefix() {
this.hitState = this.hitState | PREFIX;
}
/**
* 判断是否是不匹配
*/
public boolean isUnmatch() {
return this.hitState == UNMATCH ;
}
/**
*
*/
public void setUnmatch() {
this.hitState = UNMATCH;
}
public DictSegment getMatchedDictSegment() {
return matchedDictSegment;
}
public void setMatchedDictSegment(DictSegment matchedDictSegment) {
this.matchedDictSegment = matchedDictSegment;
}
public int getBegin() {
return begin;
}
public void setBegin(int begin) {
this.begin = begin;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
}

View File

@@ -0,0 +1,211 @@
package com.huaban.analysis.jieba
import android.content.Context
import com.huaban.analysis.jieba.viterbi.FinalSeg
import java.util.*
@Suppress("LocalVariableName")
class JiebaSegmenter(context: Context) {
val dictionary by lazy { WordDictionary.getInstance(context) }
private val finalSeg = FinalSeg.instance
enum class SegMode { INDEX, SEARCH }
private fun createDAG(sentence: String): Map<Int, MutableList<Int>> {
val dag = HashMap<Int, MutableList<Int>>()
val trie = dictionary.trie
val chars: CharArray = sentence.toCharArray()
val N = chars.size
var i = 0
var j = 0
while (i < N) {
val hit = trie.match(chars, i, j - i + 1)
if (hit.isPrefix || hit.isMatch) {
if (hit.isMatch) {
if (!dag.containsKey(i)) {
val value: MutableList<Int> = ArrayList()
dag[i] = value
value.add(j)
} else dag[i]!!.add(j)
}
j += 1
if (j >= N) {
i += 1
j = i
}
} else {
i += 1
j = i
}
}
i = 0
while (i < N) {
if (!dag.containsKey(i)) {
val value: MutableList<Int> = ArrayList()
value.add(i)
dag[i] = value
}
++i
}
return dag
}
private fun calc(sentence: String, dag: Map<Int, MutableList<Int>>): Map<Int, Pair<Int>?> {
val N = sentence.length
val route = HashMap<Int, Pair<Int>?>()
route[N] = Pair(0, 0.0)
for (i in N - 1 downTo -1 + 1) {
var candidate: Pair<Int>? = null
for (x in dag[i]!!) {
val freq = dictionary.getFreq(sentence.substring(i, x + 1)) + route[x + 1]!!.freq
if (null == candidate) {
candidate = Pair(x, freq)
} else if (candidate.freq < freq) {
candidate.freq = freq
candidate.key = x
}
}
route[i] = candidate
}
return route
}
fun process(paragraph: String, mode: SegMode): List<SegToken> {
val tokens: MutableList<SegToken> = ArrayList()
var sb = StringBuilder()
var offset = 0
for (i in paragraph.indices) {
val ch = CharacterUtil.regularize(paragraph[i])
when {
CharacterUtil.ccFind(ch) -> sb.append(ch)
else -> {
if (sb.isNotEmpty()) {
// process
when (mode) {
SegMode.SEARCH -> {
for (word in sentenceProcess(sb.toString())) {
tokens.add(SegToken(word, offset, word.length.let { offset += it; offset }))
}
}
else -> {
for (token in sentenceProcess(sb.toString())) {
if (token.length > 2) {
var gram2: String?
var j = 0
while (j < token.length - 1) {
gram2 = token.substring(j, j + 2)
if (dictionary.containsWord(gram2)) tokens.add(SegToken(gram2, offset + j, offset + j + 2))
++j
}
}
if (token.length > 3) {
var gram3: String?
var j = 0
while (j < token.length - 2) {
gram3 = token.substring(j, j + 3)
if (dictionary.containsWord(gram3)) tokens.add(SegToken(gram3, offset + j, offset + j + 3))
++j
}
}
tokens.add(SegToken(token, offset, token.length.let { offset += it; offset }))
}
}
}
sb = StringBuilder()
offset = i
}
if (dictionary.containsWord(paragraph.substring(i, i + 1))) tokens.add(SegToken(paragraph.substring(i, i + 1), offset, ++offset))
else tokens.add(SegToken(paragraph.substring(i, i + 1), offset, ++offset))
}
}
}
if (sb.isNotEmpty()) when (mode) {
SegMode.SEARCH -> {
sentenceProcess(sb.toString()).mapTo(tokens) { token -> SegToken(token, offset, token.length.let { offset += it; offset }) }
}
else -> sentenceProcess(sb.toString()).forEach { token ->
if (token.length > 2) {
var gram2: String?
var j = 0
while (j < token.length - 1) {
gram2 = token.substring(j, j + 2)
if (dictionary.containsWord(gram2)) tokens.add(SegToken(gram2, offset + j, offset + j + 2))
++j
}
}
if (token.length > 3) {
var gram3: String?
var j = 0
while (j < token.length - 2) {
gram3 = token.substring(j, j + 3)
if (dictionary.containsWord(gram3)) tokens.add(SegToken(gram3, offset + j, offset + j + 3))
++j
}
}
tokens.add(SegToken(token, offset, token.length.let { offset += it; offset }))
}
}
return tokens
}
private fun sentenceProcess(sentence: String): List<String> {
val tokens: MutableList<String> = ArrayList()
val N = sentence.length
val dag = createDAG(sentence)
val route = calc(sentence, dag)
var x = 0
var y: Int
var buf: String
var sb = StringBuilder()
while (x < N) {
y = route[x]!!.key + 1
val lWord: String = sentence.substring(x, y)
when {
y - x == 1 -> sb.append(lWord)
else -> {
if (sb.isNotEmpty()) {
buf = sb.toString()
sb = StringBuilder()
when (buf.length) {
1 -> tokens.add(buf)
else -> when {
dictionary.containsWord(buf) -> tokens.add(buf)
else -> finalSeg.cut(buf, tokens)
}
}
}
tokens.add(lWord)
}
}
x = y
}
buf = sb.toString()
if (buf.isNotEmpty()) {
when (buf.length) {
1 -> tokens.add(buf)
else -> when {
dictionary.containsWord(buf) -> tokens.add(buf)
else -> finalSeg.cut(buf, tokens)
}
}
}
return tokens
}
fun cutSmall(hans: String, limit: Int): List<String> = when {
hans.isEmpty() || limit <= 0 -> emptyList()
else -> process(hans, SegMode.SEARCH)
.map { token -> token.word }
.flatMap { word ->
when {
word.length > limit -> word.chunked(limit)
else -> listOf(word)
}
}
}
}

View File

@@ -0,0 +1,24 @@
package com.huaban.analysis.jieba;
/**
* @description: enable output content to be controlled by switch
* @author: sharkdoodoo@foxmail.com
* @date: 2022/6/21
*/
public class Log {
private static final boolean LOG_ENABLE = Boolean.parseBoolean(System.getProperty("jieba.log.enable", "true"));
public static final void debug(String debugInfo) {
if (LOG_ENABLE) {
System.out.println(debugInfo);
}
}
public static final void error(String errorInfo) {
if (LOG_ENABLE) {
System.err.println(errorInfo);
}
}
}

View File

@@ -0,0 +1,13 @@
package com.huaban.analysis.jieba;
public class Node {
public Character value;
public Node parent;
public Node(Character value, Node parent) {
this.value = value;
this.parent = parent;
}
}

View File

@@ -0,0 +1,20 @@
package com.huaban.analysis.jieba;
import org.jetbrains.annotations.NotNull;
public class Pair<K> {
public K key;
public Double freq;
public Pair(K key, double freq) {
this.key = key;
this.freq = freq;
}
@NotNull
@Override
public String toString() {
return "Candidate [key=" + key + ", freq=" + freq + "]";
}
}

View File

@@ -0,0 +1,20 @@
package com.huaban.analysis.jieba
import android.content.Context
open class PhrasesDictionaryDatabase private constructor(context: Context): DictionaryDatabase(context) {
override val databaseName = "dict-chinese-phrases.db"
companion object {
@Volatile
private var instance: PhrasesDictionaryDatabase? = null
fun getInstance(context: Context): PhrasesDictionaryDatabase = instance ?: synchronized(this) {
instance ?: PhrasesDictionaryDatabase(context.applicationContext).also { instance = it }
}
}
}

View File

@@ -0,0 +1,22 @@
package com.huaban.analysis.jieba;
public class SegToken {
public String word;
public int startOffset;
public int endOffset;
public SegToken(String word, int startOffset, int endOffset) {
this.word = word;
this.startOffset = startOffset;
this.endOffset = endOffset;
}
@Override
public String toString() {
return "[" + word + ", " + startOffset + ", " + endOffset + "]";
}
}

View File

@@ -0,0 +1,94 @@
package com.huaban.analysis.jieba
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlin.properties.Delegates
class WordDictionary(context: Context) {
private val db: SQLiteDatabase by lazy { WordDictionaryDatabase.getInstance(context).database }
private var minFreq by Delegates.notNull<Double>()
internal var trie = DictSegment(0.toChar())
private set
init {
db.rawQuery("SELECT value FROM metadata WHERE `key` = 'min_freq' LIMIT 1", null).use {
minFreq = if (it.moveToFirst()) it.getDouble(0) else Double.MAX_VALUE
}
// db.rawQuery("SELECT word FROM dictionary", null).use { cursor ->
// trie.fillSegments(generateSequence {
// if (cursor.moveToNext()) cursor.getString(cursor.getColumnIndexOrThrow("word")) else null
// })
// }
loadTrieConcurrently()
}
private fun loadTrieConcurrently() = runBlocking {
val chunkSize = 10000
val deferredResults = mutableListOf<Deferred<Unit>>()
db.rawQuery("SELECT word FROM dictionary", null).use { cursor ->
val words = mutableListOf<String>()
while (cursor.moveToNext()) {
val word = cursor.getString(cursor.getColumnIndexOrThrow("word"))
words.add(word)
if (words.size >= chunkSize) {
val chunk = words.toList()
words.clear()
deferredResults.add(async(Dispatchers.Default) {
trie.fillSegments(chunk)
})
}
}
// 最后一块数据
if (words.isNotEmpty()) {
val chunk = words.toList()
deferredResults.add(async(Dispatchers.Default) {
trie.fillSegments(chunk)
})
}
}
// 等待所有分块完成
deferredResults.awaitAll()
}
@Suppress("unused")
fun resetDict() {
trie = DictSegment(0.toChar())
}
fun containsWord(word: String?): Boolean {
if (word.isNullOrEmpty()) return false
db.rawQuery("SELECT 1 FROM dictionary WHERE word = ? LIMIT 1", arrayOf(word)).use {
return it.count > 0
}
}
fun getFreq(key: String?): Double {
if (!key.isNullOrEmpty()) {
db.rawQuery("SELECT normalized_freq FROM dictionary WHERE word = ? LIMIT 1", arrayOf(key)).use {
if (it.moveToFirst()) {
return it.getDouble(it.getColumnIndexOrThrow("normalized_freq"))
}
}
}
return minFreq
}
companion object {
@Volatile
private var INSTANCE: WordDictionary? = null
fun getInstance(context: Context) = INSTANCE ?: synchronized(this) {
INSTANCE ?: WordDictionary(context.applicationContext).also { INSTANCE = it }
}
}
}

View File

@@ -0,0 +1,20 @@
package com.huaban.analysis.jieba
import android.content.Context
open class WordDictionaryDatabase private constructor(context: Context): DictionaryDatabase(context) {
override val databaseName = "dict.db"
companion object {
@Volatile
private var instance: WordDictionaryDatabase? = null
fun getInstance(context: Context): WordDictionaryDatabase = instance ?: synchronized(this) {
instance ?: WordDictionaryDatabase(context.applicationContext).also { instance = it }
}
}
}

View File

@@ -0,0 +1,201 @@
package com.huaban.analysis.jieba.viterbi
import com.huaban.analysis.jieba.CharacterUtil
import com.huaban.analysis.jieba.Log
import com.huaban.analysis.jieba.Node
import com.huaban.analysis.jieba.Pair
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.Charset
import java.util.*
class FinalSeg private constructor() {
private fun loadModel() {
val s = System.currentTimeMillis()
val prevStatus = HashMap<Char, CharArray>().also { prevStatus = it }
prevStatus['B'] = charArrayOf('E', 'S')
prevStatus['M'] = charArrayOf('M', 'B')
prevStatus['S'] = charArrayOf('S', 'E')
prevStatus['E'] = charArrayOf('B', 'M')
val start = HashMap<Char, Double>().also { start = it }
start['B'] = -0.26268660809250016
start['E'] = -3.14e+100
start['M'] = -3.14e+100
start['S'] = -1.4652633398537678
val trans = HashMap<Char, Map<Char, Double>>().also { trans = it }
val transB: MutableMap<Char, Double> = HashMap()
transB['E'] = -0.510825623765990
transB['M'] = -0.916290731874155
trans['B'] = transB
val transE: MutableMap<Char, Double> = HashMap()
transE['B'] = -0.5897149736854513
transE['S'] = -0.8085250474669937
trans['E'] = transE
val transM: MutableMap<Char, Double> = HashMap()
transM['E'] = -0.33344856811948514
transM['M'] = -1.2603623820268226
trans['M'] = transM
val transS: MutableMap<Char, Double> = HashMap()
transS['B'] = -0.7211965654669841
transS['S'] = -0.6658631448798212
trans['S'] = transS
val `is` = javaClass.getResourceAsStream(PROB_EMIT)!!
try {
val br = BufferedReader(InputStreamReader(`is`, Charset.forName("UTF-8")))
val emit = HashMap<Char, Map<Char, Double>>().also { emit = it }
var values: MutableMap<Char, Double>? = null
while (br.ready()) {
val line = br.readLine()
val tokens = line.split("\t".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (tokens.size == 1) {
values = HashMap()
emit[tokens[0][0]] = values
} else {
values!![tokens[0][0]] = tokens[1].toDouble()
}
}
} catch (e: IOException) {
Log.error(String.format(Locale.getDefault(), "%s: load model failure!", PROB_EMIT))
} finally {
try {
`is`.close()
} catch (e: IOException) {
Log.error(String.format(Locale.getDefault(), "%s: close failure!", PROB_EMIT))
}
}
Log.debug(
String.format(
Locale.getDefault(), "model load finished, time elapsed %d ms.",
System.currentTimeMillis() - s
)
)
}
fun cut(sentence: String, tokens: MutableList<String>) {
var chinese = StringBuilder()
var other = StringBuilder()
for (element in sentence) {
if (CharacterUtil.isChineseLetter(element)) {
if (other.isNotEmpty()) {
processOtherUnknownWords(other.toString(), tokens)
other = StringBuilder()
}
chinese.append(element)
} else {
if (chinese.isNotEmpty()) {
viterbi(chinese.toString(), tokens)
chinese = StringBuilder()
}
other.append(element)
}
}
if (chinese.isNotEmpty()) viterbi(chinese.toString(), tokens)
else {
processOtherUnknownWords(other.toString(), tokens)
}
}
private fun viterbi(sentence: String, tokens: MutableList<String>) {
val v = Vector<MutableMap<Char, Double>>()
var path: MutableMap<Char?, Node?> = HashMap()
v.add(HashMap())
for (state in states) {
var emP = emit!![state]!![sentence[0]]
if (null == emP) emP = MIN_FLOAT
v[0][state] = start!![state]!! + emP
path[state] = Node(state, null)
}
for (i in 1..<sentence.length) {
val vv: MutableMap<Char, Double> = HashMap()
v.add(vv)
val newPath: MutableMap<Char?, Node?> = HashMap()
for (y in states) {
var emp = emit!![y]!![sentence[i]]
if (emp == null) emp = MIN_FLOAT
var candidate: Pair<Char>? = null
for (y0 in prevStatus!![y]!!) {
var tranp = trans!![y0]!![y]
if (null == tranp) tranp = MIN_FLOAT
tranp += (emp + v[i - 1][y0]!!)
if (null == candidate) candidate = Pair(y0, tranp)
else if (candidate.freq <= tranp) {
candidate.freq = tranp
candidate.key = y0
}
}
vv[y] = candidate!!.freq
newPath[y] = Node(y, path[candidate.key])
}
path = newPath
}
val probE = v[sentence.length - 1]['E']!!
val probS = v[sentence.length - 1]['S']!!
val posList = Vector<Char>(sentence.length)
var win: Node?
win = if (probE < probS) path['S']
else path['E']
while (win != null) {
posList.add(win.value)
win = win.parent
}
posList.reverse()
var begin = 0
var next = 0
for (i in sentence.indices) {
val pos = posList[i]
when (pos) {
'B' -> begin = i
'E' -> {
tokens.add(sentence.substring(begin, i + 1))
next = i + 1
}
'S' -> {
tokens.add(sentence.substring(i, i + 1))
next = i + 1
}
}
}
if (next < sentence.length) tokens.add(sentence.substring(next))
}
private fun processOtherUnknownWords(other: String, tokens: MutableList<String>) {
val mat = CharacterUtil.reSkip.matcher(other)
var offset = 0
while (mat.find()) {
if (mat.start() > offset) {
tokens.add(other.substring(offset, mat.start()))
}
tokens.add(mat.group())
offset = mat.end()
}
if (offset < other.length) tokens.add(other.substring(offset))
}
companion object {
private var singleInstance: FinalSeg? = null
private const val PROB_EMIT = "/prob_emit.txt"
private val states = charArrayOf('B', 'M', 'E', 'S')
private var emit: MutableMap<Char, Map<Char, Double>>? = null
private var start: MutableMap<Char, Double>? = null
private var trans: MutableMap<Char, Map<Char, Double>>? = null
private var prevStatus: MutableMap<Char, CharArray>? = null
private const val MIN_FLOAT = -3.14e100
@get:Synchronized
val instance: FinalSeg
get() {
if (singleInstance == null) {
singleInstance = FinalSeg().apply { loadModel() }
}
return singleInstance!!
}
}
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -75,11 +75,11 @@ android {
arguments(*[
// @Hint by LZX284 (https://github.com/LZX284) on Sep 30, 2023.
// ! "ANDROID_PLATFORM" 默认为 "android-23", 这里修改为与 AutoJs6 最低 SDK 版本一致的 24.
// ! "ANDROID_PLATFORM" 默认为 "android-23", 这里修改为与 AutoJs6 最低 SDK 版本一致的 `versionMap.MIN_SDK`.
// ! en-US (translated by SuperMonster003 on Oct 22, 2024):
// ! "ANDROID_PLATFORM" with default value "android-23" was changed to 24
// ! "ANDROID_PLATFORM" with default value "android-23" was changed to `versionMap.MIN_SDK`
// ! to align with the min SDK version of AutoJs6.
ANDROID_PLATFORM: "android-24",
ANDROID_PLATFORM: "android-${versionMap.MIN_SDK}",
ANDROID_STL : "c++_shared",
ANDROID_ARM_NEON: "TRUE",
].collect {

View File

@@ -27,7 +27,7 @@
include(
":app",
":jieba-analysis",
":libs:android-job-simplified-1.4.3",
":libs:androidx.appcompat-1.0.2",
":libs:apk-parser-1.0.2",
@@ -47,6 +47,8 @@ include(
":libs:markwon-syntax-highlight-4.6.2",
)
project(":jieba-analysis").projectDir = file("jieba-analysis")
pluginManagement {
val consts = object {
@@ -619,4 +621,10 @@ pluginManagement {
set("isCleanupRapidOcr", config.isCleanupRapidOcr)
}
gradle.beforeProject {
extensions.extraProperties["kotlinVersion"] = notations.classpath.find {
it.contains("kotlin-gradle-plugin")
}?.substringAfterLast(":") ?: config.fallbackKotlinVersion
}
}

View File

@@ -1,5 +1,5 @@
#Fri Dec 13 16:40:27 CST 2024
BUILD_TIME=1734079227022
#Tue Dec 24 20:14:13 CST 2024
BUILD_TIME=1735042453409
COMPILE_SDK_VERSION=34
JAVA_VERSION=23
JAVA_VERSION_MIN_RADICAL=0
@@ -17,6 +17,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=34
TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=2909
VERSION_BUILD=2920
VERSION_NAME=6.6.1 Alpha2
VSCODE_EXT_REQUIRED_VERSION=1.0.8