import assert from "node:assert/strict"; import { test, after } from "node:test"; import { stripTypeScriptTypes } from "node:module"; import { readFile, mkdtemp, writeFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; const directory = await mkdtemp(join(tmpdir(), "gutenberg-test-")); for (const name of ["catalog", "download"]) { const source = await readFile(new URL("../src/" + name + ".ts", import.meta.url), "utf8"); await writeFile( join(directory, name + ".mjs"), stripTypeScriptTypes(source).replace("./catalog.js", "./catalog.mjs"), ); } after(() => rm(directory, { recursive: true, force: true })); const { parseIndex, searchIndex, openingExcerpt } = await import(pathToFileURL(join(directory, "catalog.mjs"))); const { createBookLoader, bookURL, fetchBook } = await import(pathToFileURL(join(directory, "download.mjs"))); test("CSV preserves quoted/multiline titles and selects text editions", () => { const books = parseIndex( 'Text#,Type,Title,Language,Authors\r\n1342,Text,"Pride, and\r\nPrejudice",en,"Austen, Jane"\r\n11,Text,"Alice and ""friends""",en,Carroll\r\n12,Sound,Audio,en,Someone\r\n', ); assert.deepEqual(books, [ { ebook: 1342, title: "Pride, and Prejudice", language: "en", author: "Austen, Jane" }, { ebook: 11, title: 'Alice and "friends"', language: "en", author: "Carroll" }, ]); assert.throws(() => parseIndex('Text#,Type,Title,Language,Authors\n1,Text,"broken'), /Unclosed/); }); test("fuzzy titles and diacritics work, with a bounded result list", () => { const books = [ { ebook: 1342, title: "Pride and Prejudice", author: "Austen, Jane", language: "en" }, { ebook: 11, title: "Alice in Wonderland", author: "Carroll", language: "en" }, ...Array.from({ length: 40 }, (_, i) => ({ ebook: 100 + i, title: "Émile " + i, author: "Rousseau", language: "fr", })), ]; const search = searchIndex(books); assert.equal(search("prde and prejudi").at(0).ebook, 1342); assert.equal(search("austen").at(0).ebook, 1342); assert.equal(search("emile").length, 20); assert.deepEqual(search("a"), []); }); test("opening excerpt keeps paragraph boundaries and excludes Gutenberg framing", () => { const body = "license\n*** START OF THE PROJECT GUTENBERG EBOOK TEST ***\n\nFirst\nline\n\nSecond\n\n*** END OF THE PROJECT GUTENBERG EBOOK TEST ***\nlicense"; assert.deepEqual(openingExcerpt(body), { text: "First\nline\n\nSecond", words: 3 }); const first = "word ".repeat(600).trim(), second = "more ".repeat(450).trim(); const text = "*** START OF THE PROJECT GUTENBERG EBOOK TEST ***\n\n" + first + "\n\n" + second; assert.equal(openingExcerpt(text), null, "Wait for the whole final paragraph"); assert.deepEqual(openingExcerpt(text + "\n\nignored"), { text: first + "\n\n" + second, words: 1050 }); assert.deepEqual(openingExcerpt("short book", true), { text: "short book", words: 2 }); const long = "😀".repeat(30000); const bounded = openingExcerpt(long, true); assert(bounded.text.length <= 56000); assert(!/[\uD800-\uDBFF]$/.test(bounded.text)); }); test("streaming fetch stops at the excerpt and releases the remaining download", async () => { const original = globalThis.fetch; let cancelled = false, reads = 0; const chunks = [ "licence\n*** START OF THE PROJECT GUTEN", "BERG EBOOK TEST ***\n\n", "word ".repeat(600) + "\n\n", "more ".repeat(450) + "\n\n", "unused rest of the book", ]; globalThis.fetch = async () => new Response( new ReadableStream( { pull(controller) { reads++; const chunk = chunks.shift(); if (chunk) controller.enqueue(new TextEncoder().encode(chunk)); else controller.close(); }, cancel() { cancelled = true; }, }, { highWaterMark: 0 }, ), ); try { const text = await fetchBook(1342); assert.equal(reads, 4); assert.equal(cancelled, true); assert.equal(openingExcerpt(text, true).words, 1050); assert(!text.includes("unused")); } finally { globalThis.fetch = original; } }); test("downloads coalesce, cache, serialize, and permit retries after failure", async () => { let calls = 0, active = 0, maximum = 0, fail = true; const load = createBookLoader(async (ebook) => { calls++; active++; maximum = Math.max(maximum, active); await new Promise((r) => setTimeout(r, 2)); active--; if (ebook === 3 && fail) { fail = false; throw Error("offline"); } return "Book " + ebook; }, 1); assert.deepEqual(await Promise.all([load(1), load(1), load(2)]), [ { text: "Book 1", words: 2 }, { text: "Book 1", words: 2 }, { text: "Book 2", words: 2 }, ]); assert.equal(calls, 2); assert.equal(maximum, 1); await load(1); assert.equal(calls, 2); await assert.rejects(load(3), /offline/); assert.deepEqual(await load(3), { text: "Book 3", words: 2 }); assert.equal(bookURL(1342), "https://gutenberg.pglaf.org/cache/epub/1342/pg1342.txt"); });