Merge remote-tracking branch 'origin/master' into master

# Conflicts:
#	app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt
pull/1114/head
bushixuanqi 3 years ago
commit 126f4d3991
  1. 445
      app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt
  2. 5
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  3. 37
      app/src/main/java/io/legado/app/ui/dict/DictDialog.kt
  4. 25
      app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt
  5. 11
      app/src/main/res/layout/dialog_dict.xml
  6. 4
      app/src/main/res/menu/content_select_action.xml
  7. 1
      app/src/main/res/values-ja-rJP/strings.xml
  8. 1
      app/src/main/res/values-pt-rBR/strings.xml
  9. 1
      app/src/main/res/values-zh-rHK/strings.xml
  10. 1
      app/src/main/res/values-zh-rTW/strings.xml
  11. 1
      app/src/main/res/values-zh/strings.xml
  12. 1
      app/src/main/res/values/strings.xml

@ -255,202 +255,202 @@ class RuleAnalyzer(data: String, code:Boolean = false) {
else if( c=='\\' ){ //不在引号中的转义字符才将下个字符转义 else if( c=='\\' ){ //不在引号中的转义字符才将下个字符转义
pos++ pos++
continue continue
} }
if ( c == open )depth++ //开始嵌套一层 if ( c == open )depth++ //开始嵌套一层
else if ( c== close) depth-- //闭合一层嵌套 else if ( c== close) depth-- //闭合一层嵌套
} while (depth > 0 ) //拉出一个平衡字串 } while (depth > 0 ) //拉出一个平衡字串
return if(depth > 0) false else { return if(depth > 0) false else {
this.pos = pos //同步位置 this.pos = pos //同步位置
true true
}
} }
}
/** /**
* 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则
* 解决jsonPath自带的"&&""||"与阅读的规则冲突,以及规则正则或字符串中包含"&&""||""%%""@"导致的冲突 * 解决jsonPath自带的"&&""||"与阅读的规则冲突,以及规则正则或字符串中包含"&&""||""%%""@"导致的冲突
*/ */
tailrec fun splitRule(vararg split: String): Array<String>{ //首段匹配,elementsType为空 tailrec fun splitRule(vararg split: String): Array<String>{ //首段匹配,elementsType为空
if(split.size == 1) { if(split.size == 1) {
elementsType = split[0] //设置分割字串 elementsType = split[0] //设置分割字串
return if(!consumeTo(elementsType)) { return if(!consumeTo(elementsType)) {
rule += queue.substring(startX)
rule
}else {
step = elementsType.length //设置分隔符长度
splitRule()
} //递归匹配
}else if (!consumeToAny(* split)) { //未找到分隔符
rule += queue.substring(startX) rule += queue.substring(startX)
rule return rule
}else { }
step = elementsType.length //设置分隔符长度
splitRule()
} //递归匹配
}else if (!consumeToAny(* split)) { //未找到分隔符
rule += queue.substring(startX)
return rule
}
val end = pos //记录分隔位置 val end = pos //记录分隔位置
pos = start //重回开始,启动另一种查找 pos = start //重回开始,启动另一种查找
do{ do{
val st = findToAny('[', '(') //查找筛选器位置 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) //设置组合类型 elementsType = queue.substring(end, end + step) //设置组合类型
pos = end + step //跳过分隔符 pos = end + step //跳过分隔符
while (consumeTo(elementsType)) { //循环切分规则压入数组 while (consumeTo(elementsType)) { //循环切分规则压入数组
rule += queue.substring(start, pos) rule += queue.substring(start, pos)
pos += step //跳过分隔符 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) //设置组合类型 elementsType = queue.substring(end, end + step) //设置组合类型
pos = end + step //跳过分隔符 pos = end + step //跳过分隔符
while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组
rule += queue.substring(start, pos) rule += queue.substring(start, pos)
pos += step //跳过分隔符 pos += step //跳过分隔符
} }
return if(pos > st) { return if(pos > st) {
startX = start startX = start
splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配
} }
else { //执行到此,证明后面再无分隔字符 else { //执行到此,证明后面再无分隔字符
rule += queue.substring(pos) //将剩余字段压入数组末尾 rule += queue.substring(pos) //将剩余字段压入数组末尾
rule rule
}
} }
}
pos = st //位置推移到筛选器处 pos = st //位置推移到筛选器处
val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符
if (!chompBalanced(queue[pos], next)) throw Error( if (!chompBalanced(queue[pos], next)) throw Error(
queue.substring( queue.substring(
0, 0,
start start
) + "后未平衡" ) + "后未平衡"
) //拉出一个筛选器,不平衡则报错 ) //拉出一个筛选器,不平衡则报错
}while( end > pos ) }while( end > pos )
start = pos //设置开始查找筛选器位置的起始位置 start = pos //设置开始查找筛选器位置的起始位置
return splitRule(* split) //递归调用首段匹配
}
@JvmName("splitRuleNext") return splitRule(* split) //递归调用首段匹配
private tailrec fun splitRule(): Array<String>{ //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 }
val end = pos //记录分隔位置
pos = start //重回开始,启动另一种查找
do{ @JvmName("splitRuleNext")
val st = findToAny('[', '(') //查找筛选器位置 private tailrec fun splitRule(): Array<String>{ //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快
if (st == -1) { val end = pos //记录分隔位置
pos = start //重回开始,启动另一种查找
rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 do{
pos = end + step //跳过分隔符 val st = findToAny('[', '(') //查找筛选器位置
while (consumeTo(elementsType)) { //循环切分规则压入数组 if (st == -1) {
rule += queue.substring(start, pos)
pos += step //跳过分隔符
}
rule += queue.substring(pos) //将剩余字段压入数组末尾 rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组
pos = end + step //跳过分隔符
return rule while (consumeTo(elementsType)) { //循环切分规则压入数组
} rule += queue.substring(start, pos)
pos += step //跳过分隔符
if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 }
rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 rule += queue.substring(pos) //将剩余字段压入数组末尾
pos = end + step //跳过分隔符
while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 return rule
rule += queue.substring(start, pos)
pos += step //跳过分隔符
} }
return if(pos > st) { if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组
startX = start
splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配
}
else { //执行到此,证明后面再无分隔字符
rule += queue.substring(pos) //将剩余字段压入数组末尾
rule
}
}
pos = st //位置推移到筛选器处 rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组
val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字 pos = end + step //跳过分隔符
if (!chompBalanced(queue[pos], next)) throw Error( while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组
queue.substring( rule += queue.substring(start, pos)
0, pos += step //跳过分隔符
start }
) + "后未平衡"
) //拉出一个筛选器,不平衡则报错
}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)) { if (!chompBalanced(queue[pos], next)) throw Error(
rule += queue.substring(startX) queue.substring(
rule 0,
}else splitRule() //递归匹配 start
) + "后未平衡"
) //拉出一个筛选器,不平衡则报错
} }while( end > pos )
start = pos //设置开始查找筛选器位置的起始位置
/** return if(!consumeTo(elementsType)) {
* 替换内嵌规则 rule += queue.substring(startX)
* @param inner 起始标志,{$. {{ rule
* @param startStep 不属于规则部分的前置字符长度{$.{不属于规则的组成部分故startStep为1 }else splitRule() //递归匹配
* @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('{', '}')) { * @param inner 起始标志,{$. {{
val frv= fr(currBalancedString(startStep,endStep)) * @param startStep 不属于规则部分的前置字符长度{$.{不属于规则的组成部分故startStep为1
if(frv != null) { * @param endStep 不属于规则部分的后置字符长度}}长度为2
st.append(queue.substring(start,posPre)+frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 * @param fr 查找到内嵌规则时用于解析的函数
continue //获取内容成功,继续选择下个内嵌规则 *
* */
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 { return if(start0 == start) "" else {
st.append(remainingString()) //压入剩余字符串 st.append(remainingString()) //压入剩余字符串
st.toString() st.toString()
}
} }
}
// /** // /**
// * 匹配并返回标签中的属性键字串(字母、数字、-、_、:) // * 匹配并返回标签中的属性键字串(字母、数字、-、_、:)
@ -485,83 +485,84 @@ fun innerRule( inner:String,startStep:Int = 1,endStep:Int = 1,fr:(String)->Strin
// return query // return query
// } // }
companion object { companion object {
/** /**
* 转义字符 * 转义字符
*/ */
private const val ESC = '\\' private const val ESC = '\\'
/** /**
* 阅读共有分隔字串起始部分 * 阅读共有分隔字串起始部分
* "##","@@","{{","{[","<js>", "@js:" * "##","@@","{{","{[","<js>", "@js:"
*/ */
val splitList =arrayOf("##","@@","{{","{[","<js>", "@js:") val splitList =arrayOf("##","@@","{{","{[","<js>", "@js:")
/** /**
* 发现名称-链接分隔字串 * 发现名称-链接分隔字串
* "::" * "::"
*/ */
const val splitListFaXian = "::" const val splitListFaXian = "::"
/**
* 目录专有起始字符
* "-"
*/
const val splitListMulu = "-"
/**
* 结果为元素列表的 all in one 模式起始字符
* "+"
*/
const val splitListTongYi = "+"
/**
* 结果为元素列表的项的同规则组合结构
* "||","&&","%%"
*/
val splitListReSplit = arrayOf("||","&&","%%")
/**
* js脚本结束字串
* "</js>"
*/
const val splitListEndJS = "</js>"
/**
*内嵌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脚本结束字串
* "</js>"
*/
const val splitListEndJS = "</js>"
/**
*内嵌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("-",".","!","@","@@")
}}

@ -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.searchContent.SearchContentActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.TocActivityResult 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.login.SourceLoginActivity
import io.legado.app.ui.replace.ReplaceRuleActivity import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.replace.edit.ReplaceEditActivity import io.legado.app.ui.replace.edit.ReplaceEditActivity
@ -531,6 +532,10 @@ class ReadBookActivity : ReadBookBaseActivity(),
openSearchActivity(selectedText) openSearchActivity(selectedText)
return true return true
} }
R.id.menu_dict -> {
DictDialog.dict(supportFragmentManager, selectedText)
return true
}
} }
return false return false
} }

@ -1,11 +1,21 @@
package io.legado.app.ui.dict package io.legado.app.ui.dict
import android.os.Bundle import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment 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() { class DictDialog : BaseDialogFragment() {
companion object { companion object {
@ -21,10 +31,33 @@ class DictDialog : BaseDialogFragment() {
} }
private val viewModel by viewModels<DictViewModel>() private val viewModel by viewModels<DictViewModel>()
private val binding by viewBinding(DialogDictBinding::bind)
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onStart() {
viewModel.word = arguments?.getString("word") ?: "" 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)
} }

@ -1,11 +1,30 @@
package io.legado.app.ui.dict package io.legado.app.ui.dict
import android.app.Application 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<String> = 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)
}
}
} }

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_dict"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

@ -17,6 +17,10 @@
android:id="@+id/menu_aloud" android:id="@+id/menu_aloud"
android:title="@string/read_aloud" /> android:title="@string/read_aloud" />
<item
android:id="@+id/menu_dict"
android:title="@string/dict" />
<item <item
android:id="@+id/menu_search_content" android:id="@+id/menu_search_content"
android:title="@string/search_content" /> android:title="@string/search_content" />

@ -846,4 +846,5 @@
<string name="export_file_name">导出文件名</string> <string name="export_file_name">导出文件名</string>
<string name="reset">重置</string> <string name="reset">重置</string>
<string name="null_url">url为空</string> <string name="null_url">url为空</string>
<string name="dict">字典</string>
</resources> </resources>

@ -846,4 +846,5 @@
<string name="export_file_name">Exportar o nome do arquivo</string> <string name="export_file_name">Exportar o nome do arquivo</string>
<string name="reset">重置</string> <string name="reset">重置</string>
<string name="null_url">url为空</string> <string name="null_url">url为空</string>
<string name="dict">字典</string>
</resources> </resources>

@ -844,5 +844,6 @@
<string name="export_file_name">导出文件名</string> <string name="export_file_name">导出文件名</string>
<string name="reset">重置</string> <string name="reset">重置</string>
<string name="null_url">url为空</string> <string name="null_url">url为空</string>
<string name="dict">字典</string>
</resources> </resources>

@ -845,5 +845,6 @@
<string name="export_file_name">匯出檔案名</string> <string name="export_file_name">匯出檔案名</string>
<string name="reset">重設</string> <string name="reset">重設</string>
<string name="null_url">url为空</string> <string name="null_url">url为空</string>
<string name="dict">字典</string>
</resources> </resources>

@ -845,5 +845,6 @@
<string name="export_file_name">导出文件名</string> <string name="export_file_name">导出文件名</string>
<string name="reset">重置</string> <string name="reset">重置</string>
<string name="null_url">url为空</string> <string name="null_url">url为空</string>
<string name="dict">字典</string>
</resources> </resources>

@ -846,5 +846,6 @@
<string name="export_file_name">Export file name</string> <string name="export_file_name">Export file name</string>
<string name="reset">Reset</string> <string name="reset">Reset</string>
<string name="null_url">Null url</string> <string name="null_url">Null url</string>
<string name="dict">字典</string>
</resources> </resources>

Loading…
Cancel
Save