This commit is contained in:
√(noham)²
2026-06-19 15:48:32 +02:00
parent 58c210ec45
commit 13c9bcbcd2
8 changed files with 325 additions and 321 deletions

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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>", "URL of the article to convert")
.name('pdfy')
.description('Convert web articles to PDF using Reader View and Puppeteer')
.argument('<url>', 'URL of the article to convert')
.option(
"-t, --theme <theme>",
`Reader View theme (${VALID_THEMES.join(", ")})`,
'-t, --theme <theme>',
`Reader View theme (${VALID_THEMES.join(', ')})`
)
.option("-c, --css <path>", "path to custom CSS file")
.option("-p, --prefs <path>", "path to extension preferences JSON")
.option('-c, --css <path>', 'path to custom CSS file')
.option('-p, --prefs <path>', 'path to extension preferences JSON')
.option(
"-o, --output <path>",
"output file path (default: ./output/<title>.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
}
}
}

View File

@@ -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')

View File

@@ -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)
})

View File

@@ -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}`)
}

View File

@@ -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', '')
})
}

View File

@@ -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)