Add configurable highlighting style

This commit is contained in:
√(noham)²
2026-06-20 14:50:39 +02:00
parent 158d6b8eab
commit 5ce76ffea6
7 changed files with 43 additions and 27 deletions

View File

@@ -8,7 +8,7 @@ Convert web articles to PDF using the [Reader View](https://chromewebstore.googl
- Renders using Reader View for clean typography - Renders using Reader View for clean typography
- Supports all Reader View themes: `light`, `dark`, `sepia`, `groove-dark`, `solarized-light`, `solarized-dark`, `nord-light`, `nord-dark` - Supports all Reader View themes: `light`, `dark`, `sepia`, `groove-dark`, `solarized-light`, `solarized-dark`, `nord-light`, `nord-dark`
- Custom CSS injection - Custom CSS injection
- Syntax highlighting via highlight.js - Syntax highlighting via highlight.js (configurable style)
- Automatic reading time estimation - Automatic reading time estimation
- Configurable extension preferences - Configurable extension preferences
@@ -33,13 +33,14 @@ Arguments:
url URL of the article to convert url URL of the article to convert
Options: Options:
-t, --theme <theme> Reader View theme (light, dark, sepia, groove-dark, solarized-light, solarized-dark, nord-light, nord-dark) -t, --theme <theme> Reader View theme (light, dark, sepia, groove-dark, solarized-light, solarized-dark, nord-light, nord-dark)
-c, --css <path> path to custom CSS file --highlight-style <style> highlight.js style (e.g. github, monokai, solarized-light, atom-one-dark)
--config <path> path to config file (.pdfyrc, .pdfyrc.json, pdfy.config.js) -c, --css <path> path to custom CSS file
-p, --prefs <path> path to extension preferences JSON --config <path> path to config file (.pdfyrc, .pdfyrc.json, pdfy.config.js)
-o, --output <path> output file path (default: ./output/<title>.pdf) -p, --prefs <path> path to extension preferences JSON
-l, --log-level <level> logging level (fatal, error, warn, info, debug) -o, --output <path> output file path (default: ./output/<title>.pdf)
-h, --help display help for command -l, --log-level <level> logging level (fatal, error, warn, info, debug)
-h, --help display help for command
``` ```
### Configuration ### Configuration
@@ -78,6 +79,7 @@ CLI flags always override config file values. Use `--config <path>` to specify a
node src/index.js https://example.com/article 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 --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 --prefs config/reader-view-preferences.json
node src/index.js https://example.com/article --highlight-style monokai
node src/index.js https://example.com/article --config .pdfyrc node src/index.js https://example.com/article --config .pdfyrc
npm start -- https://example.com/article --theme dark --css config/rules.css npm start -- https://example.com/article --theme dark --css config/rules.css
``` ```
@@ -134,6 +136,7 @@ try {
|--------|------|---------|-------------| |--------|------|---------|-------------|
| `theme` | string | `'light'` | Reader View theme | | `theme` | string | `'light'` | Reader View theme |
| `css` | string | `null` | Custom CSS string | | `css` | string | `null` | Custom CSS string |
| `highlightStyle` | string | `'github'` | highlight.js style name |
| `prefs` | object | `{}` | Extension preferences | | `prefs` | object | `{}` | Extension preferences |
| `output` | string | `null` | File path to write PDF (returns buffer when omitted) | | `output` | string | `null` | File path to write PDF (returns buffer when omitted) |
| `browser` | Browser | `null` | Reuse a Puppeteer browser instance | | `browser` | Browser | `null` | Reuse a Puppeteer browser instance |
@@ -178,6 +181,7 @@ curl -X POST http://localhost:8080/convert \
| `url` | string | Article URL (required unless `html` is provided) | | `url` | string | Article URL (required unless `html` is provided) |
| `theme` | string | Reader View theme | | `theme` | string | Reader View theme |
| `css` | string | Custom CSS string | | `css` | string | Custom CSS string |
| `highlightStyle` | string | highlight.js style (default: `"github"`) |
| `html` | string | Pre-fetched HTML (bypasses URL fetch) | | `html` | string | Pre-fetched HTML (bypasses URL fetch) |
| `prefs` | object | Extension preferences | | `prefs` | object | Extension preferences |

View File

@@ -21,6 +21,10 @@ program
'-t, --theme <theme>', '-t, --theme <theme>',
`Reader View theme (${VALID_THEMES.join(', ')})` `Reader View theme (${VALID_THEMES.join(', ')})`
) )
.option(
'--highlight-style <style>',
'highlight.js style (e.g. github, monokai, solarized-light, atom-one-dark)'
)
.option('-c, --css <path>', 'path to custom CSS file') .option('-c, --css <path>', 'path to custom CSS file')
.option('--config <path>', 'path to config file (.pdfyrc, .pdfyrc.json, pdfy.config.js)') .option('--config <path>', 'path to config file (.pdfyrc, .pdfyrc.json, pdfy.config.js)')
.option('-p, --prefs <path>', 'path to extension preferences JSON') .option('-p, --prefs <path>', 'path to extension preferences JSON')
@@ -37,8 +41,8 @@ program
` `
Examples: Examples:
node src/index.js <URL> node src/index.js <URL>
node src/index.js <URL> --theme dark --css config/rules.css node src/index.js <URL> --theme dark --highlight-style monokai --css config/rules.css
npm start -- <URL> --theme dark --css config/rules.css npm start -- <URL> --theme dark --highlight-style monokai --css config/rules.css
` `
) )
@@ -52,7 +56,8 @@ export function parseArgs () {
css: opts.css || null, css: opts.css || null,
prefs: opts.prefs || null, prefs: opts.prefs || null,
output: opts.output || null, output: opts.output || null,
config: opts.config || null config: opts.config || null,
highlightStyle: opts.highlightStyle || null
} }
} }
} }

