Kaynağa Gözat

fix(reader): Latin 换行回退不再制造行尾禁则违规

2274 的 safeBreakStart 在词首前是开引号/开括号时会把开标点留在行尾,与 ChineseLineBreaker.isForbiddenBreak 及 ReaderPaginator 的收窄回退相矛盾(实测 50 处违规,基线 0 处)。

现在这种情况退回原有断点:宁可保留中文字符级断行,也不制造新的避头尾违规;同时补充违规断点审计测试与超长词下移行为测试。
HapeLee 1 ay önce
ebeveyn
işleme
4782798ed5

+ 11 - 3
app/src/main/java/io/legado/app/feature/reader/core/layout/ChineseLineBreaker.kt

@@ -139,10 +139,18 @@ class ChineseLineBreaker(
         clusterStarts += cluster
     }
 
-    /** Keeps a candidate line start from splitting a Latin word that fits on a fresh line. */
+    /**
+     * Keeps a candidate line start from splitting a Latin word that fits on a fresh line.
+     *
+     * 回退到词首不得制造新的避头尾违规:词首前一个 cluster 若是开引号/开括号(`opening`),
+     * 回退会让它留在行尾——正是 [isForbiddenBreak] 判定的非法断点(`ReaderPaginator` 的
+     * 收窄回退也依赖该谓词)。此时退回原有断点,宁可保留旧的中文字符级断行,也不制造违规。
+     */
     private fun safeBreakStart(candidate: Int, lineStart: Int): Int {
-        val wordStart = latinWordStartBefore(candidate)
-        return if (wordStart != null && wordStart > lineStart) wordStart else candidate
+        val wordStart = latinWordStartBefore(candidate) ?: return candidate
+        if (wordStart <= lineStart) return candidate
+        if (clusters[wordStart - 1] in opening) return candidate
+        return wordStart
     }
 
     /**

+ 57 - 0
app/src/test/java/io/legado/app/feature/reader/core/layout/ChineseLineBreakerTest.kt

@@ -2,6 +2,7 @@ package io.legado.app.feature.reader.core.layout
 
 import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
 import org.junit.Test
 
 class ChineseLineBreakerTest {
@@ -135,4 +136,60 @@ class ChineseLineBreakerTest {
         assertArrayEquals(intArrayOf(0, 2, 4, 5, 7), result.lineStarts)
         assertArrayEquals(floatArrayOf(20f, 20f, 10f, 20f), result.lineWidthsPx, 0f)
     }
+
+    /**
+     * 超长 Latin 单词前有正文时:先整体移到下一行,再在下一行按字符回退。上一行因此变短,
+     * 这是浏览器 `overflow-wrap: break-word` 的同类行为,锁住以免被当成回退。
+     */
+    @Test
+    fun overlongLatinWordAfterTextMovesDownThenBreaksByCharacter() {
+        val result = breakText("aa supercalifragilistic".map(Char::toString), 80)
+
+        assertArrayEquals(intArrayOf(0, 3, 11, 19, 23), result.lineStarts)
+        assertArrayEquals(floatArrayOf(30f, 80f, 80f, 40f), result.lineWidthsPx, 0f)
+    }
+
+    /**
+     * Latin 词边界回退不得制造避头尾违规:词首前是开引号/开括号时(`isForbiddenBreak` 判定
+     * 非法),必须退回原有断点,否则开标点会落到行尾。
+     */
+    @Test
+    fun latinRewindNeverProducesForbiddenBreak() {
+        val texts = listOf(
+            "aa “word”",
+            "中文“English”中文",
+            "中文(English)中文",
+            "中文「English」中文",
+            "aa (word) bb",
+            "aa 中文“word”",
+            "请阅读“README.md”文件中的说明。",
+            "他打开“Settings”面板,然后点击“About”。",
+            "这本书引用了《The Great Gatsby》里的一句话。",
+        )
+        texts.forEach { text ->
+            val clusters = text.map(Char::toString)
+            listOf(40, 60, 80, 100).forEach { limit ->
+                val starts = breakText(clusters, limit).lineStarts
+                for (index in 1 until starts.size - 1) {
+                    assertFalse(
+                        "「$text」limit=$limit 断点非法:" +
+                                "行尾「${clusters[starts[index] - 1]}」/ 行首「${clusters[starts[index]]}」",
+                        ChineseLineBreaker.isForbiddenBreak(
+                            clusters[starts[index] - 1],
+                            clusters[starts[index]],
+                        ),
+                    )
+                }
+            }
+        }
+    }
+
+    /** 安全回退生效时,原有的 Latin 整词换行收益必须保留。 */
+    @Test
+    fun latinWordBoundaryStillKeepsWordsIntactWithoutOpeningMark() {
+        val result = breakText("中文English中文".map(Char::toString), 80)
+
+        assertArrayEquals(intArrayOf(0, 2, 10, 11), result.lineStarts)
+        assertArrayEquals(floatArrayOf(20f, 80f, 10f), result.lineWidthsPx, 0f)
+    }
 }