From c5c72511b4b26327e309010954e33bddac57cc55 Mon Sep 17 00:00:00 2001 From: bushixuanqi <57338301+bushixuanqi@users.noreply.github.com> Date: Tue, 6 Jul 2021 18:57:21 +0800 Subject: [PATCH 1/5] Update RuleAnalyzer.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 进一步简化 AnalyzeByJSoup 中的 data class ElementsSingle,尽量以共享变量代替传递变量。 --- .../app/model/analyzeRule/RuleAnalyzer.kt | 458 +++++++++--------- 1 file changed, 231 insertions(+), 227 deletions(-) diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt b/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt index 5167b526f..686e2768d 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt @@ -2,7 +2,7 @@ package io.legado.app.model.analyzeRule //通用的规则切分处理 -class RuleAnalyzer(data: String) { +class RuleAnalyzer(data: String, code:Boolean = false) { private var queue: String = data //被处理字符串 private var pos = 0 //处理到的位置 @@ -12,6 +12,8 @@ class RuleAnalyzer(data: String) { private var startX = 0 //规则的开始 private var step:Int = 0 //分割字符的长度 + val chompBalanced = if(code) ::chompCodeBalanced else ::chompRuleBalanced //设置平衡组函数,json或JavaScript时设置成chompCodeBalanced,否则为chompRuleBalanced + var elementsType = "" //当前平衡字段 @@ -195,7 +197,7 @@ class RuleAnalyzer(data: String) { /** * 拉出一个代码平衡组,存在转义文本,没有实体字符,通常以{}作为模块 */ - fun chompCodeBalanced(open: Char = '{', close: Char = '}'): Boolean { + fun chompCodeBalanced(open: Char, close: Char): Boolean { var pos = pos //声明临时变量记录匹配位置,匹配成功后才同步到类的pos @@ -234,9 +236,9 @@ class RuleAnalyzer(data: String) { } /** - * 拉出一个规则平衡组,没有转义文本,有实体字符,通常以[]作为选择器 + * 拉出一个规则平衡组,经过仔细测试xpath和jsoup中,引号内转义字符无效。 */ - fun chompRuleBalanced(open: Char = '[', close: Char = ']'): Boolean { + fun chompRuleBalanced(open: Char, close: Char): Boolean { var pos = pos //声明临时变量记录匹配位置,匹配成功后才同步到类的pos var depth = 0 //嵌套深度 @@ -246,207 +248,210 @@ class RuleAnalyzer(data: String) { do { if (pos == queue.length) break val c = queue[pos++] - if (c == '\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 else if (c == '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束,直接进入下个循环 + else if( c=='\\' ){ //不在引号中的转义字符才将下个字符转义 + pos++ + continue + } - if ( c == open )depth++ //开始嵌套一层 - else if ( c== close) depth-- //闭合一层嵌套 + if ( c == open )depth++ //开始嵌套一层 + else if ( c== close) depth-- //闭合一层嵌套 - } while (depth > 0 ) //拉出一个平衡字串 + } while (depth > 0 ) //拉出一个平衡字串 - return if(depth > 0) false else { - this.pos = pos //同步位置 - true - } + return if(depth > 0) false else { + this.pos = pos //同步位置 + true } +} - /** - * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 - * 解决jsonPath自带的"&&"和"||"与阅读的规则冲突,以及规则正则或字符串中包含"&&"、"||"、"%%"、"@"导致的冲突 - */ - tailrec fun splitRule(vararg split: String): Array{ //首段匹配,elementsType为空 +/** + * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 + * 解决jsonPath自带的"&&"和"||"与阅读的规则冲突,以及规则正则或字符串中包含"&&"、"||"、"%%"、"@"导致的冲突 + */ +tailrec fun splitRule(vararg split: String): Array{ //首段匹配,elementsType为空 - if(split.size == 1) { - elementsType = split[0] //设置分割字串 - return if(!consumeTo(elementsType)) { - rule += queue.substring(startX) - rule - }else { - step = elementsType.length //设置分隔符长度 - splitRule() - } //递归匹配 - }else if (!consumeToAny(* split)) { //未找到分隔符 + if(split.size == 1) { + elementsType = split[0] //设置分割字串 + return if(!consumeTo(elementsType)) { rule += queue.substring(startX) - return rule - } + rule + }else { + step = elementsType.length //设置分隔符长度 + splitRule() + } //递归匹配 + }else if (!consumeToAny(* split)) { //未找到分隔符 + rule += queue.substring(startX) + return rule + } - val end = pos //记录分隔位置 - pos = start //重回开始,启动另一种查找 + val end = pos //记录分隔位置 + pos = start //重回开始,启动另一种查找 - do{ - val st = findToAny('[', '(') //查找筛选器位置 + do{ + val st = findToAny('[', '(') //查找筛选器位置 - if (st == -1) { + if (st == -1) { - rule = arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + rule = arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 - elementsType = queue.substring(end, end + step) //设置组合类型 - pos = end + step //跳过分隔符 + elementsType = queue.substring(end, end + step) //设置组合类型 + pos = end + step //跳过分隔符 - while (consumeTo(elementsType)) { //循环切分规则压入数组 - rule += queue.substring(start, pos) - pos += step //跳过分隔符 - } + while (consumeTo(elementsType)) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } - rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule += queue.substring(pos) //将剩余字段压入数组末尾 - return rule - } + return rule + } - if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 + if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 - rule = arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + rule = arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 - elementsType = queue.substring(end, end + step) //设置组合类型 - pos = end + step //跳过分隔符 + elementsType = queue.substring(end, end + step) //设置组合类型 + pos = end + step //跳过分隔符 - while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 - rule += queue.substring(start, pos) - pos += step //跳过分隔符 - } + while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } - return if(pos > st) { - startX = start - splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 - } - else { //执行到此,证明后面再无分隔字符 - rule += queue.substring(pos) //将剩余字段压入数组末尾 - rule - } + return if(pos > st) { + startX = start + splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 + } + else { //执行到此,证明后面再无分隔字符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule } + } - pos = st //位置推移到筛选器处 - val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 + pos = st //位置推移到筛选器处 + val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 - if (!chompRuleBalanced(queue[pos], next)) throw Error( - queue.substring( - 0, - start - ) + "后未平衡" - ) //拉出一个筛选器,不平衡则报错 + if (!chompBalanced(queue[pos], next)) throw Error( + queue.substring( + 0, + start + ) + "后未平衡" + ) //拉出一个筛选器,不平衡则报错 - }while( end > pos ) + }while( end > pos ) - start = pos //设置开始查找筛选器位置的起始位置 + start = pos //设置开始查找筛选器位置的起始位置 - return splitRule(* split) //递归调用首段匹配 - } + return splitRule(* split) //递归调用首段匹配 +} - @JvmName("splitRuleNext") - private tailrec fun splitRule(): Array{ //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 +@JvmName("splitRuleNext") +private tailrec fun splitRule(): Array{ //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 - val end = pos //记录分隔位置 - pos = start //重回开始,启动另一种查找 + val end = pos //记录分隔位置 + pos = start //重回开始,启动另一种查找 - do{ - val st = findToAny('[', '(') //查找筛选器位置 + do{ + val st = findToAny('[', '(') //查找筛选器位置 - if (st == -1) { + if (st == -1) { - rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 - pos = end + step //跳过分隔符 + rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + pos = end + step //跳过分隔符 - while (consumeTo(elementsType)) { //循环切分规则压入数组 - rule += queue.substring(start, pos) - pos += step //跳过分隔符 - } + while (consumeTo(elementsType)) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } - rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule += queue.substring(pos) //将剩余字段压入数组末尾 - return rule - } + return rule + } - if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 + if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 - rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 - pos = end + step //跳过分隔符 + rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + pos = end + step //跳过分隔符 - while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 - rule += queue.substring(start, pos) - pos += step //跳过分隔符 - } + while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } - return if(pos > st) { - startX = start - splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 - } - else { //执行到此,证明后面再无分隔字符 - rule += queue.substring(pos) //将剩余字段压入数组末尾 - rule - } + return if(pos > st) { + startX = start + splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 } + else { //执行到此,证明后面再无分隔字符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule + } + } - pos = st //位置推移到筛选器处 - val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 + pos = st //位置推移到筛选器处 + val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 - if (!chompRuleBalanced(queue[pos], next)) throw Error( - queue.substring( - 0, - start - ) + "后未平衡" - ) //拉出一个筛选器,不平衡则报错 + if (!chompBalanced(queue[pos], next)) throw Error( + queue.substring( + 0, + start + ) + "后未平衡" + ) //拉出一个筛选器,不平衡则报错 - }while( end > pos ) + }while( end > pos ) - start = pos //设置开始查找筛选器位置的起始位置 + start = pos //设置开始查找筛选器位置的起始位置 - return if(!consumeTo(elementsType)) { - rule += queue.substring(startX) - rule - }else splitRule() //递归匹配 + return if(!consumeTo(elementsType)) { + rule += queue.substring(startX) + rule + }else splitRule() //递归匹配 - } +} - /** - * 替换内嵌规则 - * @param inner 起始标志,如{$. 或 {{ - * @param startStep 不属于规则部分的前置字符长度,如{$.中{不属于规则的组成部分,故startStep为1 - * @param endStep 不属于规则部分的后置字符长度,如}}长度为2 - * @param fr 查找到内嵌规则时,用于解析的函数 - * - * */ - fun innerRule( inner:String,startStep:Int = 1,endStep:Int = 1,fr:(String)->String?): String { - - val start0 = pos //规则匹配前起点 - - val st = StringBuilder() - - while (!isEmpty && consumeTo(inner)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true - val posPre = pos //记录上次结束位置 - if (chompCodeBalanced()) { - val frv= fr(currBalancedString(startStep,endStep)) - if(frv != null) { - st.append(queue.substring(start,posPre)+frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 - continue //获取内容成功,继续选择下个内嵌规则 +/** + * 替换内嵌规则 + * @param inner 起始标志,如{$. 或 {{ + * @param startStep 不属于规则部分的前置字符长度,如{$.中{不属于规则的组成部分,故startStep为1 + * @param endStep 不属于规则部分的后置字符长度,如}}长度为2 + * @param fr 查找到内嵌规则时,用于解析的函数 + * + * */ +fun innerRule( inner:String,startStep:Int = 1,endStep:Int = 1,fr:(String)->String?): String { - } - } + val start0 = pos //规则匹配前起点 - pos += inner.length //拉出字段不平衡,inner只是个普通字串,跳到此inner后继续匹配 + val st = StringBuilder() - } + while (!isEmpty && consumeTo(inner)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true + val posPre = pos //记录上次结束位置 + if (chompBalanced('{', '}')) { + val frv= fr(currBalancedString(startStep,endStep)) + if(frv != null) { + st.append(queue.substring(start,posPre)+frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 + continue //获取内容成功,继续选择下个内嵌规则 - //匹配前起点与当前规则起点相同,证明无替换成功的内嵌规则,返回空字符串。否则返回替换后的字符串 - return if(start0 == start) "" else { - st.append(remainingString()) //压入剩余字符串 - st.toString() + } } + + pos += inner.length //拉出字段不平衡,inner只是个普通字串,跳到此inner后继续匹配 + } + //匹配前起点与当前规则起点相同,证明无替换成功的内嵌规则,返回空字符串。否则返回替换后的字符串 + return if(start0 == start) "" else { + st.append(remainingString()) //压入剩余字符串 + st.toString() + } +} + // /** // * 匹配并返回标签中的属性键字串(字母、数字、-、_、:) // * @return 属性键字串 @@ -480,84 +485,83 @@ class RuleAnalyzer(data: String) { // return query // } - companion object { - /** - * 转义字符 - */ - private const val ESC = '\\' - - /** - * 阅读共有分隔字串起始部分 - * "##","@@","{{","{[","", "@js:" - */ - val splitList =arrayOf("##","@@","{{","{[","", "@js:") - - /** - * 发现‘名称-链接’分隔字串 - * "::" - */ - const val splitListFaXian = "::" - - /** - * 目录专有起始字符 - * "-" - */ - const val splitListMulu = "-" - - /** - * 结果为元素列表的 all in one 模式起始字符 - * "+" - */ - const val splitListTongYi = "+" - - /** - * 结果为元素列表的项的同规则组合结构 - * "||","&&","%%" - */ - val splitListReSplit = arrayOf("||","&&","%%") - - /** - * js脚本结束字串 - * "" - */ - const val splitListEndJS = "" - - /** - *内嵌js结束字串 - * "}}" - */ - const val splitListEndInnerJS = "}}" - - /** - * 内嵌规则结束字串 - * "]}" - */ - const val splitListEndInnerRule = "]}" - - /** - * '[', ']', '(', ')','{','}' - */ - val splitListPublic = charArrayOf('[', ']', '(', ')','{','}') - - /** - * '*',"/","//",":","::","@","|","@xpath:" - */ - val splitListXpath = arrayOf("*","/","//",":","::","@","|","@xpath:") - - /** - * '*','$',".","..", "@json:" - */ - val splitListJson = arrayOf('*','$',".","..", "@json:") - - /** - * '*',"+","~",".",",","|","@","@css:",":" - */ - val splitListCss = arrayOf('*',"+","~",".",",","|","@","@css:",":") - - /** - * "-",".","!","@","@@" - */ - val splitListDefault = arrayOf("-",".","!","@","@@") +companion object { + /** + * 转义字符 + */ + private const val ESC = '\\' - } -} + /** + * 阅读共有分隔字串起始部分 + * "##","@@","{{","{[","", "@js:" + */ + val splitList =arrayOf("##","@@","{{","{[","", "@js:") + + /** + * 发现‘名称-链接’分隔字串 + * "::" + */ + const val splitListFaXian = "::" + + /** + * 目录专有起始字符 + * "-" + */ + const val splitListMulu = "-" + + /** + * 结果为元素列表的 all in one 模式起始字符 + * "+" + */ + const val splitListTongYi = "+" + + /** + * 结果为元素列表的项的同规则组合结构 + * "||","&&","%%" + */ + val splitListReSplit = arrayOf("||","&&","%%") + + /** + * js脚本结束字串 + * "" + */ + const val splitListEndJS = "" + + /** + *内嵌js结束字串 + * "}}" + */ + const val splitListEndInnerJS = "}}" + + /** + * 内嵌规则结束字串 + * "]}" + */ + const val splitListEndInnerRule = "]}" + + /** + * '[', ']', '(', ')','{','}' + */ + val splitListPublic = charArrayOf('[', ']', '(', ')','{','}') + + /** + * '*',"/","//",":","::","@","|","@xpath:" + */ + val splitListXpath = arrayOf("*","/","//",":","::","@","|","@xpath:") + + /** + * '*','$',".","..", "@json:" + */ + val splitListJson = arrayOf('*','$',".","..", "@json:") + + /** + * '*',"+","~",".",",","|","@","@css:",":" + */ + val splitListCss = arrayOf('*',"+","~",".",",","|","@","@css:",":") + + /** + * "-",".","!","@","@@" + */ + val splitListDefault = arrayOf("-",".","!","@","@@") + +}} From d5aba03fcf07c83b12da53f9364e85f2e24e3abc Mon Sep 17 00:00:00 2001 From: bushixuanqi <57338301+bushixuanqi@users.noreply.github.com> Date: Tue, 6 Jul 2021 18:59:09 +0800 Subject: [PATCH 2/5] Update AnalyzeByJSonPath.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增设置平衡函数的参数 --- .../io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt index b2704513a..36c5c2840 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt @@ -30,7 +30,7 @@ class AnalyzeByJSonPath(json: Any) { fun getString(rule: String): String? { if (rule.isEmpty()) return null var result: String - val ruleAnalyzes = RuleAnalyzer(rule) + val ruleAnalyzes = RuleAnalyzer(rule,true) //设置平衡组为代码平衡 val rules = ruleAnalyzes.splitRule("&&","||") if (rules.size == 1) { @@ -83,7 +83,7 @@ class AnalyzeByJSonPath(json: Any) { internal fun getStringList(rule: String): List { val result = ArrayList() if (rule.isEmpty()) return result - val ruleAnalyzes = RuleAnalyzer(rule) + val ruleAnalyzes = RuleAnalyzer(rule,true) //设置平衡组为代码平衡 val rules = ruleAnalyzes.splitRule("&&","||","%%") if (rules.size == 1) { @@ -148,7 +148,7 @@ class AnalyzeByJSonPath(json: Any) { internal fun getList(rule: String): ArrayList? { val result = ArrayList() if (rule.isEmpty()) return result - val ruleAnalyzes = RuleAnalyzer(rule) + val ruleAnalyzes = RuleAnalyzer(rule,true) //设置平衡组为代码平衡 val rules = ruleAnalyzes.splitRule("&&","||","%%") if (rules.size == 1) { ctx.let { From 408fb77019f7c48d8136c6f7e040a2e56f927ba5 Mon Sep 17 00:00:00 2001 From: bushixuanqi <57338301+bushixuanqi@users.noreply.github.com> Date: Tue, 6 Jul 2021 18:59:55 +0800 Subject: [PATCH 3/5] Update AnalyzeByJSoup.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 进一步简化 AnalyzeByJSoup 中的 ElementsSingle,尽量以共享代替传递 --- .../app/model/analyzeRule/AnalyzeByJSoup.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt index 048e759c5..0d5438348 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt @@ -42,8 +42,8 @@ class AnalyzeByJSoup(doc: Any) { * 合并内容列表,得到内容 */ internal fun getString(ruleStr: String) = - if(ruleStr.isEmpty()) null - else getStringList(ruleStr).takeIf { it.isNotEmpty() }?.joinToString("\n") + if(ruleStr.isEmpty()) null + else getStringList(ruleStr).takeIf { it.isNotEmpty() }?.joinToString("\n") /** * 获取一个字符串 @@ -75,15 +75,15 @@ class AnalyzeByJSoup(doc: Any) { for (ruleStrX in ruleStrS) { val temp: List? = - if (sourceRule.isCss) { - val lastIndex = ruleStrX.lastIndexOf('@') - getResultLast( - element.select(ruleStrX.substring(0, lastIndex)), - ruleStrX.substring(lastIndex + 1) - ) - } else { - getResultList(ruleStrX) - } + if (sourceRule.isCss) { + val lastIndex = ruleStrX.lastIndexOf('@') + getResultLast( + element.select(ruleStrX.substring(0, lastIndex)), + ruleStrX.substring(lastIndex + 1) + ) + } else { + getResultList(ruleStrX) + } if (!temp.isNullOrEmpty()) { From 47aa7095b89e544a2bdb938cb936747df6ff10e1 Mon Sep 17 00:00:00 2001 From: bushixuanqi <57338301+bushixuanqi@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:04:04 +0800 Subject: [PATCH 4/5] Update RuleAnalyzer.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化排版 --- .../app/model/analyzeRule/RuleAnalyzer.kt | 445 +++++++++--------- 1 file changed, 223 insertions(+), 222 deletions(-) diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt b/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt index 686e2768d..6a5c674ec 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt @@ -255,202 +255,202 @@ class RuleAnalyzer(data: String, code:Boolean = false) { else if( c=='\\' ){ //不在引号中的转义字符才将下个字符转义 pos++ continue - } + } - if ( c == open )depth++ //开始嵌套一层 - else if ( c== close) depth-- //闭合一层嵌套 + if ( c == open )depth++ //开始嵌套一层 + else if ( c== close) depth-- //闭合一层嵌套 - } while (depth > 0 ) //拉出一个平衡字串 + } while (depth > 0 ) //拉出一个平衡字串 - return if(depth > 0) false else { - this.pos = pos //同步位置 - true + return if(depth > 0) false else { + this.pos = pos //同步位置 + true + } } -} -/** - * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 - * 解决jsonPath自带的"&&"和"||"与阅读的规则冲突,以及规则正则或字符串中包含"&&"、"||"、"%%"、"@"导致的冲突 - */ -tailrec fun splitRule(vararg split: String): Array{ //首段匹配,elementsType为空 + /** + * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 + * 解决jsonPath自带的"&&"和"||"与阅读的规则冲突,以及规则正则或字符串中包含"&&"、"||"、"%%"、"@"导致的冲突 + */ + tailrec fun splitRule(vararg split: String): Array{ //首段匹配,elementsType为空 - if(split.size == 1) { - elementsType = split[0] //设置分割字串 - return if(!consumeTo(elementsType)) { + if(split.size == 1) { + elementsType = split[0] //设置分割字串 + return if(!consumeTo(elementsType)) { + rule += queue.substring(startX) + rule + }else { + step = elementsType.length //设置分隔符长度 + splitRule() + } //递归匹配 + }else if (!consumeToAny(* split)) { //未找到分隔符 rule += queue.substring(startX) - rule - }else { - step = elementsType.length //设置分隔符长度 - splitRule() - } //递归匹配 - }else if (!consumeToAny(* split)) { //未找到分隔符 - rule += queue.substring(startX) - return rule - } + return rule + } - val end = pos //记录分隔位置 - pos = start //重回开始,启动另一种查找 + val end = pos //记录分隔位置 + pos = start //重回开始,启动另一种查找 - do{ - val st = findToAny('[', '(') //查找筛选器位置 + do{ + val st = findToAny('[', '(') //查找筛选器位置 - if (st == -1) { + if (st == -1) { - rule = arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + rule = arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 - elementsType = queue.substring(end, end + step) //设置组合类型 - pos = end + step //跳过分隔符 + elementsType = queue.substring(end, end + step) //设置组合类型 + pos = end + step //跳过分隔符 - while (consumeTo(elementsType)) { //循环切分规则压入数组 - rule += queue.substring(start, pos) - pos += step //跳过分隔符 - } + while (consumeTo(elementsType)) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } - rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule += queue.substring(pos) //将剩余字段压入数组末尾 - return rule - } + return rule + } - if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 + if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 - rule = arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + rule = arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 - elementsType = queue.substring(end, end + step) //设置组合类型 - pos = end + step //跳过分隔符 + elementsType = queue.substring(end, end + step) //设置组合类型 + pos = end + step //跳过分隔符 - while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 - rule += queue.substring(start, pos) - pos += step //跳过分隔符 - } + while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } - return if(pos > st) { - startX = start - splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 - } - else { //执行到此,证明后面再无分隔字符 - rule += queue.substring(pos) //将剩余字段压入数组末尾 - rule + return if(pos > st) { + startX = start + splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 + } + else { //执行到此,证明后面再无分隔字符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule + } } - } - pos = st //位置推移到筛选器处 - val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 + pos = st //位置推移到筛选器处 + val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 - if (!chompBalanced(queue[pos], next)) throw Error( - queue.substring( - 0, - start - ) + "后未平衡" - ) //拉出一个筛选器,不平衡则报错 + if (!chompBalanced(queue[pos], next)) throw Error( + queue.substring( + 0, + start + ) + "后未平衡" + ) //拉出一个筛选器,不平衡则报错 - }while( end > pos ) + }while( end > pos ) - start = pos //设置开始查找筛选器位置的起始位置 - - return splitRule(* split) //递归调用首段匹配 -} + start = pos //设置开始查找筛选器位置的起始位置 -@JvmName("splitRuleNext") -private tailrec fun splitRule(): Array{ //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 - - val end = pos //记录分隔位置 - pos = start //重回开始,启动另一种查找 + return splitRule(* split) //递归调用首段匹配 + } - do{ - val st = findToAny('[', '(') //查找筛选器位置 + @JvmName("splitRuleNext") + private tailrec fun splitRule(): Array{ //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 - if (st == -1) { + val end = pos //记录分隔位置 + pos = start //重回开始,启动另一种查找 - rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 - pos = end + step //跳过分隔符 + do{ + val st = findToAny('[', '(') //查找筛选器位置 - while (consumeTo(elementsType)) { //循环切分规则压入数组 - rule += queue.substring(start, pos) - pos += step //跳过分隔符 - } + if (st == -1) { - rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + pos = end + step //跳过分隔符 - return rule - } - - if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 + while (consumeTo(elementsType)) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } - rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 - pos = end + step //跳过分隔符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 - while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 - rule += queue.substring(start, pos) - pos += step //跳过分隔符 + return rule } - return if(pos > st) { - startX = start - splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 - } - else { //执行到此,证明后面再无分隔字符 - rule += queue.substring(pos) //将剩余字段压入数组末尾 - rule - } - } + if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 - pos = st //位置推移到筛选器处 - val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 + rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + pos = end + step //跳过分隔符 - if (!chompBalanced(queue[pos], next)) throw Error( - queue.substring( - 0, - start - ) + "后未平衡" - ) //拉出一个筛选器,不平衡则报错 + while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } - }while( end > pos ) + return if(pos > st) { + startX = start + splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 + } + else { //执行到此,证明后面再无分隔字符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule + } + } - start = pos //设置开始查找筛选器位置的起始位置 + pos = st //位置推移到筛选器处 + val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 - return if(!consumeTo(elementsType)) { - rule += queue.substring(startX) - rule - }else splitRule() //递归匹配 + if (!chompBalanced(queue[pos], next)) throw Error( + queue.substring( + 0, + start + ) + "后未平衡" + ) //拉出一个筛选器,不平衡则报错 -} + }while( end > pos ) + start = pos //设置开始查找筛选器位置的起始位置 -/** - * 替换内嵌规则 - * @param inner 起始标志,如{$. 或 {{ - * @param startStep 不属于规则部分的前置字符长度,如{$.中{不属于规则的组成部分,故startStep为1 - * @param endStep 不属于规则部分的后置字符长度,如}}长度为2 - * @param fr 查找到内嵌规则时,用于解析的函数 - * - * */ -fun innerRule( inner:String,startStep:Int = 1,endStep:Int = 1,fr:(String)->String?): String { + return if(!consumeTo(elementsType)) { + rule += queue.substring(startX) + rule + }else splitRule() //递归匹配 - val start0 = pos //规则匹配前起点 + } - val st = StringBuilder() - while (!isEmpty && consumeTo(inner)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true - val posPre = pos //记录上次结束位置 - if (chompBalanced('{', '}')) { - val frv= fr(currBalancedString(startStep,endStep)) - if(frv != null) { - st.append(queue.substring(start,posPre)+frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 - continue //获取内容成功,继续选择下个内嵌规则 + /** + * 替换内嵌规则 + * @param inner 起始标志,如{$. 或 {{ + * @param startStep 不属于规则部分的前置字符长度,如{$.中{不属于规则的组成部分,故startStep为1 + * @param endStep 不属于规则部分的后置字符长度,如}}长度为2 + * @param fr 查找到内嵌规则时,用于解析的函数 + * + * */ + fun innerRule( inner:String,startStep:Int = 1,endStep:Int = 1,fr:(String)->String?): String { + + val start0 = pos //规则匹配前起点 + + val st = StringBuilder() + + while (!isEmpty && consumeTo(inner)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true + val posPre = pos //记录上次结束位置 + if (chompBalanced('{', '}')) { + val frv= fr(currBalancedString(startStep,endStep)) + if(frv != null) { + st.append(queue.substring(start,posPre)+frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 + continue //获取内容成功,继续选择下个内嵌规则 + } } - } - pos += inner.length //拉出字段不平衡,inner只是个普通字串,跳到此inner后继续匹配 + pos += inner.length //拉出字段不平衡,inner只是个普通字串,跳到此inner后继续匹配 - } + } - //匹配前起点与当前规则起点相同,证明无替换成功的内嵌规则,返回空字符串。否则返回替换后的字符串 - return if(start0 == start) "" else { - st.append(remainingString()) //压入剩余字符串 - st.toString() + //匹配前起点与当前规则起点相同,证明无替换成功的内嵌规则,返回空字符串。否则返回替换后的字符串 + return if(start0 == start) "" else { + st.append(remainingString()) //压入剩余字符串 + st.toString() + } } -} // /** // * 匹配并返回标签中的属性键字串(字母、数字、-、_、:) @@ -485,83 +485,84 @@ fun innerRule( inner:String,startStep:Int = 1,endStep:Int = 1,fr:(String)->Strin // return query // } -companion object { - /** - * 转义字符 - */ - private const val ESC = '\\' - - /** - * 阅读共有分隔字串起始部分 - * "##","@@","{{","{[","", "@js:" - */ - val splitList =arrayOf("##","@@","{{","{[","", "@js:") - - /** - * 发现‘名称-链接’分隔字串 - * "::" - */ - const val splitListFaXian = "::" + companion object { + /** + * 转义字符 + */ + private const val ESC = '\\' + + /** + * 阅读共有分隔字串起始部分 + * "##","@@","{{","{[","", "@js:" + */ + val splitList =arrayOf("##","@@","{{","{[","", "@js:") + + /** + * 发现‘名称-链接’分隔字串 + * "::" + */ + const val splitListFaXian = "::" + + /** + * 目录专有起始字符 + * "-" + */ + const val splitListMulu = "-" + + /** + * 结果为元素列表的 all in one 模式起始字符 + * "+" + */ + const val splitListTongYi = "+" + + /** + * 结果为元素列表的项的同规则组合结构 + * "||","&&","%%" + */ + val splitListReSplit = arrayOf("||","&&","%%") + + /** + * js脚本结束字串 + * "" + */ + const val splitListEndJS = "" + + /** + *内嵌js结束字串 + * "}}" + */ + const val splitListEndInnerJS = "}}" + + /** + * 内嵌规则结束字串 + * "]}" + */ + const val splitListEndInnerRule = "]}" + + /** + * '[', ']', '(', ')','{','}' + */ + val splitListPublic = charArrayOf('[', ']', '(', ')','{','}') + + /** + * '*',"/","//",":","::","@","|","@xpath:" + */ + val splitListXpath = arrayOf("*","/","//",":","::","@","|","@xpath:") + + /** + * '*','$',".","..", "@json:" + */ + val splitListJson = arrayOf('*','$',".","..", "@json:") + + /** + * '*',"+","~",".",",","|","@","@css:",":" + */ + val splitListCss = arrayOf('*',"+","~",".",",","|","@","@css:",":") + + /** + * "-",".","!","@","@@" + */ + val splitListDefault = arrayOf("-",".","!","@","@@") - /** - * 目录专有起始字符 - * "-" - */ - const val splitListMulu = "-" - - /** - * 结果为元素列表的 all in one 模式起始字符 - * "+" - */ - const val splitListTongYi = "+" - - /** - * 结果为元素列表的项的同规则组合结构 - * "||","&&","%%" - */ - val splitListReSplit = arrayOf("||","&&","%%") - - /** - * js脚本结束字串 - * "" - */ - const val splitListEndJS = "" - - /** - *内嵌js结束字串 - * "}}" - */ - const val splitListEndInnerJS = "}}" - - /** - * 内嵌规则结束字串 - * "]}" - */ - const val splitListEndInnerRule = "]}" - - /** - * '[', ']', '(', ')','{','}' - */ - val splitListPublic = charArrayOf('[', ']', '(', ')','{','}') - - /** - * '*',"/","//",":","::","@","|","@xpath:" - */ - val splitListXpath = arrayOf("*","/","//",":","::","@","|","@xpath:") - - /** - * '*','$',".","..", "@json:" - */ - val splitListJson = arrayOf('*','$',".","..", "@json:") - - /** - * '*',"+","~",".",",","|","@","@css:",":" - */ - val splitListCss = arrayOf('*',"+","~",".",",","|","@","@css:",":") - - /** - * "-",".","!","@","@@" - */ - val splitListDefault = arrayOf("-",".","!","@","@@") - -}} + } +} From 8e18de3129f797650371318755700c5785bff6f4 Mon Sep 17 00:00:00 2001 From: gedoor Date: Tue, 6 Jul 2021 22:41:45 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E5=AD=97=E5=85=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/book/read/ReadBookActivity.kt | 5 +++ .../java/io/legado/app/ui/dict/DictDialog.kt | 37 ++++++++++++++++++- .../io/legado/app/ui/dict/DictViewModel.kt | 25 +++++++++++-- app/src/main/res/layout/dialog_dict.xml | 11 ++++++ .../main/res/menu/content_select_action.xml | 4 ++ app/src/main/res/values-ja-rJP/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-zh-rHK/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 11 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/layout/dialog_dict.xml diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 4726949cb..c5657da1e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -46,6 +46,7 @@ import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.book.searchContent.SearchContentActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.toc.TocActivityResult +import io.legado.app.ui.dict.DictDialog import io.legado.app.ui.login.SourceLoginActivity import io.legado.app.ui.replace.ReplaceRuleActivity import io.legado.app.ui.replace.edit.ReplaceEditActivity @@ -531,6 +532,10 @@ class ReadBookActivity : ReadBookBaseActivity(), openSearchActivity(selectedText) return true } + R.id.menu_dict -> { + DictDialog.dict(supportFragmentManager, selectedText) + return true + } } return false } diff --git a/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt b/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt index d4f9fefab..573e708ae 100644 --- a/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt +++ b/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt @@ -1,11 +1,21 @@ package io.legado.app.ui.dict import android.os.Bundle +import android.text.Html +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels +import io.legado.app.R import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogDictBinding +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +/** + * 词典 + */ class DictDialog : BaseDialogFragment() { companion object { @@ -21,10 +31,33 @@ class DictDialog : BaseDialogFragment() { } private val viewModel by viewModels() + private val binding by viewBinding(DialogDictBinding::bind) - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - viewModel.word = arguments?.getString("word") ?: "" + override fun onStart() { + super.onStart() + dialog?.window + ?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + return inflater.inflate(R.layout.dialog_dict, container) + } + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val word = arguments?.getString("word") + if (word.isNullOrEmpty()) { + toastOnUi(R.string.cannot_empty) + dismiss() + return + } + viewModel.dictHtmlData.observe(viewLifecycleOwner) { + binding.tvDict.text = Html.fromHtml(it) + } + viewModel.dict(word) } diff --git a/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt b/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt index 8b694776f..774876f76 100644 --- a/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt @@ -1,11 +1,30 @@ package io.legado.app.ui.dict import android.app.Application -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.help.http.get +import io.legado.app.help.http.newCallStrResponse +import io.legado.app.help.http.okHttpClient +import org.jsoup.Jsoup -class DictViewModel(application: Application) : AndroidViewModel(application) { +class DictViewModel(application: Application) : BaseViewModel(application) { - var word: String? = null + var dictHtmlData: MutableLiveData = MutableLiveData() + fun dict(word: String) { + execute { + val body = okHttpClient.newCallStrResponse { + get("http://apii.dict.cn/mini.php", mapOf(Pair("q", word))) + }.body + val jsoup = Jsoup.parse(body) + jsoup.body() + }.onSuccess { + dictHtmlData.postValue(it.html()) + }.onError { + toastOnUi(it.localizedMessage) + } + + } } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_dict.xml b/app/src/main/res/layout/dialog_dict.xml new file mode 100644 index 000000000..e04adae59 --- /dev/null +++ b/app/src/main/res/layout/dialog_dict.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/content_select_action.xml b/app/src/main/res/menu/content_select_action.xml index ec918aec4..63a4bb184 100644 --- a/app/src/main/res/menu/content_select_action.xml +++ b/app/src/main/res/menu/content_select_action.xml @@ -17,6 +17,10 @@ android:id="@+id/menu_aloud" android:title="@string/read_aloud" /> + + diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index e24a01296..961d104c5 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -846,4 +846,5 @@ 导出文件名 重置 url为空 + 字典 diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 05869cbb9..efde516ba 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -846,4 +846,5 @@ Exportar o nome do arquivo 重置 url为空 + 字典 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index be7e0ac7b..05c592dc2 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -844,5 +844,6 @@ 导出文件名 重置 url为空 + 字典 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index dfce22319..b781f4ee1 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -845,5 +845,6 @@ 匯出檔案名 重設 url为空 + 字典 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index ce903631d..509525cbc 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -845,5 +845,6 @@ 导出文件名 重置 url为空 + 字典 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 722607042..722ac6550 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -846,5 +846,6 @@ Export file name Reset Null url + 字典