"use strict"; const assert = require("node:assert/strict"); require("../static/reader.js"); const { split, clipPlan, locations, readBookmark, bookmarkKey } = globalThis.ReaderPlan; for (const text of ["这是一篇长文。".repeat(200), "甲".repeat(598) + "2026年3.1415版本CPUModel。".repeat(60), "𠮷长文与😀测试。".repeat(200), "甲".repeat(20000)]) { const plan = split(text); assert.equal(plan.map((part) => part.text).join(""), text); let offset = 0; for (const part of plan) { assert.equal(part.start, offset); assert.equal(text.slice(part.start, part.end), part.text); assert.ok(Array.from(part.text).length <= 600); assert.ok(Array.from(part.context_before).length <= 32); assert.ok(Array.from(part.context_after).length <= 32); assert.ok(text.slice(0, part.start).endsWith(part.context_before)); assert.ok(text.slice(part.end).startsWith(part.context_after)); offset = part.end; } assert.equal(offset, text.length); } assert.equal(split("甲".repeat(598) + "2026年。", 600, 600)[0].text, "甲".repeat(598)); assert.throws(() => split("a".repeat(601))); assert.equal(split("a".repeat(200))[0].text, "a".repeat(200)); assert.throws(() => split("甲".repeat(20001))); assert.throws(() => split(" ")); const good = { version: 1, text: "长文😀", voice: "zf_001", speed: 1, offset: 2, seconds: 1.3 }; const storage = { getItem: (key) => key === bookmarkKey ? JSON.stringify(good) : null }; assert.deepEqual(readBookmark(storage), good); for (const changes of [{ seconds: -1 }, { offset: 100 }, { speed: 9 }, { voice: "unknown" }, { playbackRate: 8 }, { pitchSemitones: 4 }, { pitchSemitones: 1.5 }, { pitchSemitones: "2" }, { pitchSemitones: true }, { text: "甲".repeat(20001) }]) { assert.equal(readBookmark({ getItem: () => JSON.stringify({ ...good, ...changes }) }), null); } assert.equal(readBookmark({ getItem: () => "malformed" }), null); assert.equal(readBookmark({ getItem: () => JSON.stringify({ ...good, pitchSemitones: -3 }) }).pitchSemitones, -3); const sources = ["第一章\n\n正文第一句。正文第二句。\n1、第一项\n2、第二项\n", "甲\n".repeat(400), "甲".repeat(58) + "2026年3.1415版本CPUModel。", "𠀀😀正文。".repeat(50), "正文。" + "\n".repeat(100), "\n".repeat(100) + "正文。", "甲".repeat(800), "校长文章写得很好。".repeat(40), "前言。\n1.马克思主义的基本原理是什么。\n2.为什么要学习。\n(3).第三条。", "目录:1.总论,2.分论。" + "甲".repeat(70)]; for (const token of ["2026-10-18", "版本10.02.3", "编号00123", "电话138-0013-8000", "2026年10月18日", "1,234.50元", "3/4"]) { sources.push("甲".repeat(58) + token + "。后面的长文。"); const text = "甲".repeat(178) + token + "。"; const plan = split(text); for (const atom of ReaderPlan.numberSpans(text)) assert.ok(plan.some((part) => part.start <= atom.start && part.end >= atom.end)); } const { spawnSync } = require("node:child_process"); const python = spawnSync("python3", ["-B", "-c", "import json,sys;from tts.text import playback_segments,utf16_offset;print(json.dumps([[{'start':utf16_offset(t,a),'end':utf16_offset(t,b)} for a,b,_ in playback_segments(t)] for t in json.load(sys.stdin)]))"], { cwd: require("node:path").resolve(__dirname, ".."), input: JSON.stringify(sources), encoding: "utf8" }); assert.equal(python.status, 0, python.stderr); assert.deepEqual(sources.map(clipPlan), JSON.parse(python.stdout)); for (const text of sources) { const list = locations(split(text)); assert.equal(list.map((part) => text.slice(part.start, part.end)).join(""), text); assert.ok(list.every((part, index) => part.start === (index ? list[index - 1].end : 0))); } console.log("Reader: lossless Unicode batches, source navigation/Python parity, context and opt-in bookmarks PASS"); let now = 0; const sleep = new ReaderPlan.SleepTimer(() => now); sleep.arm("5"); assert.equal(sleep.remaining(), 300); now = 299999; assert.equal(sleep.due(), false); now = 300000; assert.equal(sleep.due(), true); sleep.arm("clip", "0:1"); assert.equal(sleep.ended("0:0"), false); assert.equal(sleep.ended("0:1"), true); sleep.arm("clip"); sleep.bind("1:2"); sleep.bind("1:3"); assert.equal(sleep.ended("1:2"), true); sleep.clear(); assert.equal(sleep.due(), false); assert.equal(sleep.ended("1:2"), false); assert.throws(() => sleep.arm("0.01")); console.log("Sleep timer: wall-clock deadlines, current-clip binding and clearing PASS");