From eadb35539629225ceb8fe0045177f322b8059cd7 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 2 May 2026 00:13:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(txt):=20recognize=20=E7=95=AA=E5=A4=96/?= =?UTF-8?q?=E5=A4=96=E4=BC=A0=20chapter=20prefixes,=20closes=20#4016=20(#4?= =?UTF-8?q?025)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chinese novels commonly use 番外 (bonus), 番外篇, or 外传 as chapter headings, optionally combined with 第N章. The previous regex only matched 第N章 at line start, so lines like "番外 第1章 旗开得胜" were dropped from the TOC. Treat 番外篇/番外/外传 as preface-style keywords so they match alongside 楔子/前言/etc. Co-authored-by: Claude Opus 4.7 (1M context) --- .../src/__tests__/utils/txt.test.ts | 38 +++++++++++++++++++ apps/readest-app/src/utils/txt.ts | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/readest-app/src/__tests__/utils/txt.test.ts b/apps/readest-app/src/__tests__/utils/txt.test.ts index 970a2fb9..b16232ad 100644 --- a/apps/readest-app/src/__tests__/utils/txt.test.ts +++ b/apps/readest-app/src/__tests__/utils/txt.test.ts @@ -115,6 +115,22 @@ describe('createChapterRegexps — Chinese (zh) regex matching', () => { ); }); + describe('番外 (bonus) prefix variants', () => { + it.each([ + '番外', + '番外 第1章 旗开得胜', + '番外 第一章 灰猫', + '番外 第二章 再战', + '番外 灰猫', + '番外篇 灰猫', + '外传', + '外传 一', + ])('should match "%s"', (heading) => { + const regex = getFirstRegex('zh'); + expect(regex.test(`\n${heading}\n`)).toBe(true); + }); + }); + describe('should not match', () => { it('should not match chapter heading embedded mid-line', () => { const regex = getFirstRegex('zh'); @@ -333,6 +349,28 @@ describe('extractChaptersFromSegment — Chinese (zh)', () => { expect(chapters.length).toBeGreaterThanOrEqual(2); expect(chapters[0]!.content).toContain('前文'); }); + + it('should extract 番外 chapters mixed with regular chapters (issue #4016)', () => { + const text = [ + '第57章 s级冰系掌控', + '第57章正文', + '第58章 觉醒天赋', + '第58章正文', + '番外 第1章 旗开得胜', + '番外1正文', + '番外 第2章 再战', + '番外2正文', + '番外 灰猫', + '番外3正文', + ].join('\n'); + const chapters = extractChapters(text, 'zh'); + const titles = chapters.map((c) => c.title); + expect(titles.some((t) => t.includes('第57章'))).toBe(true); + expect(titles.some((t) => t.includes('第58章'))).toBe(true); + expect(titles.some((t) => t.includes('番外 第1章'))).toBe(true); + expect(titles.some((t) => t.includes('番外 第2章'))).toBe(true); + expect(titles.some((t) => t.includes('番外 灰猫'))).toBe(true); + }); }); // --------------------------------------------------------------------------- diff --git a/apps/readest-app/src/utils/txt.ts b/apps/readest-app/src/utils/txt.ts index 4da26816..cc22b50f 100644 --- a/apps/readest-app/src/utils/txt.ts +++ b/apps/readest-app/src/utils/txt.ts @@ -635,7 +635,7 @@ export class TxtToEpubConverter { '(' + [ String.raw`第[  零〇一二三四五六七八九十0-9][  零〇一二三四五六七八九十百千万0-9]*(?:[章卷节回讲篇封本册部话])(?:[::、  \(\)0-9]*[^\n-]{0,36})(?!\S)`, - String.raw`(?:楔子|前言|简介|引言|序言|序章|总论|概论|后记)(?:[::  ][^\n-]{0,36})?(?!\S)`, + String.raw`(?:楔子|前言|简介|引言|序言|序章|总论|概论|后记|番外篇|番外|外传)(?:[::  ][^\n-]{0,36})?(?!\S)`, String.raw`chapter[\s.]*[0-9]+(?:[::.  ]+[^\n-]{0,50})?(?!\S)`, ].join('|') + ')',