From 099a76c39e2f98785d2ce168036f3e15f8f336a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=88=9A=28noham=29=C2=B2?= <100566912+NohamR@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:27:27 +0200 Subject: [PATCH] rewrite --- .gitignore | 3 + config/reader-view-preferences.json.example | 6 + config/rules.css.example | 5 + index.js | 342 +------------------- package.json | 5 +- src/browser.js | 72 +++++ src/cli.js | 59 ++++ src/config.js | 25 ++ src/index.js | 123 +++++++ src/pdf.js | 58 ++++ src/reader.js | 174 ++++++++++ src/test/test.js | 70 ++++ src/test/test.txt | 3 + 13 files changed, 603 insertions(+), 342 deletions(-) create mode 100644 config/reader-view-preferences.json.example create mode 100644 config/rules.css.example create mode 100644 src/browser.js create mode 100644 src/cli.js create mode 100644 src/config.js create mode 100644 src/index.js create mode 100644 src/pdf.js create mode 100644 src/reader.js create mode 100644 src/test/test.js create mode 100644 src/test/test.txt diff --git a/.gitignore b/.gitignore index b45602b..5e63730 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ extension/Reader-View-Chrome-Web-Store.crx extension/Reader-View-Chrome-Web-Store.zip /node_modules +/output +config/reader-view-preferences.json +config/rules.css diff --git a/config/reader-view-preferences.json.example b/config/reader-view-preferences.json.example new file mode 100644 index 0000000..5e2ca65 --- /dev/null +++ b/config/reader-view-preferences.json.example @@ -0,0 +1,6 @@ +{ + "tip.0": "s", + "tip.1": "s", + "tip.2": "s", + "tip.3": "s" +} diff --git a/config/rules.css.example b/config/rules.css.example new file mode 100644 index 0000000..9d8c9e0 --- /dev/null +++ b/config/rules.css.example @@ -0,0 +1,5 @@ +html[data-mode="light"] h1 { + font-size: 40px; + margin: 40px 0 32px; + line-height: 1.2; +} \ No newline at end of file diff --git a/index.js b/index.js index 28c16f2..5cf4632 100644 --- a/index.js +++ b/index.js @@ -1,340 +1,2 @@ -import fs from "node:fs"; -import path from "node:path"; -import process from "node:process"; -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 VALID_THEMES = [ - "light", "dark", "sepia", "groove-dark", - "solarized-light", "solarized-dark", "nord-light", "nord-dark", -]; - -function parseArgs() { - const args = process.argv.slice(2); - const url = args[0]; - const opts = { theme: null, css: null, prefs: null }; - for (let i = 1; i < args.length; i++) { - if (args[i] === "--theme" && args[i + 1]) opts.theme = args[++i]; - else if (args[i] === "--css" && args[i + 1]) opts.css = args[++i]; - else if (args[i] === "--prefs" && args[i + 1]) opts.prefs = args[++i]; - } - return { url, opts }; -} - -function loadPreferences(prefsPath) { - if (!prefsPath || !fs.existsSync(prefsPath)) return {}; - try { - return JSON.parse(fs.readFileSync(prefsPath, "utf-8")); - } catch (e) { - console.warn(`Warning: could not parse preferences file: ${prefsPath}`); - return {}; - } -} - -function readCustomCss(cssPath) { - if (!cssPath || !fs.existsSync(cssPath)) return null; - const css = fs.readFileSync(cssPath, "utf-8").trim(); - return css || null; -} - -function getOutputFilename(title) { - const safe = title - .replace(/[<>:"/\\|?*]/g, "") - .trim() - .slice(0, 120); - return `${safe || "article"}.pdf`; -} - -async function discoverExtensionId(browser) { - const page = await browser.newPage(); - await page.goto("chrome://extensions/", { timeout: 8000 }); - await new Promise(r => setTimeout(r, 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(); - if (!extId) { - throw new Error("Could not discover Reader View extension ID"); - } - console.log(`Discovered extension ID: ${extId}`); - return extId; -} - -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 readingSpeeds = { - en: { cpm: 987, variance: 118 }, ar: { cpm: 612, variance: 88 }, - de: { cpm: 920, variance: 86 }, es: { cpm: 1025, variance: 127 }, - fi: { cpm: 1078, variance: 121 }, fr: { cpm: 998, variance: 126 }, - he: { cpm: 833, variance: 130 }, it: { cpm: 950, variance: 131 }, - jp: { cpm: 891, variance: 121 }, nl: { cpm: 1021, variance: 96 }, - pl: { cpm: 916, variance: 126 }, pt: { cpm: 1007, variance: 122 }, - 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; - 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 || ""; - return { - ...parsed, - lang, - readingTimeMinsFast: Math.ceil(text.length / charsPerMinHigh), - readingTimeMinsSlow: Math.ceil(text.length / charsPerMinLow), - published_time: publishedTime ? (new Date(publishedTime)).toLocaleDateString() : "", - url: window.location.href, - }; - }); - if (!article) { - 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; -} - -async function storeArticle(extPage, extId, articleId, article) { - await extPage.goto( - `chrome-extension://${extId}/manifest.json`, - { timeout: 8000 } - ); - await extPage.evaluate(({ id, article }) => { - return new Promise((resolve, reject) => { - 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" }); - } - }; - 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"); - tx.oncomplete = () => { 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"); -} - -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; - try { - const body = iframe.contentDocument?.body; - if (!body) return false; - return body.textContent.trim().length > 200; - } catch { - return false; - } - }, - { timeout: 60000 } - ); - console.log("Article rendered."); -} - -async function setExtensionPreferences(page, extId, prefs) { - await page.goto( - `chrome-extension://${extId}/manifest.json`, - { 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)`); -} - -async function extractRenderedContent(page, baseUrl) { - const html = await page.evaluate((extBase) => { - const iframe = document.querySelector("#content iframe"); - if (!iframe) return null; - try { - const doc = iframe.contentDocument; - if (!doc) return null; - const b = doc.createElement("base"); - b.href = extBase; - doc.head.insertBefore(b, doc.head.firstChild); - return "\n" + doc.documentElement.outerHTML; - } catch { - return null; - } - }, baseUrl); - if (!html) throw new Error("Could not extract rendered content from iframe"); - return html; -} - -async function main() { - const { url, opts } = parseArgs(); - if (!url) { - console.error("Usage: node index.js [--theme ] [--css ] [--prefs ]"); - console.error("Themes: " + VALID_THEMES.join(", ")); - process.exit(1); - } - - 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); - } - 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); - } - - let preferences = {}; - if (opts.prefs) { - preferences = loadPreferences(path.resolve(opts.prefs)); - } - - 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); - } - preferences.mode = theme; - console.log(`Theme: ${theme}`); - - let customCss = null; - if (opts.css) { - const cssFile = path.resolve(opts.css); - customCss = readCustomCss(cssFile); - if (customCss !== null) { - preferences["user-css"] = customCss; - console.log(`Custom CSS loaded: ${cssFile}`); - } - } - - 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", - ], - args: [ - `--load-extension=${EXTENSION_DIR}`, - "--no-sandbox", - "--disable-setuid-sandbox", - ], - }); - - try { - 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, READABILITY_PATH); - await articlePage.close(); - - const extPage = await browser.newPage(); - await storeArticle(extPage, extId, 1, article); - - const prefsToSet = Object.fromEntries( - Object.entries(preferences).filter(([_, v]) => v !== undefined && v !== null) - ); - if (Object.keys(prefsToSet).length > 0) { - await setExtensionPreferences(extPage, extId, prefsToSet); - } - await extPage.close(); - - const readerUrl = [ - `chrome-extension://${extId}/data/reader/index.html`, - "?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 }); - - await waitForReaderView(readerPage); - await new Promise(r => setTimeout(r, 1000)); - - const title = await readerPage.evaluate(() => { - return (document.title || "").replace(" :: Reader View", ""); - }); - const outputFile = getOutputFilename(title); - - const contentHtml = await extractRenderedContent(readerPage, `chrome-extension://${extId}/data/reader/`); - await readerPage.close(); - - const highlightCss = ''; - const highlightJs = '