View File

@@ -11,6 +11,8 @@ function normalizeConfig (config) {
if (config.css) result.css = config.css if (config.css) result.css = config.css
if (config.prefs) result.prefs = config.prefs if (config.prefs) result.prefs = config.prefs
if (config.output) result.output = config.output if (config.output) result.output = config.output
if (config.highlightStyle) result.highlightStyle = config.highlightStyle
if (config['highlight-style']) result.highlightStyle = config['highlight-style']
if (config['log-level']) result.logLevel = config['log-level'] if (config['log-level']) result.logLevel = config['log-level']
if (config.logLevel) result.logLevel = config.logLevel if (config.logLevel) result.logLevel = config.logLevel
return result return result

View File

@@ -23,9 +23,9 @@ async function main () {
const config = await loadConfigFile(opts.config) const config = await loadConfigFile(opts.config)
const prefsPath = opts.prefs || null const prefsPath = opts.prefs || config.prefs || null
const cssPath = opts.css || null const cssPath = opts.css || config.css || null
const outputPath = opts.output || null const outputPath = opts.output || config.output || null
const preferences = prefsPath ? loadPreferences(path.resolve(prefsPath)) : {} const preferences = prefsPath ? loadPreferences(path.resolve(prefsPath)) : {}
const customCss = cssPath ? readCustomCss(path.resolve(cssPath)) : null const customCss = cssPath ? readCustomCss(path.resolve(cssPath)) : null
@@ -37,10 +37,11 @@ async function main () {
try { try {
const result = await convert(currentUrl, { const result = await convert(currentUrl, {
html, html,
theme: opts.theme || preferences.mode, theme: opts.theme || config.theme || preferences.mode || 'light',
css: customCss, css: customCss,
prefs: preferences, prefs: preferences,
output: outputPath output: outputPath,
highlightStyle: opts.highlightStyle || config.highlightStyle
}) })
const sizeKb = (result.pdfBuffer.length / 1024).toFixed(0) const sizeKb = (result.pdfBuffer.length / 1024).toFixed(0)

View File

@@ -2,11 +2,13 @@ import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import logger from './logger.js' import logger from './logger.js'
const HIGHLIGHT_CSS =
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">'
const HIGHLIGHT_JS = 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>'
function highlightCss (style) {
return `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/${style}.min.css">`
}
export function getOutputFilename (title, outputDir) { export function getOutputFilename (title, outputDir) {
const safe = title const safe = title
.replace(/[<>:"/\\|?*]/g, '') .replace(/[<>:"/\\|?*]/g, '')
@@ -27,10 +29,10 @@ export function resolveOutputPath (outputFlag) {
return resolved.endsWith('.pdf') ? resolved : `${resolved}.pdf` return resolved.endsWith('.pdf') ? resolved : `${resolved}.pdf`
} }
export function enhanceHtml (html, customCss) { export function enhanceHtml (html, customCss, highlightStyle = 'github') {
let enhanced = html.replace( let enhanced = html.replace(
'</head>', '</head>',
`${HIGHLIGHT_CSS}${HIGHLIGHT_JS}</head>` `${highlightCss(highlightStyle)}${HIGHLIGHT_JS}</head>`
) )
if (customCss !== null) { if (customCss !== null) {
enhanced = enhanced.replace( enhanced = enhanced.replace(
@@ -51,8 +53,8 @@ export async function generatePdf (page, html, outputPath, { timeout = 30000 } =
format: 'A4', format: 'A4',
printBackground: true, printBackground: true,
margin: { margin: {
top: '0mm', top: '10mm',
bottom: '0mm', bottom: '10mm',
left: '0mm', left: '0mm',
right: '0mm' right: '0mm'
} }

View File

@@ -37,7 +37,8 @@ export async function convert (url, options = {}) {
css = null, css = null,
prefs = {}, prefs = {},
output = null, output = null,
browser: externalBrowser = null browser: externalBrowser = null,
highlightStyle
} = options } = options
if (!VALID_THEMES.includes(theme)) { if (!VALID_THEMES.includes(theme)) {
@@ -112,7 +113,7 @@ export async function convert (url, options = {}) {
contentHtml = contentHtml.replace(/ loading="lazy"/g, '') contentHtml = contentHtml.replace(/ loading="lazy"/g, '')
await readerPage.close() await readerPage.close()
const enhancedHtml = enhanceHtml(contentHtml, css) const enhancedHtml = enhanceHtml(contentHtml, css, highlightStyle)
const pdfPage = await browser.newPage() const pdfPage = await browser.newPage()
await pdfPage.setViewport({ width: 1280, height: 7200 }) await pdfPage.setViewport({ width: 1280, height: 7200 })

View File

@@ -29,7 +29,7 @@ async function handleConvert (req, res) {
return sendJson(res, 400, { error: 'Invalid JSON body' }) return sendJson(res, 400, { error: 'Invalid JSON body' })
} }
const { url, theme, css, prefs, html } = body const { url, theme, css, prefs, html, highlightStyle } = body
if (!url && !html) { if (!url && !html) {
return sendJson(res, 400, { error: 'Missing required field: url (or html for direct content)' }) return sendJson(res, 400, { error: 'Missing required field: url (or html for direct content)' })
@@ -47,7 +47,8 @@ async function handleConvert (req, res) {
html: html || null, html: html || null,
theme: theme || 'light', theme: theme || 'light',
css: css || null, css: css || null,
prefs: prefs || {} prefs: prefs || {},
highlightStyle
}) })
const sizeKb = (result.pdfBuffer.length / 1024).toFixed(0) const sizeKb = (result.pdfBuffer.length / 1024).toFixed(0)
@@ -92,7 +93,7 @@ const server = createServer((req, res) => {
pdfy server pdfy server
POST /convert Convert a URL to PDF POST /convert Convert a URL to PDF
Body: { "url": "...", "theme": "...", "css": "...", "html": "..." } Body: { "url": "...", "theme": "...", "css": "...", "html": "...", "highlightStyle": "..." }
GET /health Health check GET /health Health check
GET / This help GET / This help