const fs = require('fs') const path = require('path') const assert = require('assert') const { assertCode, codePattern, readReaderSource } = require('./reader_source_contract') const root = path.resolve(__dirname, '..') const readerEntry = fs.readFileSync(path.join(root, 'static/reader.js'), 'utf8') const reader = readReaderSource(root) assert.doesNotMatch(readerEntry, /^\s*(?:async\s+)?function\s+/m) assertCode(readerEntry, 'from "./reader/reader-components.mjs"') const searchController = fs.readFileSync(path.join(root, 'static/reader/search/reader-search-controller.mjs'), 'utf8') assertCode(reader, 'import { createReaderSearchController } from "./reader/search/reader-search-controller.mjs"') assertCode(reader, 'trackReaderResource(fullSearchController.dispose)') assertCode(reader, 'backend: { mode: () => capability.mode, search: searchReaderText') assert.doesNotMatch(reader, /dataset\.panelView\s*=\s*"full-search"/) assert.doesNotMatch(searchController, /new Worker|\bfetch\(|import\(/) const domSearch = fs.readFileSync(path.join(root, 'static/reader/search/reader-dom-search.mjs'), 'utf8') assertCode(reader, 'import { createReaderDomSearch, fullSearchEscape, fullSearchSnippet, fullSearchPageOffset } from "./reader/search/reader-dom-search.mjs"') assertCode(reader, 'trackReaderResource(domSearch.dispose)') assertCode(reader, 'getTocEntries: () => navigationState.tocEntries') assertCode(reader, 'domSearch.pruneMarks()') assert.doesNotMatch(reader, /function (?:fullSearchDomMatches|createDomSearchSnapshot|createFullSearchDomLocator|fullSearchTextNodes|highlightTextParts)\(/) assert.doesNotMatch(domSearch, /new Worker|\bfetch\(|import\(|readerRuntime|navigationState|fullSearchSession|fullSearchStatus/) assertCode(domSearch, 'const stopGeneration = runtime.events.on("generation"') assertCode(domSearch, 'function dispose() { stopGeneration()') assertCode(domSearch, 'publishPage(page(0), page, true)') const imageLoader = fs.readFileSync(path.join(root, 'static/reader/formats/reader-image-loader.mjs'), 'utf8') assertCode(reader, 'import { createReaderImageLoader } from "./reader/formats/reader-image-loader.mjs"') assertCode(reader, 'trackResource: trackReaderResource') assertCode(reader, 'await imageLoader.load(image, target, renderController.signal)') assert.doesNotMatch(reader, /function (?:waitReaderImage|loadReaderImage)\(/) assert.doesNotMatch(imageLoader, /readerRuntime|readerContentUrl|pdfPageManifest|documentState|new Image|import\(/) const css = fs.readFileSync(path.join(root, 'static/reader.css'), 'utf8') const view = fs.readFileSync(path.join(root, 'static/foliate-reader/view.js'), 'utf8') for (const marker of ["import('./epub.js')", "import('./mobi.js')", "import('./fb2.js')", "import('./pdf.js')", "isFBZ(file)", "isFB2(file)"]) assert.ok(view.includes(marker), marker) for (const file of ['epub.js', 'mobi.js', 'fb2.js', 'pdf.js', 'view.js', 'progress.js', 'ui/tree.js']) assert.ok(fs.existsSync(path.join(root, 'static/foliate-reader', file)), file) assert.match(fs.readFileSync(path.join(root, 'static/foliate-reader/mobi.js'), 'utf8'), /createDocument/) assert.match(fs.readFileSync(path.join(root, 'static/foliate-reader/fb2.js'), 'utf8'), /resolveHref/) assert.match(fs.readFileSync(path.join(root, 'static/foliate-reader/epub.js'), 'utf8'), /String\(href \?\? ''\)/) assert.match(fs.readFileSync(path.join(root, 'static/foliate-reader/epub.js'), 'utf8'), /item\.mediaType === MIME\.XHTML/) const foliateReader = fs.readFileSync(path.join(root, 'static/foliate-reader/reader.js'), 'utf8') for (const message of ['voice-foliate-goto-cfi', 'voice-foliate-search', 'voice-foliate-seek', 'voice-foliate-prev', 'voice-foliate-next']) assert.ok(foliateReader.includes(message), message) assert.strictEqual((reader.match(/async function renderFoliate\(/g) || []).length, 1) assert.strictEqual(reader.includes('renderFoliateClean'), false) assert.strictEqual(reader.includes('renderFoliateLegacy'), false) assert.strictEqual(reader.includes('foliate-continuous'), true) assert.strictEqual(reader.includes('document.querySelector("#toc-list").addEventListener'), false) assert.strictEqual(reader.includes('activateFoliateTocEntryLatest'), false) assert.strictEqual(reader.includes('setupFoliateToc'), false) assert.strictEqual(reader.includes('foliateVisibleSections'), false) assert.strictEqual(reader.includes('foliateVisibleNodes'), false) assert.strictEqual(reader.includes('directFoliateMoving'), false) assert.strictEqual(reader.includes('directFoliateContainer'), false) assert.strictEqual(reader.includes('setInterval(() => { loadNextFoliateSection(); }, 100)'), false) assert.strictEqual(reader.includes('setInterval(() => { const tab = document.querySelector("#toc-tab")'), false) assert.strictEqual(reader.includes('setInterval('), false) assert.match(reader, /function scheduleFoliateScrollSync\(\)/) assert.match(reader, /foliateSectionObserver\?\.disconnect\(\)/) assert.strictEqual((reader.match(/function disposeReader\(\)/g) || []).length, 1) assert.strictEqual((reader.match(/function setReaderPhase\(/g) || []).length, 1) assert.strictEqual((reader.match(/function classifyReaderError\(/g) || []).length, 1) assert.match(reader, /const readerLifecycle = readerRuntime\.state\.lifecycle/) assert.doesNotMatch(reader, /(?:^|\n)\s*let readerStage\b/) assert.match(reader, /readerRuntime\.setStage\(stage\)/) assert.match(reader, /\$\{readerLifecycle\.stage\}/) assertCode(reader, 'function scheduleSave() { if (readerLifecycle.disposed || readerAbortController.signal.aborted) return;') assertCode(reader, 'readerLifecycle.disposed || !documentState.restorationReady') assert.strictEqual((reader.match(/if \(!setReaderPhase\("ready"\)\) return/g) || []).length, 2) assert.match(reader, /addEventListener\("pagehide"/) assert.match(reader, /event\.persisted/) assert.match(reader, /!event\.persisted/) assert.doesNotMatch(reader, /id=["']ocr["']/i) assert.match(reader, /validatePdfPageManifest/) assert.match(reader, /const PDF_PREFETCH_MARGIN = 2400/) assert.match(reader, /const visibleObserver = new IntersectionObserver/) assert.match(reader, /promotePdfRenderWaiter\(shell\)/) assert.match(reader, /function pdfPagePriority\(shell\)/) assert.match(reader, /priority >= 3 \? "high" : priority >= 1 \? "auto" : "low"/) assert.match(reader, /const waiter = \{\s*shell,\s*priority,/) assert.match(reader, /waiter\.shell \? pdfPagePriority\(waiter\.shell\) : waiter\.priority/) assert.match(reader, /function cancelSpeculativePdfRenders\(protectedShell\)/) assert.match(reader, /isPdfPageVisible\(shell\)/) assert.match(reader, /shell\._renderCancel\?\.\(\)/) assert.match(reader, /const PDF_MANIFEST_PREFETCH_LIMIT = 12/) assert.match(reader, /const PDF_MANIFEST_PREFETCH_CONCURRENCY = 3/) assert.match(reader, /function takePdfManifestPrefetch\(target\)/) assert.match(reader, /function schedulePdfManifestPrefetch\(page\)/) assert.match(reader, /image\.fetchPriority = "low"/) assert.match(reader, /pdfManifestPrefetchActive\.add\(record\)/) assert.match(reader, /queuePdfManifestPrefetch\(\)/) const pdfPageRender = reader.match(/function renderPdfManifestShell\(shell, force = false, priority = false\) \{[\s\S]*?\n\}\s+function renderPdfShell/)[0] assert.match(pdfPageRender, /(?:const|let) target = pdfPageManifest\.pageUrl\(entry\.page\)/) assert.doesNotMatch(pdfPageRender, /entry\.j|entry\.w|\.jxl/) assert.match(reader, /chapterManifestObserver\?\.disconnect\(\)/) assert.match(reader, /content\.dataset\.errorCode = code/) assert.match(reader, /reader-request-manager\.js/) assert.match(reader, /VoiceOfMLReaderRequests\.createReaderRequestManager\(\)/) assert.match(reader, /readerRuntime\.track\(readerRequestManager\)/) assert.doesNotMatch(reader, /legacyReaderTitle/) for (const generation of ['navigation', 'search', 'bookmarks', 'pdf']) assert.match(reader, new RegExp(`nextReaderGeneration\\("${generation}"\\)`)) for (const timer of ['saveTimer', 'themeAnimationTimer', 'panelAnimationTimer']) assert.match(reader, new RegExp(`clearTimeout\\(${timer}\\)`)) assertCode(reader, 'for (const frame of [markerFrame, foliateScrollFrame, foliateWindowFrame, epubSeekFrame]) if (frame) cancelAnimationFrame(frame)') assert.match(reader, /trackReaderResource\(destroy\)/) assert.match(reader, /task\.destroy\(\)/) assert.match(reader, /mediaElement\.pause\(\)/) assert.match(reader, /mediaElement\.removeAttribute\("src"\)/) assert.strictEqual(reader.includes('activeReaderControllers'), false) assert.strictEqual(reader.includes('readerRequests'), false) assert.match(reader, /function syncTocTab\(\)/) assert.match(reader, /function applyReaderMetadata\(data\)/) assert.strictEqual((reader.match(/function applyReaderMetadata\(data\)/g) || []).length, 1) const viewportDeclaration = codePattern('const viewport = document.querySelector("#viewport")').exec(reader) const metadataApplication = codePattern('applyReaderMetadata(resolvedReaderData)').exec(reader) assert.ok(viewportDeclaration && metadataApplication && viewportDeclaration.index < metadataApplication.index) assertCode(reader, 'formatAdapters.active.open()') assertCode(reader, 'await formatAdapters.active.render(prepared)') assertCode(reader, 'await applyInitialRestoration(restored, generation)') assertCode(reader, 'await restoreInitialPosition(entry, generation)') assertCode(reader, 'await formatAdapters.active.restore(entry, generation)') assert.match(reader, /const restorationPromise = loadReaderRestoration\(\)/) assert.match(reader, /await formatAdapters\.active\.render\(prepared\)[\s\S]*?const restored = await awaitReader\(restorationPromise\)/) assert.doesNotMatch(reader, /Promise\.all\(\[loadReaderRestoration\(\)/) assert.strictEqual((reader.match(/function loadPdfDocument\(/g) || []).length, 1) for (const renderer of ['renderImageDocument', 'renderMedia']) assert.strictEqual((reader.match(new RegExp(`function ${renderer}\\s*\\(`,'g')) || []).length, 1) for (const renderer of ['renderTextDocument', 'renderMarkdownDocument']) assert.strictEqual((reader.match(new RegExp(`function ${renderer}\\(`,'g')) || []).length, 1) for (const loader of ['loadTextDocument', 'loadMarkdownDocument', 'loadHtmlDocument']) assert.strictEqual((reader.match(new RegExp(`function ${loader}\\(`,'g')) || []).length, 1) for (const loader of ['loadMediaDocument', 'loadImageDocument', 'loadChapterManifestDocument']) assert.strictEqual((reader.match(new RegExp(`function ${loader}\\(`,'g')) || []).length, 1) assert.doesNotMatch(reader, /loadFoliateDocument/) assert.match(reader, /reader-chapter-repository\.js/) assert.match(reader, /reader-scroll-anchor\.js/) assert.match(reader, /reader-section-virtualizer\.js/) assert.match(reader, /VoiceOfMLReaderChapters\.createChapterRepository\(/) assert.match(reader, /VoiceOfMLReaderScroll\.createScrollAnchorManager\(/) assert.match(reader, /VoiceOfMLReaderVirtual\.createSectionVirtualizer\(/) assert.match(reader, /foliateChapterRepository\?\.dispose\(\)/) assertCode(reader, 'readerRuntime.track(VoiceOfMLReaderScroll') assert.match(reader, /foliateSectionVirtualizer\?\.dispose\(\)/) assert.match(reader, /function foliateSectionRoot\(article\)/) assertCode(reader, 'article.attachShadow({ mode: "open" })') assert.match(reader, /event\.composedPath\(\)/) assert.match(reader, /foliateSectionRoot\(node\)/) assert.match(reader, /foliateScrollAnchors\.observe\(article\)/) assert.ok((reader.match(/foliateScrollAnchors\.invalidate\(\)/g) || []).length >= 3) const renderFoliateSource = reader.match(/async\s+function\s+renderFoliate\s*\([\s\S]*?status\.textContent\s*=\s*"EPUB";/)[0] assert.strictEqual(renderFoliateSource.includes('loaded = new Set'), false) assert.strictEqual(renderFoliateSource.includes('pending = new Map'), false) assert.match(reader, /stream\.insertBefore\(article, next \|\| null\)/) assert.match(reader, /foliateSectionLoader\s*=\s*(?:\(index\)|index)\s*=>/) assert.doesNotMatch(reader, /^let navigationGeneration\b/m) assert.doesNotMatch(reader, /syncReaderState/) assert.doesNotMatch(reader, /^(?:let|var|const)\s+title\b/m) for (const mirror of ['zoom', 'currentPage', 'pageCount', 'restoredEntry', 'restorationReady', 'tocEntries', 'currentChapterIndex', 'fullSearchResults', 'fullSearchIndex', 'fullSearchGeneration', 'showingAllBookmarks', 'editingBookmark', 'pdfRenderGeneration']) assert.doesNotMatch(reader, new RegExp(`(?:let|var|const)\\s+${mirror}\\b`), mirror) assert.doesNotMatch(reader, /readerRuntime\.updateFormat\(/) for (const domain of ['document', 'navigation', 'search', 'panel']) assert.match(reader, new RegExp(`readerRuntime\\.state\\.${domain}`)) assert.match(reader, /readerRuntime\.currentGeneration\("pdf"\)/) assertCode(reader, 'nextGeneration: () => nextReaderGeneration("search")') assertCode(searchController, 'const generation = nextGeneration()') assert.match(reader, /isReaderGenerationCurrent\("navigation", generation\)/) assert.match(reader, /registerReaderFormatAdapters\(\)/) assert.match(reader, /function disposeFormatResources\(mode\)/) assertCode(reader, 'link.target = "_blank"; link.rel = "noopener noreferrer"') assert.doesNotMatch(reader, /function (?:navigateFormat|searchFormat)\(/) assert.match(reader, /async function restoreFormat\(mode, entry, generation/) assert.match(reader, /formatAdapters\.activate\(capability\.mode\)/) assert.match(reader, /readerRuntime\.events\.on\("phase"/) assert.strictEqual((reader.match(/function navigateTocEntry\(/g) || []).length, 1) assert.match(reader, /await navigateTocEntry\(index\)/) assert.strictEqual((reader.match(/function navigateFoliateSearchResult\(/g) || []).length, 1) assert.strictEqual((reader.match(/function\s+fullSearchFoliateMatches\s*\(/g) || []).length, 1) assertCode(reader, 'publishSearchResults(generation, await fullSearchFoliateMatches(query, generation))') assert.match(reader, /function captureFoliateBookmarkPosition\(\)/) assert.match(reader, /function restoreFoliateBookmarkPosition\(entry, generation/) assert.match(reader, /foliatePosition = captureFoliateBookmarkPosition\(\)/) assert.match(reader, /new IntersectionObserver/) assert.doesNotMatch(reader, /article\.innerHTML = styles \+/) assert.match(reader, /target = await epubRendition\?\.book\?\.resolveHref/) assert.match(reader, /applyReaderMetadata\(resolvedReaderData\)/) assert.match(css, /\.foliate-continuous\s+a\s*\{\s*color:\s*#8ab4e8\s*!important;\s*\}/) assert.match(css, /min-height:\s*0\s*!important;\s*height:\s*auto\s*!important;/) assert.strictEqual(view.includes('.find(x => x.index = resolved.index)'), false) assert.strictEqual(view.includes('.find(x => x.index === resolved.index)'), true) const vm = require('vm') const priorityFunctions = reader.match(/function pdfPagePriority\(shell\) \{[\s\S]*?\n\}\s+function normalizePdfPriority\(priority\) \{[\s\S]*?\n\}/) assert.ok(priorityFunctions, 'PDF page priority functions') const priorityContext = { viewport: { getBoundingClientRect: () => ({ top: 0, bottom: 100 }) }, PDF_PREFETCH_MARGIN: 2400 } vm.runInNewContext(priorityFunctions[0], priorityContext) for (const [top, bottom, expected] of [[10, 90, 3], [70, 160, 2], [110, 190, 1], [2600, 2800, 0]]) assert.strictEqual(priorityContext.pdfPagePriority({ getBoundingClientRect: () => ({ top, bottom }) }), expected) assert.strictEqual(priorityContext.normalizePdfPriority(true), 3) const sandbox = { self: {} }; sandbox.self = sandbox; const frontMatterSource = fs.readFileSync(path.join(root, 'static/reader/formats/reader-html-format.mjs'), 'utf8') const frontMatterStart = frontMatterSource.indexOf('export function stripMarkdownFrontMatter') const frontMatterEnd = frontMatterSource.indexOf('\n}\n', frontMatterStart) assert.ok(frontMatterStart >= 0 && frontMatterEnd > frontMatterStart, 'Markdown front matter helper') const frontMatterContext = {} vm.runInNewContext( frontMatterSource.slice(frontMatterStart, frontMatterEnd + 3).replace(/^export /, '') + ';this.stripMarkdownFrontMatter = stripMarkdownFrontMatter;', frontMatterContext ) const stripFrontMatter = frontMatterContext.stripMarkdownFrontMatter for (const [input, expected] of [ ['---\ntitle: "标题"\nauthor: "作者"\nsection: "分类"\ndate: "日期"\nsource: "https://example.invalid"\nsource_path: "chinese/a.htm"\n---\n# 正文\n', '# 正文\n'], ['\uFEFF---\r\ntitle: "T"\r\n---\r\n正文', '正文'], ['---\ntitle: "T"\n---', ''], ['---\n\n---\n# Body', '---\n\n---\n# Body'], ['# Body\n---\n', '# Body\n---\n'], ['---\ntitle: "T"\n', '---\ntitle: "T"\n'], ]) assert.strictEqual(stripFrontMatter(input), expected) assert.match(reader, /marked\.parse\(stripMarkdownFrontMatter\(text\)\)/) function loadExports(file, name) { vm.runInNewContext(fs.readFileSync(path.join(root, 'static', file), 'utf8'), sandbox); return sandbox[name]; } const contractFeatures = loadExports('reader/core/reader-contract.js', 'VoiceOfMLReader').features const runtimeMatrix = loadExports('reader/core/reader-runtime.js', 'VoiceOfMLReaderRuntime').FEATURE_MATRIX assert.strictEqual(runtimeMatrix, contractFeatures) for (const mode of Object.keys(contractFeatures)) assert.strictEqual(JSON.stringify(runtimeMatrix[mode]), JSON.stringify(contractFeatures[mode]), mode) assert.strictEqual(JSON.stringify(runtimeMatrix.unsupported), JSON.stringify({ toc: false, search: false, zoom: false, bookmarks: false, pagination: false, media: false })) // Evaluate the owning registry with identifiable loaders/renderers, so comments or // dead dispatch functions cannot satisfy the format wiring contract. const registry = reader.match(/function\s+registerReaderFormatAdapters\s*\(\)\s*\{[\s\S]*?\n\}/) assert.ok(registry, 'format adapter registry') const expected = { pdf: ['loadPdfDocument', 'renderPdf'], image: ['loadImageDocument', 'renderImageDocument'], text: ['loadTextDocument', 'renderTextDocument'], markdown: ['loadMarkdownDocument', 'renderMarkdownDocument'], html: ['loadHtmlDocument', 'renderHtml'], 'epub-chapters': ['loadChapterManifestDocument', 'renderChapterManifest'], foliate: ['loadMediaDocument', 'renderFoliate'], swf: ['loadSwfDocument', 'renderSwf'], } const registered = new Map(), calls = [] const registryContext = { formatAdapters: { register: (mode, adapter) => registered.set(mode, adapter) } } for (const name of [...new Set(Object.values(expected).flat()), 'preloadPdfFirstPage', 'fetchReaderResponse', 'renderPdfPages', 'renderImagePages', 'renderMedia', 'restoreFormat', 'disposeFormatResources']) registryContext[name] = (...args) => calls.push([name, ...args]) vm.runInNewContext(registry[0] + '; registerReaderFormatAdapters();', registryContext) assert.deepStrictEqual([...registered.keys()].sort(), [...Object.keys(expected), 'pdf-pages', 'image-pages', 'audio', 'video'].sort()) for (const [mode, [open, render]] of Object.entries(expected)) { assert.strictEqual(registered.get(mode).open, registryContext[open], mode + ' loader') assert.strictEqual(registered.get(mode).render, registryContext[render], mode + ' renderer') } registered.get('pdf-pages').open() assert.deepStrictEqual(calls.splice(0), [['fetchReaderResponse']]) assert.match(reader, /const initialPage = initialReaderPage\(totalPages\);[\s\S]*?await preloadPdfFirstPage\(totalPages\)/) assert.strictEqual(registered.get('pdf-pages').render, registryContext.renderPdfPages) registered.get('image-pages').open() assert.deepStrictEqual(calls.splice(0), [['fetchReaderResponse']]) assert.strictEqual(registered.get('image-pages').render, registryContext.renderImagePages) for (const mode of ['audio', 'video']) { assert.strictEqual(registered.get(mode).open, registryContext.loadMediaDocument) registered.get(mode).render() assert.deepStrictEqual(calls.splice(0), [['renderMedia', mode]]) } for (const [mode, adapter] of registered) { const entry = { page: 2 } adapter.restore(entry, 17) adapter.dispose() assert.deepStrictEqual(calls.splice(0), [['restoreFormat', mode, entry, 17], ['disposeFormatResources', mode]]) } const html = fs.readFileSync(path.join(root, 'static/reader.html'), 'utf8') const securityScript = html.indexOf('src="/static/reader/core/reader-security.js"') assert.ok(securityScript >= 0 && securityScript < html.indexOf('src="/static/reader/core/reader-contract.js"')) assert.match(reader, /readerRuntime\.track\(window\.__VOICE_PDF_PRELOAD__\)/) console.log('reader architecture contracts passed')