Просмотр исходного кода

fix(book): 简介统一段首缩进并过滤书籍信息字段

详情页与书架的简介走 formatDisplayText, 之前 paragraphIndent 传空,
既不补缩进, 各来源自带的全角/半角/nbsp 缩进又宽窄不一, 观感是顶格。

改为出口处按行归一化: 先去掉行首行尾各类空白(含全角及特殊宽度空格),
再统一补两个全角空格, 结果幂等, 二次处理不会叠加。

同时丢弃与书籍信息重复的字段行(书名/作者/分类/最新章节等),
并剥掉简介自身的标题前缀。字段名后须紧跟冒号且整行不超过 40 字,
避免误删以这些词开头的正文。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5151561 3 месяцев назад
Родитель
Сommit
e71de0f89e

+ 26 - 0
app/src/main/java/io/legado/app/utils/HtmlFormatter.kt

@@ -21,6 +21,24 @@ object HtmlFormatter {
     private val indent1Regex = "\\s*\\n+\\s*".toRegex()
     private val indent2Regex = "^[\\n\\s]+".toRegex()
     private val lastRegex = "[\\n\\s]+$".toRegex()
+    private const val PARAGRAPH_INDENT = "  "
+
+    //半角空白由 indent 正则处理, 这里去掉全角及特殊宽度空格
+    private val blankChars = charArrayOf(
+        ' ', '\t', '\u00a0', '\u2002', '\u2003', '\u2009', '\u3000'
+    )
+
+    //简介里重复书籍信息的字段, 整行丢弃
+    private const val metaLabels =
+        "书名|名称|书籍名称|作者|译者|分类|类别|类型|题材|状态|连载状态|字数|标签|关键字|来源|首发|" +
+            "更新时间|最后更新|最近更新|最新章节|最新更新"
+
+    //简介自身的标题, 只去标题保留正文
+    private const val introLabels = "内容简介|作品简介|小说简介|内容介绍|简介|文案|摘要"
+    private val metaLineRegex =
+        "^(?:[【\\[((](?:$metaLabels)[】\\]))]|(?:$metaLabels)\\s*[::])\\s*.{0,40}$".toRegex()
+    private val introLabelRegex =
+        "^(?:[【\\[((](?:$introLabels)[】\\]))]|(?:$introLabels)\\s*[::])\\s*".toRegex()
 
     fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String =
         format(html, otherRegex, "  ")
@@ -41,6 +59,8 @@ object HtmlFormatter {
     /**
      * Formats untrusted HTML for plain-text UI surfaces.
      * Script and style elements must be removed with their contents before stripping tags.
+     * 各来源的段首缩进宽度不一, 先清空再统一补两个全角空格。
+     * 同时丢弃与书籍信息重复的字段行(书名/作者/最新章节等)。
      */
     fun formatDisplayText(html: String?): String {
         if (html.isNullOrBlank()) return ""
@@ -49,6 +69,12 @@ object HtmlFormatter {
         val body = document.body()
         body.select("script, style, noscript").remove()
         return format(body.html(), otherHtmlRegex, "")
+            .lineSequence()
+            .map { it.trim(*blankChars) }
+            .filterNot { it.isEmpty() || metaLineRegex.matches(it) }
+            .map { it.replaceFirst(introLabelRegex, "").trim(*blankChars) }
+            .filterNot { it.isEmpty() }
+            .joinToString("\n") { PARAGRAPH_INDENT + it }
     }
 
     fun formatKeepImg(html: String?, redirectUrl: URL? = null): String {

+ 20 - 3
app/src/test/java/io/legado/app/utils/HtmlFormatterTest.kt

@@ -18,7 +18,7 @@ class HtmlFormatterTest {
             """.trimIndent()
         )
 
-        assertEquals("第一段\n第二段", result)
+        assertEquals("  第一段\n  第二段", result)
         assertTrue(result.contains("第一段"))
         assertTrue(result.contains("第二段"))
         assertFalse(result.contains("color: red"))
@@ -27,13 +27,30 @@ class HtmlFormatterTest {
 
     @Test
     fun formatDisplayText_keepsPlainText() {
-        assertEquals("普通简介", HtmlFormatter.formatDisplayText("普通简介"))
+        assertEquals("  普通简介", HtmlFormatter.formatDisplayText("普通简介"))
+    }
+
+    @Test
+    fun formatDisplayText_dropsBookMetaLines() {
+        val result = HtmlFormatter.formatDisplayText(
+            "书名:某某传<br>作者:张三<br>【分类】玄幻<br>最新章节:第一千章 大结局<br>简介:这是正文第一段<br>这是正文第二段"
+        )
+
+        assertEquals("  这是正文第一段\n  这是正文第二段", result)
+    }
+
+    @Test
+    fun formatDisplayText_keepsContentStartingWithMetaWord() {
+        assertEquals(
+            "  作者的话:这本书写了三年",
+            HtmlFormatter.formatDisplayText("作者的话:这本书写了三年")
+        )
     }
 
     @Test
     fun formatDisplayText_keepsPlainTextLineBreaks() {
         assertEquals(
-            "第一段\n第二段",
+            "  第一段\n  第二段",
             HtmlFormatter.formatDisplayText("第一段\n第二段")
         )
     }