Search / tests /test_reader_architecture.js
vomebook's picture
fix(reader): hide Markdown front matter
cf97836
Raw History Blame Contribute Delete
20.3 kB
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')