diff --git a/.gitignore b/.gitignore index 5e63730..3b7e4f3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ extension/Reader-View-Chrome-Web-Store.zip /output config/reader-view-preferences.json config/rules.css +.pdfyrc diff --git a/.pdfyrc.example b/.pdfyrc.example new file mode 100644 index 0000000..2f47927 --- /dev/null +++ b/.pdfyrc.example @@ -0,0 +1,4 @@ +{ + "theme": "light", + "css": "config/rules.css" +} \ No newline at end of file diff --git a/README.md b/README.md index 6177624..df864e0 100644 --- a/README.md +++ b/README.md @@ -35,18 +35,50 @@ Arguments: Options: -t, --theme Reader View theme (light, dark, sepia, groove-dark, solarized-light, solarized-dark, nord-light, nord-dark) -c, --css path to custom CSS file + --config path to config file (.pdfyrc, .pdfyrc.json, pdfy.config.js) -p, --prefs path to extension preferences JSON -o, --output output file path (default: ./output/.pdf) -l, --log-level <level> logging level (fatal, error, warn, info, debug) -h, --help display help for command ``` +### Configuration + +pdfy automatically discovers a config file in the current directory. Supported formats: + +- `.pdfyrc` (JSON) +- `.pdfyrc.json` (JSON) +- `pdfy.config.js` (ESM module exporting a default object) + +Example `.pdfyrc`: +```json +{ + "theme": "dark", + "css": "config/rules.css", + "prefs": "config/reader-view-preferences.json", + "output": "./output/article.pdf" +} +``` + +Example `pdfy.config.js`: +```js +export default { + theme: 'dark', + css: 'config/rules.css', + prefs: 'config/reader-view-preferences.json', + output: './output/article.pdf' +} +``` + +CLI flags always override config file values. Use `--config <path>` to specify a custom config file location. + ### Examples ```bash node src/index.js https://example.com/article node src/index.js https://example.com/article --theme dark --css config/rules.css node src/index.js https://example.com/article --prefs config/reader-view-preferences.json +node src/index.js https://example.com/article --config .pdfyrc npm start -- https://example.com/article --theme dark --css config/rules.css ``` diff --git a/src/cli.js b/src/cli.js index 95bc0c7..a588a85 100644 --- a/src/cli.js +++ b/src/cli.js @@ -22,6 +22,7 @@ program `Reader View theme (${VALID_THEMES.join(', ')})` ) .option('-c, --css <path>', 'path to custom CSS file') + .option('--config <path>', 'path to config file (.pdfyrc, .pdfyrc.json, pdfy.config.js)') .option('-p, --prefs <path>', 'path to extension preferences JSON') .option( '-o, --output <path>', @@ -50,7 +51,8 @@ export function parseArgs () { theme: opts.theme || null, css: opts.css || null, prefs: opts.prefs || null, - output: opts.output || null + output: opts.output || null, + config: opts.config || null } } } diff --git a/src/config.js b/src/config.js index 6cb3865..bb2dc74 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,54 @@ import fs from 'node:fs' import path from 'node:path' +import { pathToFileURL } from 'node:url' import logger from './logger.js' +const CONFIG_FILE_NAMES = ['.pdfyrc', '.pdfyrc.json', 'pdfy.config.js'] + +function normalizeConfig (config) { + const result = {} + if (config.theme) result.theme = config.theme + if (config.css) result.css = config.css + if (config.prefs) result.prefs = config.prefs + if (config.output) result.output = config.output + if (config['log-level']) result.logLevel = config['log-level'] + if (config.logLevel) result.logLevel = config.logLevel + return result +} + +function findConfigFile () { + for (const name of CONFIG_FILE_NAMES) { + const fullPath = path.resolve(name) + if (fs.existsSync(fullPath)) return fullPath + } + return null +} + +async function parseConfigFile (filePath) { + if (filePath.endsWith('.js')) { + const mod = await import(pathToFileURL(filePath).href) + return normalizeConfig(mod.default || {}) + } + const raw = fs.readFileSync(filePath, 'utf-8') + return normalizeConfig(JSON.parse(raw)) +} + +export async function loadConfigFile (configPath) { + const target = configPath + ? path.resolve(configPath) + : findConfigFile() + + if (!target || !fs.existsSync(target)) return {} + + logger.debug(`Config loaded from: "${target}"`) + try { + return await parseConfigFile(target) + } catch (err) { + logger.warn(`Could not parse config file "${target}": ${err.message}`) + return {} + } +} + export function loadPreferences (prefsPath) { if (!prefsPath || !fs.existsSync(prefsPath)) return {} try { diff --git a/src/index.js b/src/index.js index 00a954c..f489cd6 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,7 @@ import process from 'node:process' import path from 'node:path' import readline from 'node:readline/promises' import { parseArgs, VALID_THEMES } from './cli.js' -import { loadPreferences, readCustomCss } from './config.js' +import { loadConfigFile, loadPreferences, readCustomCss } from './config.js' import logger from './logger.js' import { checkExtension, @@ -43,12 +43,18 @@ async function main () { await checkExtension() + const config = await loadConfigFile(opts.config) + + const prefsPath = opts.prefs || config.prefs || null + const cssPath = opts.css || config.css || null + const outputPath = opts.output || config.output || null + let preferences = {} - if (opts.prefs) { - preferences = loadPreferences(path.resolve(opts.prefs)) + if (prefsPath) { + preferences = loadPreferences(path.resolve(prefsPath)) } - const theme = opts.theme || preferences.mode || 'light' + const theme = opts.theme || config.theme || preferences.mode || 'light' if (!VALID_THEMES.includes(theme)) { logger.error( `Invalid theme "${theme}". Valid themes: ${VALID_THEMES.join(', ')}` @@ -59,11 +65,11 @@ async function main () { logger.info(`Theme: ${theme}`) let customCss = null - if (opts.css) { - customCss = readCustomCss(path.resolve(opts.css)) + if (cssPath) { + customCss = readCustomCss(path.resolve(cssPath)) if (customCss !== null) { preferences['user-css'] = customCss - logger.info(`Custom CSS loaded: "${path.resolve(opts.css)}"`) + logger.info(`Custom CSS loaded: "${path.resolve(cssPath)}"`) } } @@ -124,8 +130,8 @@ async function main () { await new Promise((resolve) => setTimeout(resolve, 1000)) const title = await getArticleTitle(readerPage) - const outputFile = opts.output - ? resolveOutputPath(opts.output) + const outputFile = outputPath + ? resolveOutputPath(outputPath) : getOutputFilename(title) let contentHtml = await extractRenderedContent(readerPage)