This commit is contained in:
Horis 2024-01-28 17:09:25 +08:00
parent b9d6616ca6
commit e3aa72b315
22 changed files with 350 additions and 160 deletions

View File

@ -20,7 +20,6 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import splitties.init.appCtx
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@ -206,10 +205,8 @@ object Backup {
withContext(IO) {
if (list.isNotEmpty()) {
val file = FileUtils.createFileIfNotExist(path + File.separator + fileName)
FileOutputStream(file).use { fos ->
BufferedOutputStream(fos, 64 * 1024).use {
GSON.writeToOutputStream(it, list)
}
file.outputStream().buffered().use {
GSON.writeToOutputStream(it, list)
}
}
}

View File

@ -75,10 +75,8 @@ import me.ag2s.epublib.epub.EpubWriterProcessor
import me.ag2s.epublib.util.ResourceUtil
import splitties.init.appCtx
import splitties.systemservices.notificationManager
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.nio.charset.Charset
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.coroutineContext
@ -257,23 +255,21 @@ class ExportBookService : BaseService() {
val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
val charset = Charset.forName(AppConfig.exportCharset)
contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
BufferedOutputStream(bookOs, 64 * 1024).use { bos ->
getAllContents(book) { text, srcList ->
bos.write(text.toByteArray(charset))
srcList?.forEach {
val vFile = BookHelp.getImage(book, it.src)
if (vFile.exists()) {
DocumentUtils.createFileIfNotExist(
doc,
"${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg",
subDirs = arrayOf(
"${book.name}_${book.author}",
"images",
it.chapterTitle
)
)?.writeBytes(this, vFile.readBytes())
}
contentResolver.openOutputStream(bookDoc.uri, "wa")?.bufferedWriter(charset)?.use { bw ->
getAllContents(book) { text, srcList ->
bw.write(text)
srcList?.forEach {
val vFile = BookHelp.getImage(book, it.src)
if (vFile.exists()) {
DocumentUtils.createFileIfNotExist(
doc,
"${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg",
subDirs = arrayOf(
"${book.name}_${book.author}",
"images",
it.chapterTitle
)
)?.writeBytes(this, vFile.readBytes())
}
}
}
@ -289,10 +285,10 @@ class ExportBookService : BaseService() {
val bookPath = FileUtils.getPath(file, filename)
val bookFile = FileUtils.createFileWithReplace(bookPath)
val charset = Charset.forName(AppConfig.exportCharset)
val bos = BufferedOutputStream(bookFile.outputStream(true), 64 * 1024)
bos.use {
val bw = bookFile.outputStream(true).bufferedWriter(charset)
bw.use {
getAllContents(book) { text, srcList ->
bos.write(text.toByteArray(charset))
it.write(text)
srcList?.forEach {
val vFile = BookHelp.getImage(book, it.src)
if (vFile.exists()) {
@ -315,7 +311,7 @@ class ExportBookService : BaseService() {
private suspend fun getAllContents(
book: Book,
append: (text: String, srcList: ArrayList<SrcData>?) -> Unit
) {
) = coroutineScope {
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
val contentProcessor = ContentProcessor.get(book.name, book.origin)
val qy = "${book.name}\n${
@ -327,34 +323,27 @@ class ExportBookService : BaseService() {
)
}"
append(qy, null)
if (AppConfig.parallelExportBook) {
coroutineScope {
flow {
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
val task = async(Default, start = CoroutineStart.LAZY) {
getExportData(book, chapter, contentProcessor, useReplace)
}
emit(task)
}
}.onEach { it.start() }
.buffer(AppConfig.threadCount)
.map { it.await() }
.withIndex()
.collect { (index, result) ->
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
exportProgress[book.bookUrl] = index
append.invoke(result.first, result.second)
}
}
val threads = if (AppConfig.parallelExportBook) {
AppConst.MAX_THREAD
} else {
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
coroutineContext.ensureActive()
0
}
flow {
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
val task = async(Default, start = CoroutineStart.LAZY) {
getExportData(book, chapter, contentProcessor, useReplace)
}
emit(task)
}
}.onEach { it.start() }
.buffer(threads)
.map { it.await() }
.withIndex()
.collect { (index, result) ->
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
exportProgress[book.bookUrl] = index
val result = getExportData(book, chapter, contentProcessor, useReplace)
append.invoke(result.first, result.second)
}
}
}
@ -364,34 +353,33 @@ class ExportBookService : BaseService() {
contentProcessor: ContentProcessor,
useReplace: Boolean
): Pair<String, ArrayList<SrcData>?> {
BookHelp.getContent(book, chapter).let { content ->
val content1 = contentProcessor
.getContent(
book,
// 不导出vip标识
chapter.apply { isVip = false },
content ?: if (chapter.isVolume) "" else "null",
includeTitle = !AppConfig.exportNoChapterName,
useReplace = useReplace,
chineseConvert = false,
reSegment = false
).toString()
if (AppConfig.exportPictureFile) {
//txt导出图片文件
val srcList = arrayListOf<SrcData>()
content?.split("\n")?.forEachIndexed { index, text ->
val matcher = AppPattern.imgPattern.matcher(text)
while (matcher.find()) {
matcher.group(1)?.let {
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
srcList.add(SrcData(chapter.title, index, src))
}
val content = BookHelp.getContent(book, chapter)
val content1 = contentProcessor
.getContent(
book,
// 不导出vip标识
chapter.apply { isVip = false },
content ?: if (chapter.isVolume) "" else "null",
includeTitle = !AppConfig.exportNoChapterName,
useReplace = useReplace,
chineseConvert = false,
reSegment = false
).toString()
if (AppConfig.exportPictureFile) {
//txt导出图片文件
val srcList = arrayListOf<SrcData>()
content?.split("\n")?.forEachIndexed { index, text ->
val matcher = AppPattern.imgPattern.matcher(text)
while (matcher.find()) {
matcher.group(1)?.let {
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
srcList.add(SrcData(chapter.title, index, src))
}
}
return Pair("\n\n$content1", srcList)
} else {
return Pair("\n\n$content1", null)
}
return Pair("\n\n$content1", srcList)
} else {
return Pair("\n\n$content1", null)
}
}
@ -463,8 +451,8 @@ class ExportBookService : BaseService() {
//设置正文
setEpubContent(contentModel, book, epubBook)
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
EpubWriter().write(epubBook, BufferedOutputStream(bookOs))
contentResolver.openOutputStream(bookDoc.uri, "wa")?.buffered().use { bookOs ->
EpubWriter().write(epubBook, bookOs)
}
if (AppConfig.exportToWebDav) {
// 导出到webdav
@ -489,8 +477,7 @@ class ExportBookService : BaseService() {
val bookFile = FileUtils.createFileWithReplace(bookPath)
//设置正文
setEpubContent(contentModel, book, epubBook)
@Suppress("BlockingMethodInNonBlockingContext")
EpubWriter().write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
EpubWriter().write(epubBook, bookFile.outputStream().buffered())
if (AppConfig.exportToWebDav) {
// 导出到webdav
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
@ -647,59 +634,79 @@ class ExportBookService : BaseService() {
contentModel: String,
book: Book,
epubBook: EpubBook
) {
) = coroutineScope {
//正文
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
val contentProcessor = ContentProcessor.get(book.name, book.origin)
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
coroutineContext.ensureActive()
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
exportProgress[book.bookUrl] = index
BookHelp.getContent(book, chapter).let { content ->
var content1 = fixPic(
epubBook,
book,
content ?: if (chapter.isVolume) "" else "null",
chapter
)
content1 = contentProcessor
.getContent(
val threads = if (AppConfig.parallelExportBook) {
AppConst.MAX_THREAD
} else {
0
}
flow {
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
val task = async(Default, start = CoroutineStart.LAZY) {
val content = BookHelp.getContent(book, chapter)
val (contentFix, resources) = fixPic(
book,
chapter,
content1,
includeTitle = false,
useReplace = useReplace,
chineseConvert = false,
reSegment = false
).toString()
val title = chapter.run {
// 不导出vip标识
isVip = false
getDisplayTitle(
contentProcessor.getTitleReplaceRules(),
useReplace = useReplace
content ?: if (chapter.isVolume) "" else "null",
chapter
)
}
epubBook.addSection(
title,
ResourceUtil.createChapterResource(
// 不导出vip标识
chapter.isVip = false
val content1 = contentProcessor
.getContent(
book,
chapter,
contentFix,
includeTitle = false,
useReplace = useReplace,
chineseConvert = false,
reSegment = false
).toString()
val title = chapter.run {
// 不导出vip标识
isVip = false
getDisplayTitle(
contentProcessor.getTitleReplaceRules(),
useReplace = useReplace
)
}
val chapterResource = ResourceUtil.createChapterResource(
title.replace("\uD83D\uDD12", ""),
content1,
contentModel,
"Text/chapter_${index}.html"
)
)
ExportChapter(title, chapterResource, resources)
}
emit(task)
}
}.onEach { it.start() }
.buffer(threads)
.map { it.await() }
.withIndex()
.collect { (index, exportChapter) ->
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
exportProgress[book.bookUrl] = index
epubBook.resources.addAll(exportChapter.resources)
epubBook.addSection(exportChapter.title, exportChapter.chapterResource)
}
}
}
data class ExportChapter(
val title: String,
val chapterResource: Resource,
val resources: ArrayList<Resource>
)
private fun fixPic(
epubBook: EpubBook,
book: Book,
content: String,
chapter: BookChapter
): String {
): Pair<String, ArrayList<Resource>> {
val data = StringBuilder("")
val resources = arrayListOf<Resource>()
content.split("\n").forEach { text ->
var text1 = text
val matcher = AppPattern.imgPattern.matcher(text)
@ -714,14 +721,14 @@ class ExportBookService : BaseService() {
val fp = FileResourceProvider(vFile.parent)
if (vFile.exists()) {
val img = LazyResource(fp, href, originalHref)
epubBook.resources.add(img)
resources.add(img)
}
text1 = text1.replace(src, "../${href}")
}
}
data.append(text1).append("\n")
}
return data.toString()
return data.toString() to resources
}
private fun setEpubMetadata(book: Book, epubBook: EpubBook) {
@ -875,17 +882,17 @@ class ExportBookService : BaseService() {
coroutineContext.ensureActive()
updateProgress(chapterList, index)
BookHelp.getContent(book, chapter).let { content ->
var content1 = fixPic(
epubBook,
val (contentFix, resources) = fixPic(
book,
content ?: if (chapter.isVolume) "" else "null",
chapter
)
content1 = contentProcessor
epubBook.resources.addAll(resources)
val content1 = contentProcessor
.getContent(
book,
chapter,
content1,
contentFix,
includeTitle = false,
useReplace = useReplace,
chineseConvert = false,
@ -960,14 +967,14 @@ class ExportBookService : BaseService() {
callback: (total: Int, progress: Int) -> Unit
) {
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
contentResolver.openOutputStream(bookDoc.uri, "wa")?.buffered().use { bookOs ->
EpubWriter()
.setCallback(object : EpubWriterProcessor.Callback {
override fun onProgressing(total: Int, progress: Int) {
callback(total, progress)
}
})
.write(epubBook, BufferedOutputStream(bookOs))
.write(epubBook, bookOs)
}
if (AppConfig.exportToWebDav) {
// 导出到webdav
@ -987,14 +994,13 @@ class ExportBookService : BaseService() {
) {
val bookPath = FileUtils.getPath(file, filename)
val bookFile = FileUtils.createFileWithReplace(bookPath)
@Suppress("BlockingMethodInNonBlockingContext")
EpubWriter()
.setCallback(object : EpubWriterProcessor.Callback {
override fun onProgressing(total: Int, progress: Int) {
callback(total, progress)
}
})
.write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
.write(epubBook, bookFile.outputStream().buffered())
if (AppConfig.exportToWebDav) {
// 导出到webdav
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)

View File

@ -22,9 +22,7 @@ import io.legado.app.utils.stackTraceStr
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.writeToOutputStream
import kotlinx.coroutines.delay
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
class BookshelfManageViewModel(application: Application) : BaseViewModel(application) {
@ -64,17 +62,14 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica
}
}
@Suppress("BlockingMethodInNonBlockingContext")
fun saveAllUseBookSourceToFile(success: (file: File) -> Unit) {
execute {
val path = "${context.filesDir}/shareBookSource.json"
FileUtils.delete(path)
val file = FileUtils.createFileWithReplace(path)
val sources = appDb.bookDao.getAllUseBookSource()
FileOutputStream(file).use { out ->
BufferedOutputStream(out, 64 * 1024).use {
GSON.writeToOutputStream(it, sources)
}
file.outputStream().buffered().use {
GSON.writeToOutputStream(it, sources)
}
file
}.onSuccess {

View File

@ -9,11 +9,16 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.toBookSource
import io.legado.app.help.config.SourceConfig
import io.legado.app.utils.*
import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON
import io.legado.app.utils.cnCompare
import io.legado.app.utils.outputStream
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.stackTraceStr
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.writeToOutputStream
import splitties.init.appCtx
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
/**
* 书源管理数据修改
@ -121,16 +126,13 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private fun saveToFile(sources: List<BookSource>, success: (file: File) -> Unit) {
execute {
val path = "${context.filesDir}/shareBookSource.json"
FileUtils.delete(path)
val file = FileUtils.createFileWithReplace(path)
FileOutputStream(file).use { out ->
BufferedOutputStream(out, 64 * 1024).use {
GSON.writeToOutputStream(it, sources)
}
file.outputStream().buffered().use {
GSON.writeToOutputStream(it, sources)
}
file
}.onSuccess {

View File

@ -0,0 +1,30 @@
package io.legado.app.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import org.mozilla.javascript.Context
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
inline fun <T> suspendContinuation(crossinline block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val cx = Context.enter()
try {
val pending = cx.captureContinuation()
pending.applicationState = suspend {
supervisorScope {
block()
}
}
throw pending
} catch (e: IllegalStateException) {
return runBlocking { block() }
} finally {
Context.exit()
}
}

View File

@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
<menu 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"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/menu_download"
android:title="@string/action_download"
android:icon="@drawable/ic_play_24dp"
android:title="@string/action_download"
app:showAsAction="always" />
<item
android:id="@+id/menu_book_group"
android:title="@string/group"
android:icon="@drawable/ic_groups"
android:title="@string/group"
app:showAsAction="always">
<menu />
@ -27,39 +27,39 @@
<item
android:id="@+id/menu_enable_replace"
android:title="@string/replace_purify"
android:checkable="true"
android:title="@string/replace_purify"
app:showAsAction="never" />
<!--自定义导出章节 选择菜单-->
<item
android:id="@+id/menu_enable_custom_export"
android:title="@string/custom_export_section"
android:checkable="true"
android:title="@string/custom_export_section"
app:showAsAction="never" />
<item
android:id="@+id/menu_export_web_dav"
android:title="@string/export_to_web_dav"
android:checkable="true"
android:title="@string/export_to_web_dav"
app:showAsAction="never" />
<item
android:id="@+id/menu_export_no_chapter_name"
android:title="@string/export_no_chapter_name"
android:checkable="true"
android:title="@string/export_no_chapter_name"
app:showAsAction="never" />
<item
android:id="@+id/menu_export_pics_file"
android:title="@string/export_pics_file"
android:checkable="true"
android:title="@string/export_pics_file"
app:showAsAction="never" />
<item
android:id="@+id/menu_parallel_export"
android:title="@string/parallel_export_book"
android:checkable="true"
android:title="@string/parallel_export_book"
app:showAsAction="never" />
<item

View File

@ -1038,7 +1038,7 @@
<string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
<string name="net_no_group">网络未分组</string>
<string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
<string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1041,7 +1041,7 @@
<string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
<string name="net_no_group">网络未分组</string>
<string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
<string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1041,7 +1041,7 @@
<string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
<string name="net_no_group">网络未分组</string>
<string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
<string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1037,7 +1037,7 @@ Còn </string>
<string name="cover_decode_js">Giải mã Bìa Js(coverDecodeJs)</string>
<string name="net_no_group">Đã tách nhóm mạng</string>
<string name="local_no_group">Đã tách nhóm cục bộ</string>
<string name="parallel_export_book">XT xuất đa luồng</string>
<string name="parallel_export_book">Xuất đa luồng</string>
<string name="progress_bar_behavior">Hành vi của thanh tiến trình</string>
<string name="source_edit_text_max_line">Số hàng tối đa trong hộp chỉnh sửa nguồn</string>
<string name="source_edit_max_line_summary">%s,Đặt số hàng ít hơn số hàng tối đa có thể hiển thị trên màn hình giúp trượt sang các trường khác để chỉnh sửa dễ dàng hơn.</string>

View File

@ -1038,7 +1038,7 @@
<string name="cover_decode_js">封面解密coverDecodeJs</string>
<string name="net_no_group">网络未分组</string>
<string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
<string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1040,7 +1040,7 @@
<string name="cover_decode_js">封面解密coverDecodeJs</string>
<string name="net_no_group">網路未分組</string>
<string name="local_no_group">本機未分組</string>
<string name="parallel_export_book">多執行緒匯出TXT</string>
<string name="parallel_export_book">多執行緒匯出</string>
<string name="progress_bar_behavior">進度條行為</string>
<string name="source_edit_text_max_line">源編輯框最大行數</string>
<string name="source_edit_max_line_summary">%s設定行數小於螢幕可顯示的最大行數可以更方便的滑動到其他的欄位進行編輯</string>

View File

@ -1041,7 +1041,7 @@
<string name="cover_decode_js">封面解密coverDecodeJs</string>
<string name="net_no_group">网络未分组</string>
<string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
<string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1041,7 +1041,7 @@
<string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
<string name="net_no_group">Network ungrouped</string>
<string name="local_no_group">Local ungrouped</string>
<string name="parallel_export_book">Multithreaded export TXT</string>
<string name="parallel_export_book">Multithreaded export</string>
<string name="progress_bar_behavior">Progress bar behavior</string>
<string name="source_edit_text_max_line">Maximum number of rows in the source edit box</string>
<string name="source_edit_max_line_summary">%s,Setting the number of rows less than the maximum number of rows that can be displayed on the screen makes it easier to slide to other fields for editing.</string>

View File

@ -32,4 +32,7 @@ android {
dependencies {
api(fileTree(dir: 'lib', include: ['rhino-1.7.13-2.jar']))
def coroutines_version = '1.7.3'
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
}

View File

@ -51,6 +51,10 @@ abstract class AbstractScriptEngine(val bindings: Bindings? = null) : ScriptEngi
return getBindings(100)?.get(key)
}
override suspend fun evalSuspend(script: String, scope: Scriptable): Any? {
return this.evalSuspend(StringReader(script), scope)
}
override fun eval(script: String, scope: Scriptable): Any? {
return this.eval(StringReader(script), scope)
}

View File

@ -15,6 +15,9 @@ abstract class CompiledScript {
@Throws(ScriptException::class)
abstract fun eval(scope: Scriptable): Any?
@Throws(ScriptException::class)
abstract suspend fun evalSuspend(scope: Scriptable): Any?
@Throws(ScriptException::class)
fun eval(bindings: Bindings?): Any? {
var ctxt = getEngine().context

View File

@ -14,6 +14,12 @@ interface ScriptEngine {
@Throws(ScriptException::class)
fun eval(reader: Reader, scope: Scriptable): Any?
@Throws(ScriptException::class)
suspend fun evalSuspend(reader: Reader, scope: Scriptable): Any?
@Throws(ScriptException::class)
suspend fun evalSuspend(script: String, scope: Scriptable): Any?
@Throws(ScriptException::class)
fun eval(script: String, scope: Scriptable): Any?

View File

@ -0,0 +1,25 @@
package com.script.rhino
import kotlinx.coroutines.ThreadContextElement
import kotlin.coroutines.CoroutineContext
class ContextElement : ThreadContextElement<Any?> {
companion object Key : CoroutineContext.Key<ContextElement>
override val key: CoroutineContext.Key<ContextElement>
get() = Key
private val contextHelper: Any? = VMBridgeReflect.contextLocal.get()
override fun updateThreadContext(context: CoroutineContext): Any? {
val oldState = VMBridgeReflect.contextLocal.get()
VMBridgeReflect.contextLocal.set(contextHelper)
return oldState
}
override fun restoreThreadContext(context: CoroutineContext, oldState: Any?) {
VMBridgeReflect.contextLocal.set(oldState)
}
}

View File

@ -28,7 +28,11 @@ import com.script.CompiledScript
import com.script.ScriptContext
import com.script.ScriptEngine
import com.script.ScriptException
import kotlinx.coroutines.withContext
import org.mozilla.javascript.*
import java.io.IOException
import kotlin.coroutines.Continuation
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
/**
* Represents compiled JavaScript code.
@ -91,4 +95,48 @@ internal class RhinoCompiledScript(
return result
}
override suspend fun evalSuspend(scope: Scriptable): Any? {
val cx = Context.enter()
var ret: Any?
withContext(ContextElement()) {
try {
try {
ret = cx.executeScriptWithContinuations(script, scope)
} catch (e: ContinuationPending) {
var pending = e
while (true) {
try {
@Suppress("UNCHECKED_CAST")
val suspendFunction =
pending.applicationState as Function1<Continuation<Any?>, Any?>
val functionResult = suspendCoroutineUninterceptedOrReturn { cout ->
suspendFunction.invoke(cout)
}
val continuation = pending.continuation
ret = cx.resumeContinuation(continuation, scope, functionResult)
break
} catch (e: ContinuationPending) {
pending = e
}
}
}
} catch (re: RhinoException) {
val line = if (re.lineNumber() == 0) -1 else re.lineNumber()
val msg: String = if (re is JavaScriptException) {
re.value.toString()
} else {
re.toString()
}
val se = ScriptException(msg, re.sourceName(), line)
se.initCause(re)
throw se
} catch (var14: IOException) {
throw ScriptException(var14)
} finally {
Context.exit()
}
}
return engine.unwrapReturnValue(ret)
}
}

View File

@ -25,6 +25,7 @@
package com.script.rhino
import com.script.*
import kotlinx.coroutines.withContext
import org.mozilla.javascript.*
import org.mozilla.javascript.Function
import java.io.IOException
@ -32,6 +33,8 @@ import java.io.Reader
import java.io.StringReader
import java.lang.reflect.Method
import java.security.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
/**
* Implementation of `ScriptEngine` using the Mozilla Rhino
@ -80,6 +83,54 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
return unwrapReturnValue(ret)
}
@Throws(ContinuationPending::class)
override suspend fun evalSuspend(reader: Reader, scope: Scriptable): Any? {
val cx = Context.enter()
var ret: Any?
withContext(ContextElement()) {
try {
var filename = this@RhinoScriptEngine["javax.script.filename"] as? String
filename = filename ?: "<Unknown source>"
val script = cx.compileReader(reader, filename, 1, null)
try {
ret = cx.executeScriptWithContinuations(script, scope)
} catch (e: ContinuationPending) {
var pending = e
while (true) {
try {
@Suppress("UNCHECKED_CAST")
val suspendFunction =
pending.applicationState as Function1<Continuation<Any?>, Any?>
val functionResult = suspendCoroutineUninterceptedOrReturn { cout ->
suspendFunction.invoke(cout)
}
val continuation = pending.continuation
ret = cx.resumeContinuation(continuation, scope, functionResult)
break
} catch (e: ContinuationPending) {
pending = e
}
}
}
} catch (re: RhinoException) {
val line = if (re.lineNumber() == 0) -1 else re.lineNumber()
val msg: String = if (re is JavaScriptException) {
re.value.toString()
} else {
re.toString()
}
val se = ScriptException(msg, re.sourceName(), line)
se.initCause(re)
throw se
} catch (var14: IOException) {
throw ScriptException(var14)
} finally {
Context.exit()
}
}
return unwrapReturnValue(ret)
}
override fun createBindings(): Bindings {
return SimpleBindings()
}
@ -227,7 +278,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
callable: Callable,
cx: Context,
scope: Scriptable,
thisObj: Scriptable,
thisObj: Scriptable?,
args: Array<Any>
): Any? {
var accContext: AccessControlContext? = null
@ -253,7 +304,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
callable: Callable,
cx: Context,
scope: Scriptable,
thisObj: Scriptable,
thisObj: Scriptable?,
args: Array<Any>
): Any? {
return super.doTopCall(callable, cx, scope, thisObj, args)

View File

@ -0,0 +1,20 @@
package com.script.rhino
import org.mozilla.javascript.VMBridge
object VMBridgeReflect {
val instance: VMBridge by lazy {
VMBridge::class.java.getDeclaredField("instance").apply {
isAccessible = true
}.get(null) as VMBridge
}
val contextLocal: ThreadLocal<Any> by lazy {
@Suppress("UNCHECKED_CAST")
instance::class.java.getDeclaredField("contextLocal").apply {
isAccessible = true
}.get(null) as ThreadLocal<Any>
}
}