import { expect, it } from "vitest"; import { chapterText, splitChapters } from "../src/lib/chapters"; const pad = (n: number) => "字".repeat(n); it("识别中文/英文章节标题,首章前的引子归入「开篇」", () => { const t = `${pad(300)}\n简介正文\n\n第一章 初入江湖\n${pad(500)}\n\n第二章 风起云涌\n${pad(500)}\n\nChapter 3 the end\n${pad(100)}`; const chs = splitChapters(t); expect(chs.map((c) => c.title)).toEqual(["开篇", "第一章 初入江湖", "第二章 风起云涌", "Chapter 3 the end"]); expect(chs[1].start).toBe(t.indexOf("第一章")); expect(chapterText(t, chs, 1)).toMatch(/^第一章 初入江湖/); expect(chapterText(t, chs, chs.length - 1).trim().length).toBeLessThanOrEqual(200 + 15); }); it("密集目录行(间隔 < MIN_GAP)被折叠,正文里的真章节仍可导航", () => { const toc = ["第一章 aaa", "第二章 bbb", "第三章 ccc"].map((s) => s + "\n").join(""); const t = toc + "\n" + pad(500) + "\n第三章 ccc 正式开讲\n" + pad(500) + "\n第四章 ddd\n" + pad(500); const chs = splitChapters(t); expect(chs.map((c) => c.title)).toEqual(["第一章 aaa", "第三章 ccc 正式开讲", "第四章 ddd"]); }); it("无章节标记的长文按段落边界切伪章节,覆盖全文不重叠", () => { const para = pad(2500) + "\n\n"; const t = para.repeat(40); // 10 万字 const chs = splitChapters(t); expect(chs.length).toBeGreaterThanOrEqual(5); expect(chs[0].title).toBe("第 1 节"); expect(chs[0].start).toBe(0); for (let i = 0; i < chs.length; i++) expect(chs[i].start).toBeLessThan(chs[i + 1]?.start ?? t.length); if (chs.length > 1) expect(t[chs[1].start - 1]).toBe("\n"); // 落点在换行后,不切断一行 const total = chs.reduce((n, c, i) => n + (chs[i + 1]?.start ?? t.length) - c.start, 0); expect(total).toBe(t.length); }); it("短文无标记 → 单节,chapterText 即全文", () => { const t = "hello\nworld"; const chs = splitChapters(t); expect(chs).toEqual([{ title: "第 1 节", start: 0 }]); expect(chapterText(t, chs, 0)).toBe(t); }); it("卷不感知:只按章切,卷题行留在正文里", () => { const t = "第一卷 天云世界\n第一章 铜镜\n" + pad(500) + "\n第二章 修炼\n" + pad(500) + "\n\n\n第二卷 仙界\n第三章 飞升\n" + pad(300); const chs = splitChapters(t); expect(chs.map((c) => c.title)).toEqual(["第一章 铜镜", "第二章 修炼", "第三章 飞升"]); expect(chs[0].start).toBe(t.indexOf("第一章")); expect(chs[2].start).toBe(t.indexOf("第三章")); expect(chapterText(t, chs, 1)).toContain("第二卷 仙界"); // 卷题在第二章结尾,不进第三章开头 }); it("只有卷没有章的书:按伪章节切", () => { const t = "第一卷 起\n" + pad(600) + "\n第二卷 承\n" + pad(600) + "\n第三卷 转\n" + pad(600); const chs = splitChapters(t); expect(chs).toEqual([{ title: "第 1 节", start: 0 }]); });