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('|') + ')',