mirror of
https://github.com/NohamR/pdfy.git
synced 2026-10-10 18:29:55 +00:00
Add configurable highlighting style
This commit is contained in:
20
README.md
20
README.md
@@ -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 |
|
||||||
|
|
||||||
|
|||||||
11
src/cli.js
11
src/cli.js
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
11
src/index.js
11
src/index.js
@@ -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)
|
||||||
|
|||||||
14
src/pdf.js
14
src/pdf.js
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user