diff --git a/package.json b/package.json index a1ed12f..bcdf39b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,10 @@ "start": "node src/index.js", "test": "node src/test/test.js" }, + "standard": { + "globals": ["chrome", "Readability", "indexedDB", "getComputedStyle"], + "ignore": ["extension/"] + }, "dependencies": { "commander": "^15.0.0", "puppeteer": "^24.0.0" diff --git a/src/browser.js b/src/browser.js index dfb0271..be19dff 100644 --- a/src/browser.js +++ b/src/browser.js @@ -1,72 +1,72 @@ -import fs from "node:fs"; -import path from "node:path"; -import puppeteer from "puppeteer"; +import fs from 'node:fs' +import path from 'node:path' +import puppeteer from 'puppeteer' -const EXTENSION_DIR = path.resolve("./extension/reader-view"); -const MANIFEST_PATH = path.join(EXTENSION_DIR, "manifest.json"); -const READABILITY_PATH = path.join(EXTENSION_DIR, "data/inject/Readability.js"); +const EXTENSION_DIR = path.resolve('./extension/reader-view') +const MANIFEST_PATH = path.join(EXTENSION_DIR, 'manifest.json') +const READABILITY_PATH = path.join(EXTENSION_DIR, 'data/inject/Readability.js') -export function checkExtension() { +export function checkExtension () { if (!fs.existsSync(MANIFEST_PATH)) { console.error( `Extension not found at: ${EXTENSION_DIR}\n` + - "Please extract the extension first:\n" + - " mkdir -p extension/reader-view\n" + - " unzip extension/Reader-View-Chrome-Web-Store.zip -d extension/reader-view", - ); - process.exit(1); + 'Please extract the extension first:\n' + + ' mkdir -p extension/reader-view\n' + + ' unzip extension/Reader-View-Chrome-Web-Store.zip -d extension/reader-view' + ) + process.exit(1) } if (!fs.existsSync(READABILITY_PATH)) { console.error( `Readability.js not found at: ${READABILITY_PATH}\n` + - "The extension may be incomplete. Please re-extract it.", - ); - process.exit(1); + 'The extension may be incomplete. Please re-extract it.' + ) + process.exit(1) } } -export function getReadabilityPath() { - return READABILITY_PATH; +export function getReadabilityPath () { + return READABILITY_PATH } -export function getExtensionDir() { - return EXTENSION_DIR; +export function getExtensionDir () { + return EXTENSION_DIR } -export async function launchBrowser() { - console.log(`Launching browser with extension: ${EXTENSION_DIR}`); +export async function launchBrowser () { + console.log(`Launching browser with extension: ${EXTENSION_DIR}`) const browser = await puppeteer.launch({ headless: true, ignoreDefaultArgs: [ - "--disable-extensions", - "--disable-component-extensions-with-background-pages", - "--enable-automation", + '--disable-extensions', + '--disable-component-extensions-with-background-pages', + '--enable-automation' ], args: [ `--load-extension=${EXTENSION_DIR}`, - "--no-sandbox", - "--disable-setuid-sandbox", - ], - }); - return browser; + '--no-sandbox', + '--disable-setuid-sandbox' + ] + }) + return browser } -export async function discoverExtensionId(browser) { - const page = await browser.newPage(); - await page.goto("chrome://extensions/", { timeout: 8000 }); - await new Promise((r) => setTimeout(r, 1000)); +export async function discoverExtensionId (browser) { + const page = await browser.newPage() + await page.goto('chrome://extensions/', { timeout: 8000 }) + await new Promise((resolve) => setTimeout(resolve, 1000)) const extId = await page.evaluate(() => { return new Promise((resolve) => { chrome.management.getAll((exts) => { - const found = exts.find((e) => e.name === "Reader View"); - resolve(found ? found.id : null); - }); - }); - }); - await page.close(); + const found = exts.find((e) => e.name === 'Reader View') + resolve(found ? found.id : null) + }) + }) + }) + await page.close() if (!extId) { - throw new Error("Could not discover Reader View extension ID"); + throw new Error('Could not discover Reader View extension ID') } - console.log(`Discovered extension ID: ${extId}`); - return extId; + console.log(`Discovered extension ID: ${extId}`) + return extId } diff --git a/src/cli.js b/src/cli.js index 21b3b73..1c2b10f 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,52 +1,52 @@ -import { Command } from "commander"; +import { Command } from 'commander' export const VALID_THEMES = [ - "light", - "dark", - "sepia", - "groove-dark", - "solarized-light", - "solarized-dark", - "nord-light", - "nord-dark", -]; + 'light', + 'dark', + 'sepia', + 'groove-dark', + 'solarized-light', + 'solarized-dark', + 'nord-light', + 'nord-dark' +] -const program = new Command(); +const program = new Command() program - .name("pdfy") - .description("Convert web articles to PDF using Reader View and Puppeteer") - .argument("", "URL of the article to convert") + .name('pdfy') + .description('Convert web articles to PDF using Reader View and Puppeteer') + .argument('', 'URL of the article to convert') .option( - "-t, --theme ", - `Reader View theme (${VALID_THEMES.join(", ")})`, + '-t, --theme ', + `Reader View theme (${VALID_THEMES.join(', ')})` ) - .option("-c, --css ", "path to custom CSS file") - .option("-p, --prefs ", "path to extension preferences JSON") + .option('-c, --css ', 'path to custom CSS file') + .option('-p, --prefs ', 'path to extension preferences JSON') .option( - "-o, --output ", - "output file path (default: ./output/.pdf)", + '-o, --output <path>', + 'output file path (default: ./output/<title>.pdf)' ) .addHelpText( - "after", + 'after', ` Examples: node src/index.js <URL> node src/index.js <URL> --theme dark --css config/rules.css npm start -- <URL> --theme dark --css config/rules.css -`, - ); +` + ) -export function parseArgs() { - program.parse(process.argv); - const opts = program.opts(); +export function parseArgs () { + program.parse(process.argv) + const opts = program.opts() return { url: program.args[0], opts: { theme: opts.theme || null, css: opts.css || null, prefs: opts.prefs || null, - output: opts.output || null, - }, - }; + output: opts.output || null + } + } } diff --git a/src/config.js b/src/config.js index cda6a19..0403c53 100644 --- a/src/config.js +++ b/src/config.js @@ -1,25 +1,25 @@ -import fs from "node:fs"; -import path from "node:path"; +import fs from 'node:fs' +import path from 'node:path' -export function loadPreferences(prefsPath) { - if (!prefsPath || !fs.existsSync(prefsPath)) return {}; +export function loadPreferences (prefsPath) { + if (!prefsPath || !fs.existsSync(prefsPath)) return {} try { - return JSON.parse(fs.readFileSync(prefsPath, "utf-8")); + return JSON.parse(fs.readFileSync(prefsPath, 'utf-8')) } catch (e) { - console.warn(`Warning: could not parse preferences file: ${prefsPath}`); - return {}; + console.warn(`Warning: could not parse preferences file: ${prefsPath}`) + return {} } } -export function readCustomCss(cssPath) { - if (!cssPath || !fs.existsSync(cssPath)) return null; - const css = fs.readFileSync(cssPath, "utf-8").trim(); - return css || null; +export function readCustomCss (cssPath) { + if (!cssPath || !fs.existsSync(cssPath)) return null + const css = fs.readFileSync(cssPath, 'utf-8').trim() + return css || null } -export const DEFAULT_CONFIG_DIR = path.resolve("./config"); +export const DEFAULT_CONFIG_DIR = path.resolve('./config') export const DEFAULT_PREFS_PATH = path.join( DEFAULT_CONFIG_DIR, - "reader-view-preferences.json", -); -export const DEFAULT_CSS_PATH = path.join(DEFAULT_CONFIG_DIR, "rules.css"); + 'reader-view-preferences.json' +) +export const DEFAULT_CSS_PATH = path.join(DEFAULT_CONFIG_DIR, 'rules.css') diff --git a/src/index.js b/src/index.js index 487d1ed..7aba9fd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,119 +1,119 @@ -import process from "node:process"; -import path from "node:path"; -import { parseArgs, VALID_THEMES } from "./cli.js"; -import { loadPreferences, readCustomCss } from "./config.js"; +import process from 'node:process' +import path from 'node:path' +import { parseArgs, VALID_THEMES } from './cli.js' +import { loadPreferences, readCustomCss } from './config.js' import { checkExtension, launchBrowser, discoverExtensionId, - getReadabilityPath, -} from "./browser.js"; + getReadabilityPath +} from './browser.js' import { extractArticle, storeArticle, setExtensionPreferences, waitForReaderView, extractRenderedContent, - getArticleTitle, -} from "./reader.js"; + getArticleTitle +} from './reader.js' import { getOutputFilename, resolveOutputPath, enhanceHtml, - generatePdf, -} from "./pdf.js"; + generatePdf +} from './pdf.js' -const EXTENSION_BASE = "chrome-extension://"; +const EXTENSION_BASE = 'chrome-extension://' -async function main() { - const { url, opts } = parseArgs(); +async function main () { + const { url, opts } = parseArgs() - checkExtension(); + checkExtension() - let preferences = {}; + let preferences = {} if (opts.prefs) { - preferences = loadPreferences(path.resolve(opts.prefs)); + preferences = loadPreferences(path.resolve(opts.prefs)) } - const theme = opts.theme || preferences.mode || "light"; + const theme = opts.theme || preferences.mode || 'light' if (!VALID_THEMES.includes(theme)) { console.error( - `Invalid theme "${theme}". Valid themes: ${VALID_THEMES.join(", ")}`, - ); - process.exit(1); + `Invalid theme "${theme}". Valid themes: ${VALID_THEMES.join(', ')}` + ) + process.exit(1) } - preferences.mode = theme; - console.log(`Theme: ${theme}`); + preferences.mode = theme + console.log(`Theme: ${theme}`) - let customCss = null; + let customCss = null if (opts.css) { - customCss = readCustomCss(path.resolve(opts.css)); + customCss = readCustomCss(path.resolve(opts.css)) if (customCss !== null) { - preferences["user-css"] = customCss; - console.log(`Custom CSS loaded: ${path.resolve(opts.css)}`); + preferences['user-css'] = customCss + console.log(`Custom CSS loaded: ${path.resolve(opts.css)}`) } } - const browser = await launchBrowser(); + const browser = await launchBrowser() try { - const extId = await discoverExtensionId(browser); + const extId = await discoverExtensionId(browser) - console.log(`Fetching article: ${url}`); - const articlePage = await browser.newPage(); - await articlePage.goto(url, { waitUntil: "networkidle0", timeout: 30000 }); - const article = await extractArticle(articlePage, getReadabilityPath()); - await articlePage.close(); + console.log(`Fetching article: ${url}`) + const articlePage = await browser.newPage() + await articlePage.goto(url, { waitUntil: 'networkidle0', timeout: 30000 }) + const article = await extractArticle(articlePage, getReadabilityPath()) + await articlePage.close() - const extPage = await browser.newPage(); - await storeArticle(extPage, extId, 1, article); + const extPage = await browser.newPage() + await storeArticle(extPage, extId, 1, article) const prefsToSet = Object.fromEntries( Object.entries(preferences).filter( - ([_, v]) => v !== undefined && v !== null, - ), - ); + ([_, v]) => v !== undefined && v !== null + ) + ) if (Object.keys(prefsToSet).length > 0) { - await setExtensionPreferences(extPage, extId, prefsToSet); + await setExtensionPreferences(extPage, extId, prefsToSet) } - await extPage.close(); + await extPage.close() const readerUrl = [ `${EXTENSION_BASE}${extId}/data/reader/index.html`, - "?id=1", - `&url=${encodeURIComponent(url)}`, - ].join(""); - console.log(`Opening Reader View: ${readerUrl}`); + '?id=1', + `&url=${encodeURIComponent(url)}` + ].join('') + console.log(`Opening Reader View: ${readerUrl}`) - const readerPage = await browser.newPage(); - await readerPage.goto(readerUrl, { waitUntil: "load", timeout: 30000 }); + const readerPage = await browser.newPage() + await readerPage.goto(readerUrl, { waitUntil: 'load', timeout: 30000 }) - await waitForReaderView(readerPage); - await new Promise((r) => setTimeout(r, 1000)); + await waitForReaderView(readerPage) + await new Promise((resolve) => setTimeout(resolve, 1000)) - const title = await getArticleTitle(readerPage); + const title = await getArticleTitle(readerPage) const outputFile = opts.output ? resolveOutputPath(opts.output) - : getOutputFilename(title); + : getOutputFilename(title) const contentHtml = await extractRenderedContent( readerPage, - `${EXTENSION_BASE}${extId}/data/reader/`, - ); - await readerPage.close(); + `${EXTENSION_BASE}${extId}/data/reader/` + ) + await readerPage.close() - const enhancedHtml = enhanceHtml(contentHtml, customCss); + const enhancedHtml = enhanceHtml(contentHtml, customCss) - const pdfPage = await browser.newPage(); - await generatePdf(pdfPage, enhancedHtml, outputFile); - await pdfPage.close(); + const pdfPage = await browser.newPage() + await generatePdf(pdfPage, enhancedHtml, outputFile) + await pdfPage.close() } finally { - await browser.close(); - console.log("Browser closed."); + await browser.close() + console.log('Browser closed.') } } main().catch((err) => { - console.error("Fatal error:", err); - process.exit(1); -}); + console.error('Fatal error:', err) + process.exit(1) +}) diff --git a/src/pdf.js b/src/pdf.js index 86226a0..26182e6 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -1,58 +1,58 @@ -import fs from "node:fs"; -import path from "node:path"; +import fs from 'node:fs' +import path from 'node:path' const HIGHLIGHT_CSS = - '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">'; + '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">' const HIGHLIGHT_JS = - '<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"><\/script><script>hljs.highlightAll();<\/script>'; + '<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"><script><script>hljs.highlightAll();<script>' -export function getOutputFilename(title, outputDir) { +export function getOutputFilename (title, outputDir) { const safe = title - .replace(/[<>:"/\\|?*]/g, "") + .replace(/[<>:"/\\|?*]/g, '') .trim() - .slice(0, 120); - const dir = outputDir ? path.resolve(outputDir) : path.resolve("./output"); - fs.mkdirSync(dir, { recursive: true }); - return path.join(dir, `${safe || "article"}.pdf`); + .slice(0, 120) + const dir = outputDir ? path.resolve(outputDir) : path.resolve('./output') + fs.mkdirSync(dir, { recursive: true }) + return path.join(dir, `${safe || 'article'}.pdf`) } -export function resolveOutputPath(outputFlag) { - if (!outputFlag) return null; - const resolved = path.resolve(outputFlag); - const dir = resolved.endsWith(".pdf") +export function resolveOutputPath (outputFlag) { + if (!outputFlag) return null + const resolved = path.resolve(outputFlag) + const dir = resolved.endsWith('.pdf') ? path.dirname(resolved) - : path.dirname(resolved); - fs.mkdirSync(dir, { recursive: true }); - return resolved.endsWith(".pdf") ? resolved : `${resolved}.pdf`; + : path.dirname(resolved) + fs.mkdirSync(dir, { recursive: true }) + return resolved.endsWith('.pdf') ? resolved : `${resolved}.pdf` } -export function enhanceHtml(html, customCss) { +export function enhanceHtml (html, customCss) { let enhanced = html.replace( - "</head>", - `${HIGHLIGHT_CSS}${HIGHLIGHT_JS}</head>`, - ); + '</head>', + `${HIGHLIGHT_CSS}${HIGHLIGHT_JS}</head>` + ) if (customCss !== null) { enhanced = enhanced.replace( - "</head>", - `<style>${customCss}</style></head>`, - ); + '</head>', + `<style>${customCss}</style></head>` + ) } - return enhanced; + return enhanced } -export async function generatePdf(page, html, outputPath) { - await page.setContent(html, { waitUntil: "networkidle0" }); - console.log(`Generating PDF: ${outputPath}`); +export async function generatePdf (page, html, outputPath) { + await page.setContent(html, { waitUntil: 'networkidle0' }) + console.log(`Generating PDF: ${outputPath}`) await page.pdf({ path: outputPath, - format: "A4", + format: 'A4', printBackground: true, margin: { - top: "0mm", - bottom: "0mm", - left: "0mm", - right: "0mm", - }, - }); - console.log(`PDF saved: ${outputPath}`); + top: '0mm', + bottom: '0mm', + left: '0mm', + right: '0mm' + } + }) + console.log(`PDF saved: ${outputPath}`) } diff --git a/src/reader.js b/src/reader.js index fbc6fab..7be945a 100644 --- a/src/reader.js +++ b/src/reader.js @@ -1,15 +1,15 @@ -import fs from "node:fs"; +import fs from 'node:fs' -export async function extractArticle(page, readabilityPath) { - const readabilitySource = fs.readFileSync(readabilityPath, "utf-8"); - await page.evaluate(readabilitySource); +export async function extractArticle (page, readabilityPath) { + const readabilitySource = fs.readFileSync(readabilityPath, 'utf-8') + await page.evaluate(readabilitySource) const article = await page.evaluate(() => { - const doc = document.cloneNode(true); - const reader = new Readability(doc); - const parsed = reader.parse(); - if (!parsed) return null; - const text = parsed.textContent || ""; - const lang = document.documentElement.lang || "en"; + const doc = document.cloneNode(true) + const reader = new Readability(doc) + const parsed = reader.parse() + if (!parsed) return null + const text = parsed.textContent || '' + const lang = document.documentElement.lang || 'en' const readingSpeeds = { en: { cpm: 987, variance: 118 }, ar: { cpm: 612, variance: 88 }, @@ -26,17 +26,17 @@ export async function extractArticle(page, readabilityPath) { ru: { cpm: 985, variance: 119 }, sv: { cpm: 1015, variance: 116 }, tr: { cpm: 935, variance: 103 }, - zh: { cpm: 891, variance: 121 }, - }; - const speed = readingSpeeds[lang] || readingSpeeds.en; - const charsPerMinLow = speed.cpm - speed.variance; - const charsPerMinHigh = speed.cpm + speed.variance; + zh: { cpm: 891, variance: 121 } + } + const speed = readingSpeeds[lang] || readingSpeeds.en + const charsPerMinLow = speed.cpm - speed.variance + const charsPerMinHigh = speed.cpm + speed.variance const dateMeta = document.querySelector( 'meta[property="article:published_time"],meta[property="og:pubdate"],' + 'meta[property="og:publish_date"],meta[name="citation_online_date"],' + - 'meta[name="dc.Date"]', - ); - const publishedTime = dateMeta?.content || parsed.publishedTime || ""; + 'meta[name="dc.Date"]' + ) + const publishedTime = dateMeta?.content || parsed.publishedTime || '' return { ...parsed, lang, @@ -44,131 +44,131 @@ export async function extractArticle(page, readabilityPath) { readingTimeMinsSlow: Math.ceil(text.length / charsPerMinLow), published_time: publishedTime ? new Date(publishedTime).toLocaleDateString() - : "", - url: window.location.href, - }; - }); + : '', + url: window.location.href + } + }) if (!article) { - throw new Error("Failed to extract article content with Readability"); + throw new Error('Failed to extract article content with Readability') } console.log( `Extracted article: ${article.title} (${article.length} chars, reading ${article.readingTimeMinsFast}-${article.readingTimeMinsSlow} min` + - (article.published_time ? `, ${article.published_time}` : "") + - ")", - ); - return article; + (article.published_time ? `, ${article.published_time}` : '') + + ')' + ) + return article } -export async function storeArticle(extPage, extId, articleId, article) { +export async function storeArticle (extPage, extId, articleId, article) { await extPage.goto(`chrome-extension://${extId}/manifest.json`, { - timeout: 8000, - }); + timeout: 8000 + }) await extPage.evaluate( ({ id, article }) => { return new Promise((resolve, reject) => { - const req = indexedDB.open("content-temporary-storage", 1); + const req = indexedDB.open('content-temporary-storage', 1) req.onupgradeneeded = (e) => { - const db = e.target.result; - if (!db.objectStoreNames.contains("storage")) { - db.createObjectStore("storage", { keyPath: "id" }); + const db = e.target.result + if (!db.objectStoreNames.contains('storage')) { + db.createObjectStore('storage', { keyPath: 'id' }) } - }; + } req.onsuccess = (e) => { - const db = e.target.result; - const tx = db.transaction(["storage"], "readwrite"); - const store = tx.objectStore("storage"); - const put = store.put({ id, content: article }); - put.onerror = () => reject(put.error?.message || "put failed"); + const db = e.target.result + const tx = db.transaction(['storage'], 'readwrite') + const store = tx.objectStore('storage') + const put = store.put({ id, content: article }) + put.onerror = () => reject(put.error?.message || 'put failed') tx.oncomplete = () => { - db.close(); - resolve(); - }; - tx.onerror = () => reject(tx.error?.message || "tx failed"); - }; - req.onerror = () => reject(req.error?.message || "open failed"); - }); + db.close() + resolve() + } + tx.onerror = () => reject(tx.error?.message || 'tx failed') + } + req.onerror = () => reject(req.error?.message || 'open failed') + }) }, - { id: articleId, article }, - ); - console.log("Article stored in extension IndexedDB"); + { id: articleId, article } + ) + console.log('Article stored in extension IndexedDB') } -export async function setExtensionPreferences(page, extId, prefs) { +export async function setExtensionPreferences (page, extId, prefs) { await page.goto(`chrome-extension://${extId}/manifest.json`, { - timeout: 8000, - }); + timeout: 8000 + }) await page.evaluate((settings) => { return new Promise((resolve, reject) => { chrome.storage.local.set(settings, () => { - if (chrome.runtime.lastError) reject(chrome.runtime.lastError.message); - else resolve(); - }); - }); - }, prefs); - console.log(`Extension preferences set: ${Object.keys(prefs).length} key(s)`); + if (chrome.runtime.lastError) reject(chrome.runtime.lastError.message) + else resolve() + }) + }) + }, prefs) + console.log(`Extension preferences set: ${Object.keys(prefs).length} key(s)`) } -export async function waitForReaderView(page) { - console.log("Waiting for Reader View to render article..."); +export async function waitForReaderView (page) { + console.log('Waiting for Reader View to render article...') await page.waitForFunction( () => { - const title = document.title; - if (!title || title === "Loading... :: Reader View") return false; - const iframe = document.querySelector("#content iframe"); - if (!iframe) return false; + const title = document.title + if (!title || title === 'Loading... :: Reader View') return false + const iframe = document.querySelector('#content iframe') + if (!iframe) return false try { - const body = iframe.contentDocument?.body; - if (!body) return false; - return body.textContent.trim().length > 200; + const body = iframe.contentDocument?.body + if (!body) return false + return body.textContent.trim().length > 200 } catch { - return false; + return false } }, - { timeout: 60000 }, - ); - console.log("Article rendered."); + { timeout: 60000 } + ) + console.log('Article rendered.') } -export async function extractRenderedContent(page, baseUrl) { +export async function extractRenderedContent (page, baseUrl) { const html = await page.evaluate((extBase) => { - const iframe = document.querySelector("#content iframe"); - if (!iframe) return null; + const iframe = document.querySelector('#content iframe') + if (!iframe) return null try { - const doc = iframe.contentDocument; - if (!doc) return null; + const doc = iframe.contentDocument + if (!doc) return null - const dataMode = document.documentElement.getAttribute("data-mode"); + const dataMode = document.documentElement.getAttribute('data-mode') if (dataMode) { - doc.documentElement.setAttribute("data-mode", dataMode); + doc.documentElement.setAttribute('data-mode', dataMode) } - const cs = getComputedStyle(doc.documentElement); - const cssVars = ["--fg", "--bg", "--bd", "--lk", "--hg"]; + const cs = getComputedStyle(doc.documentElement) + const cssVars = ['--fg', '--bg', '--bd', '--lk', '--hg'] const inlineVars = cssVars .map((v) => `${v}: ${cs.getPropertyValue(v)}`) .filter((s) => s.length > 5) - .join("; "); + .join('; ') if (inlineVars) { - const existing = doc.documentElement.style.cssText; + const existing = doc.documentElement.style.cssText doc.documentElement.style.cssText = existing - ? existing + "; " + inlineVars - : inlineVars; + ? existing + '; ' + inlineVars + : inlineVars } - const b = doc.createElement("base"); - b.href = extBase; - doc.head.insertBefore(b, doc.head.firstChild); - return "<!DOCTYPE html>\n" + doc.documentElement.outerHTML; + const b = doc.createElement('base') + b.href = extBase + doc.head.insertBefore(b, doc.head.firstChild) + return '<!DOCTYPE html>\n' + doc.documentElement.outerHTML } catch { - return null; + return null } - }, baseUrl); - if (!html) throw new Error("Could not extract rendered content from iframe"); - return html; + }, baseUrl) + if (!html) throw new Error('Could not extract rendered content from iframe') + return html } -export function getArticleTitle(page) { +export function getArticleTitle (page) { return page.evaluate(() => { - return (document.title || "").replace(" :: Reader View", ""); - }); + return (document.title || '').replace(' :: Reader View', '') + }) } diff --git a/src/test/test.js b/src/test/test.js index cf8a972..34842aa 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -1,70 +1,70 @@ -import { execSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; +import { execSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' -const TEST_DIR = path.dirname(new URL(import.meta.url).pathname); -const TEST_FILE = path.join(TEST_DIR, "test.txt"); -const OUTPUT_DIR = "output/test"; +const TEST_DIR = path.dirname(new URL(import.meta.url).pathname) +const TEST_FILE = path.join(TEST_DIR, 'test.txt') +const OUTPUT_DIR = 'output/test' const TESTS = [ - { label: "default (no theme/css)", args: "" }, - { label: "dark theme", args: "--theme dark" }, - { label: "sepia theme", args: "--theme sepia" }, - { label: "custom CSS", args: "--css config/rules.css" }, + { label: 'default (no theme/css)', args: '' }, + { label: 'dark theme', args: '--theme dark' }, + { label: 'sepia theme', args: '--theme sepia' }, + { label: 'custom CSS', args: '--css config/rules.css' }, { - label: "dark theme + custom CSS", - args: "--theme dark --css config/rules.css", - }, -]; + label: 'dark theme + custom CSS', + args: '--theme dark --css config/rules.css' + } +] if (!fs.existsSync(TEST_FILE)) { - console.error(`Test file not found: ${TEST_FILE}`); - process.exit(1); + console.error(`Test file not found: ${TEST_FILE}`) + process.exit(1) } const urls = fs - .readFileSync(TEST_FILE, "utf-8") - .split("\n") + .readFileSync(TEST_FILE, 'utf-8') + .split('\n') .map((l) => l.trim()) - .filter((l) => l && !l.startsWith("#")); + .filter((l) => l && !l.startsWith('#')) if (urls.length === 0) { - console.error("No URLs found in test.txt"); - process.exit(1); + console.error('No URLs found in test.txt') + process.exit(1) } -fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +fs.mkdirSync(OUTPUT_DIR, { recursive: true }) -let passed = 0; -let failed = 0; +let passed = 0 +let failed = 0 console.log( - `Running ${TESTS.length} test scenario(s) across ${urls.length} URL(s)...\n`, -); + `Running ${TESTS.length} test scenario(s) across ${urls.length} URL(s)...\n` +) for (const url of urls) { for (const { label, args } of TESTS) { - const slug = label.replace(/[^a-z0-9]+/gi, "-").replace(/(^-|-$)/g, ""); - const outputPath = path.join(OUTPUT_DIR, `${slug}.pdf`); - const testName = `[${label}] ${url}`; - console.log(`\n=== ${testName} ===`); + const slug = label.replace(/[^a-z0-9]+/gi, '-').replace(/(^-|-$)/g, '') + const outputPath = path.join(OUTPUT_DIR, `${slug}.pdf`) + const testName = `[${label}] ${url}` + console.log(`\n=== ${testName} ===`) try { execSync(`node src/index.js "${url}" ${args} --output "${outputPath}"`, { - stdio: "inherit", - timeout: 120000, - }); - console.log(` PASS: ${testName}`); - passed++; + stdio: 'inherit', + timeout: 120000 + }) + console.log(` PASS: ${testName}`) + passed++ } catch (err) { - console.error(` FAIL: ${testName}`); - failed++; + console.error(` FAIL: ${testName}`) + failed++ } } } -console.log(`\n${"=".repeat(50)}`); +console.log(`\n${'='.repeat(50)}`) console.log( - `Results: ${passed} passed, ${failed} failed out of ${passed + failed} tests`, -); -process.exit(failed > 0 ? 1 : 0); + `Results: ${passed} passed, ${failed} failed out of ${passed + failed} tests` +) +process.exit(failed > 0 ? 1 : 0)