Merge pull request #1292 from woruteyqi/fix/cross-paragraph-regex

支持regex全文匹配
This commit is contained in:
Kudomaga
2026-06-06 03:08:28 +08:00
committed by GitHub
@@ -134,6 +134,8 @@ class TextChapterLayout(
var channel = Channel<TextPage>(Channel.UNLIMITED) var channel = Channel<TextPage>(Channel.UNLIMITED)
private var globalRegexResult: RegexMatchResult? = null
init { init {
job = Coroutine.async( job = Coroutine.async(
@@ -241,8 +243,8 @@ class TextChapterLayout(
val isSingleImageStyle = imageStyle.equals(Book.imgStyleSingle, true) val isSingleImageStyle = imageStyle.equals(Book.imgStyleSingle, true)
val isTextImageStyle = imageStyle.equals(Book.imgStyleText, true) val isTextImageStyle = imageStyle.equals(Book.imgStyleText, true)
if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) { val allTitleSegments = if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
val allTitleSegments = displayTitle.splitNotBlank("\n").flatMap { rawTitle -> displayTitle.splitNotBlank("\n").flatMap { rawTitle ->
TitleStyleParser.getSegments( TitleStyleParser.getSegments(
rawTitle, rawTitle,
titleSegType, titleSegType,
@@ -251,7 +253,66 @@ class TextChapterLayout(
titleSegScaling titleSegScaling
) )
} }
} else null
if (ReadBookConfig.regexColorRules.isNotEmpty()) {
val fullTextBuilder = StringBuilder()
allTitleSegments?.forEachIndexed { index, segment ->
val reviewImg = bookChapter.reviewImg
var reviewTxt = ""
if (index == allTitleSegments.lastIndex && reviewImg != null) {
reviewTxt = if (reviewImg.contains("TEXT")) reviewChar else srcReplaceChar
}
fullTextBuilder.append(segment.text).append(reviewTxt).append("\n")
}
contents.forEach { content ->
if (adaptSpecialStyle) {
val t = content.trim()
if (t == "[newpage]" || t.startsWith("<usehtml>")) {
fullTextBuilder.append(content).append("\n")
return@forEach
}
}
val text = content.replace(srcReplaceCharC, srcReplaceCharD)
if (isTextImageStyle) {
val matcher = AppPattern.imgPattern.matcher(text)
val ssb = StringBuffer()
while (matcher.find()) {
if (matcher.group(1) != null) {
matcher.appendReplacement(ssb, srcReplaceChar)
}
}
matcher.appendTail(ssb)
fullTextBuilder.append(ssb.toString())
} else {
val matcher = AppPattern.imgPattern.matcher(text)
var start = 0
while (matcher.find()) {
val imgSrc = matcher.group(1) ?: continue
val iStyle = if (imgSrc.contains("TEXT")) "text" else imageStyle
if (start < matcher.start()) {
fullTextBuilder.append(text.substring(start, matcher.start()))
}
if (iStyle == "text" || iStyle == "TEXT") {
fullTextBuilder.append(if (iStyle == "TEXT") reviewChar else srcReplaceChar)
} else {
fullTextBuilder.append(" ")
}
start = matcher.end()
}
if (start < text.length) {
fullTextBuilder.append(text.substring(start))
}
if (AppConfig.enableReview) fullTextBuilder.append(reviewChar)
}
fullTextBuilder.append("\n")
}
preApplyRegexColorRules(fullTextBuilder.toString())
}
var currentOffset = 0
if (allTitleSegments != null) {
allTitleSegments.forEachIndexed { index, segment -> allTitleSegments.forEachIndexed { index, segment ->
val currentPaint: TextPaint val currentPaint: TextPaint
val currentHeight: Float val currentHeight: Float
@@ -277,9 +338,10 @@ class TextChapterLayout(
reviewTxt = if (reviewImg.contains("TEXT")) reviewChar else srcReplaceChar reviewTxt = if (reviewImg.contains("TEXT")) reviewChar else srcReplaceChar
} }
val text = segment.text + reviewTxt
setTypeText( setTypeText(
book = book, book = book,
text = segment.text + reviewTxt, text = text,
textPaint = currentPaint, textPaint = currentPaint,
textHeight = currentHeight, textHeight = currentHeight,
fontMetrics = currentMetrics, fontMetrics = currentMetrics,
@@ -287,8 +349,10 @@ class TextChapterLayout(
srcList = srcList.ifEmpty { null }, srcList = srcList.ifEmpty { null },
isTitle = true, isTitle = true,
emptyContent = contents.isEmpty(), emptyContent = contents.isEmpty(),
isVolumeTitle = bookChapter.isVolume isVolumeTitle = bookChapter.isVolume,
offset = currentOffset
) )
currentOffset += text.length + 1
if (segment.scale != 1.0f) { if (segment.scale != 1.0f) {
val currentLines = pendingTextPage.lines val currentLines = pendingTextPage.lines
@@ -319,9 +383,11 @@ class TextChapterLayout(
val text = content.trim() val text = content.trim()
if (text == "[newpage]") { if (text == "[newpage]") {
prepareNextPageIfNeed() prepareNextPageIfNeed()
currentOffset += content.length + 1
return@forEach return@forEach
} else if (text.startsWith("<usehtml>")) { } else if (text.startsWith("<usehtml>")) {
setTypeHtml(imageStyle, book, text.substring(9, text.lastIndexOf("<"))) setTypeHtml(imageStyle, book, text.substring(9, text.lastIndexOf("<")))
currentOffset += content.length + 1
return@forEach return@forEach
} }
} }
@@ -347,8 +413,10 @@ class TextChapterLayout(
contentPaintTextHeight, contentPaintTextHeight,
contentPaintFontMetrics, contentPaintFontMetrics,
imageStyle, imageStyle,
srcList = srcList srcList = srcList,
offset = currentOffset
) )
currentOffset += text.length
} else { } else {
if (isSingleImageStyle && isSetTypedImage) { if (isSingleImageStyle && isSetTypedImage) {
isSetTypedImage = false isSetTypedImage = false
@@ -402,10 +470,12 @@ class TextChapterLayout(
} }
if (start < matcher.start()) { if (start < matcher.start()) {
sb.append(text.substring(start, matcher.start())) val textPart = text.substring(start, matcher.start())
sb.append(textPart)
} }
if (iStyle == "text" || iStyle == "TEXT") { if (iStyle == "text" || iStyle == "TEXT") {
sb.append(if (iStyle == "TEXT") reviewChar else srcReplaceChar) val charPart = if (iStyle == "TEXT") reviewChar else srcReplaceChar
sb.append(charPart)
srcList.add(imgSrc) srcList.add(imgSrc)
clickList.add(click) clickList.add(click)
} else { } else {
@@ -413,10 +483,12 @@ class TextChapterLayout(
if (textBefore.isNotBlank()) { if (textBefore.isNotBlank()) {
wordCount += textBefore.replace(noWordCountRegex,"").length wordCount += textBefore.replace(noWordCountRegex,"").length
setTypeText( setTypeText(
book, sb.toString(), contentPaint, contentPaintTextHeight, book, textBefore, contentPaint, contentPaintTextHeight,
contentPaintFontMetrics, "TEXT", isFirstLine = isFirstLine, contentPaintFontMetrics, "TEXT", isFirstLine = isFirstLine,
srcList = srcList, clickList = clickList srcList = srcList, clickList = clickList,
offset = currentOffset
) )
currentOffset += textBefore.length
sb.setLength(0) sb.setLength(0)
isFirstLine = false isFirstLine = false
} }
@@ -428,6 +500,7 @@ class TextChapterLayout(
imgSize, imgSize,
click click
) // 传递点击信息 ) // 传递点击信息
currentOffset += 1
isSetTypedImage = true isSetTypedImage = true
} }
start = matcher.end() start = matcher.end()
@@ -444,21 +517,25 @@ class TextChapterLayout(
text = sb.toString() text = sb.toString()
if (text.isNotBlank()) { if (text.isNotBlank()) {
wordCount += text.replace(noWordCountRegex,"").length wordCount += text.replace(noWordCountRegex,"").length
val textToType = if (AppConfig.enableReview) text + reviewChar else text
setTypeText( setTypeText(
book, book,
if (AppConfig.enableReview) text + reviewChar else text, textToType,
contentPaint, contentPaint,
contentPaintTextHeight, contentPaintTextHeight,
contentPaintFontMetrics, contentPaintFontMetrics,
"TEXT", "TEXT",
isFirstLine = isFirstLine, isFirstLine = isFirstLine,
srcList = srcList.ifEmpty { null }, srcList = srcList.ifEmpty { null },
clickList = clickList.ifEmpty { null } clickList = clickList.ifEmpty { null },
offset = currentOffset
) )
currentOffset += textToType.length
} }
} }
pendingTextPage.lines.last().isParagraphEnd = true pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n") stringBuilder.append("\n")
currentOffset += 1
} }
val chapterWordCount = StringUtils.wordCountFormat(wordCount.toString()) val chapterWordCount = StringUtils.wordCountFormat(wordCount.toString())
bookChapter.wordCount = chapterWordCount bookChapter.wordCount = chapterWordCount
@@ -868,11 +945,12 @@ class TextChapterLayout(
emptyContent: Boolean = false, emptyContent: Boolean = false,
isVolumeTitle: Boolean = false, isVolumeTitle: Boolean = false,
srcList: LinkedList<String>? = null, srcList: LinkedList<String>? = null,
clickList: LinkedList<String?>? = null clickList: LinkedList<String?>? = null,
offset: Int = -1
) { ) {
val widthsArray = allocateFloatArray(text.length) val widthsArray = allocateFloatArray(text.length)
textPaint.getTextWidthsCompat(text, widthsArray) textPaint.getTextWidthsCompat(text, widthsArray)
val colorMap = applyRegexColorRules(text) val colorMap = applyRegexColorRules(text, offset)
val layout = if (useZhLayout) { val layout = if (useZhLayout) {
val (words, widths) = measureTextSplit(text, widthsArray) val (words, widths) = measureTextSplit(text, widthsArray)
val indentSize = if (isFirstLine) paragraphIndent.length else 0 val indentSize = if (isFirstLine) paragraphIndent.length else 0
@@ -1310,28 +1388,21 @@ class TextChapterLayout(
val fontPathArray: Array<String?> val fontPathArray: Array<String?>
) )
private fun applyRegexColorRules(text: String): RegexMatchResult? { private fun preApplyRegexColorRules(fullText: String) {
val rules = ReadBookConfig.regexColorRules val rules = ReadBookConfig.regexColorRules
if (rules.isEmpty()) return null if (rules.isEmpty()) return
val colorArray = IntArray(fullText.length) { -1 }
val fontPathArray = arrayOfNulls<String>(fullText.length)
var hasMatch = false var hasMatch = false
for (rule in rules) { for (rule in rules) {
try { try {
val regex = regexCache.getOrPut(rule.pattern) { Regex(rule.pattern) } val regex = synchronized(regexCache) {
if (regex.containsMatchIn(text)) { regexCache.getOrPut(rule.pattern) {
hasMatch = true Regex(rule.pattern, RegexOption.DOT_MATCHES_ALL)
break }
} }
} catch (_: Exception) { regex.findAll(fullText).forEach { match ->
} hasMatch = true
}
if (!hasMatch) return null
val colorArray = IntArray(text.length) { -1 }
val fontPathArray = arrayOfNulls<String>(text.length)
for (rule in rules) {
try {
val regex = regexCache.getOrPut(rule.pattern) { Regex(rule.pattern) }
val matches = regex.findAll(text)
for (match in matches) {
for (i in match.range) { for (i in match.range) {
colorArray[i] = rule.color colorArray[i] = rule.color
if (rule.fontPath.isNotEmpty()) { if (rule.fontPath.isNotEmpty()) {
@@ -1342,6 +1413,34 @@ class TextChapterLayout(
} catch (_: Exception) { } catch (_: Exception) {
} }
} }
if (hasMatch) globalRegexResult = RegexMatchResult(colorArray, fontPathArray)
}
private fun applyRegexColorRules(text: String, offset: Int): RegexMatchResult? {
val globalResult = globalRegexResult ?: return null
if (offset < 0) return null
val endIdx = minOf(offset + text.length, globalResult.colorArray.size)
var hasMatch = false
for (globalIdx in offset until endIdx) {
if (globalResult.colorArray[globalIdx] != -1 || globalResult.fontPathArray[globalIdx] != null) {
hasMatch = true
break
}
}
if (!hasMatch) return null
val colorArray = IntArray(text.length) { -1 }
val fontPathArray = arrayOfNulls<String>(text.length)
for (i in text.indices) {
val globalIdx = offset + i
if (globalIdx >= 0 && globalIdx < globalResult.colorArray.size) {
if (globalResult.colorArray[globalIdx] != -1) {
colorArray[i] = globalResult.colorArray[globalIdx]
}
if (globalResult.fontPathArray[globalIdx] != null) {
fontPathArray[i] = globalResult.fontPathArray[globalIdx]
}
}
}
return RegexMatchResult(colorArray, fontPathArray) return RegexMatchResult(colorArray, fontPathArray)
} }