diff --git a/README.md b/README.md index 8a5f916b05..80f0ac27e2 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ const browserless = require('browserless')() browserless .screenshot('http://example.com', { device: 'iPhone 6' }) .then(buffer => { - console.log(`your screenshot is here!`) + console.log('your screenshot is here!') }) ``` @@ -160,7 +160,7 @@ const browserless = require('browserless') ;(async () => { const url = 'https://example.com' const buffer = await browserless.pdf(url) - console.log(`PDF generated!`) + console.log('PDF generated!') })() ``` @@ -229,7 +229,7 @@ const browserless = require('browserless') ;(async () => { const url = 'https://example.com' const buffer = await browserless.screenshot(url) - console.log(`Screenshot taken!`) + console.log('Screenshot taken!') })() ``` @@ -249,6 +249,20 @@ Also, any [page.screenshot](https://github.com/puppeteer/puppeteer/blob/master/d Additionally, you can setup: +##### codeScheme + +type: `string` +default: `'atom-dark'` + +When this value is present and the response `'Content-Type'` header is `'json'`, it beautifies HTML markup using [Prism](https://prismjs.com). + + + +The syntax highlight theme can be customized, being possible to setup: + +- A [prism-themes](https://github.com/PrismJS/prism-themes/tree/master/themes) identifier (e.g., `'dracula'`). +- A remote URL (e.g., `'https://unpkg.com/prism-theme-night-owl'`). + ##### element type: `string` @@ -543,7 +557,7 @@ It can accept: scripts: [ 'https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js', 'local-file.js', - `document.body.style.backgroundColor = 'red` + "document.body.style.backgroundColor = 'red'" ] }) })() @@ -575,7 +589,7 @@ It can accept: styles: [ 'https://cdn.jsdelivr.net/npm/hack@0.8.1/dist/dark.css', 'local-file.css', - `body { background: red; }` + 'body { background: red; }' ] }) })() @@ -656,7 +670,7 @@ After that, the API is the same than **browserless**: browserlessPool .screenshot('http://example.com', { device: 'iPhone 6' }) .then(buffer => { - console.log(`your screenshot is here!`) + console.log('your screenshot is here!') }) ``` diff --git a/package.json b/package.json index 2a47fb104d..74583aa39a 100644 --- a/package.json +++ b/package.json @@ -97,5 +97,10 @@ "package.json": [ "finepack" ] + }, + "standard": { + "ignore": [ + "packages/screenshot/src/pretty/prism.js" + ] } } diff --git a/packages/goto/package.json b/packages/goto/package.json index 5b8ea82401..aa16013cb9 100644 --- a/packages/goto/package.json +++ b/packages/goto/package.json @@ -32,6 +32,7 @@ "@cliqz/adblocker-puppeteer": "~1.7.2", "debug-logfmt": "~1.0.4", "got": "~10.5.5", + "is-url-http": "~2.1.1", "p-reflect": "~2.1.0", "p-timeout": "~3.2.0", "tldts": "~5.6.3" diff --git a/packages/goto/src/index.js b/packages/goto/src/index.js index fb9c977ee5..6fd3081106 100644 --- a/packages/goto/src/index.js +++ b/packages/goto/src/index.js @@ -6,6 +6,7 @@ const createDevices = require('@browserless/devices') const { getDomain } = require('tldts') const pReflect = require('p-reflect') const pTimeout = require('p-timeout') +const isUrl = require('is-url-http') const path = require('path') const fs = require('fs') @@ -20,8 +21,6 @@ const isEmpty = val => val == null || !(Object.keys(val) || val).length const toArray = value => [].concat(value) -const isUrl = string => /^(https?|file):\/\/|^data:/.test(string) - const getInjectKey = (ext, value) => isUrl(value) ? 'url' : value.endsWith(`.${ext}`) ? 'path' : 'content' @@ -138,6 +137,32 @@ const getMediaFeatures = ({ animations, colorScheme }) => { return prefers } +const injectScripts = (page, values, attributes) => + Promise.all( + toArray(values).map(value => + pReflect( + page.addScriptTag({ + [getInjectKey('js', value)]: value, + ...attributes + }) + ) + ) + ) + +const injectStyles = (page, styles) => + Promise.all( + toArray(styles).map(style => + pReflect( + page.addStyleTag({ + [getInjectKey('css', style)]: style + }) + ) + ) + ) + +const forEachSelector = (page, selectors, fn) => + toArray(selectors).map(selector => pReflect(page.$$eval(selector, fn))) + module.exports = ({ timeout, ...deviceOpts }) => { const gotoTimeout = timeout * (1 / 4) const getDevice = createDevices(deviceOpts) @@ -219,19 +244,14 @@ module.exports = ({ timeout, ...deviceOpts }) => { await page.evaluate(disableAnimations) } - if (hide) { - debug({ hide }) - await Promise.all( - toArray(hide).map(selector => pReflect(page.$$eval(selector, hideElements))) - ) - } + debug({ hide, remove }) - if (remove) { - debug({ remove }) - await Promise.all( - toArray(remove).map(selector => pReflect(page.$$eval(selector, removeElements))) - ) - } + await Promise.all( + [ + hide && forEachSelector(page, hide, hideElements), + remove && forEachSelector(page, hide, removeElements) + ].filter(Boolean) + ) if (click) { for (const selector of toArray(click)) { @@ -240,42 +260,15 @@ module.exports = ({ timeout, ...deviceOpts }) => { } } - if (modules) { - await Promise.all( - toArray(modules).map(m => - pReflect( - page.addScriptTag({ - [getInjectKey('js', m)]: m, - type: 'module' - }) - ) - ) - ) - } + debug({ modules, scripts, styles }) - if (scripts) { - await Promise.all( - toArray(scripts).map(script => - pReflect( - page.addScriptTag({ - [getInjectKey('js', script)]: script - }) - ) - ) - ) - } - - if (styles) { - await Promise.all( - toArray(styles).map(style => - pReflect( - page.addStyleTag({ - [getInjectKey('css', style)]: style - }) - ) - ) - ) - } + await Promise.all( + [ + modules && injectScripts(page, modules, { type: 'modules' }), + scripts && injectScripts(page, scripts), + styles && injectStyles(page, styles) + ].filter(Boolean) + ) if (scrollTo) { debug({ scrollTo }) @@ -296,3 +289,5 @@ module.exports = ({ timeout, ...deviceOpts }) => { } module.exports.parseCookies = parseCookies +module.exports.injectScripts = injectScripts +module.exports.injectStyles = injectStyles diff --git a/packages/screenshot/media/browser.sketch b/packages/screenshot/media/browser.sketch index 4bb202e3ed..901e9dd559 100644 Binary files a/packages/screenshot/media/browser.sketch and b/packages/screenshot/media/browser.sketch differ diff --git a/packages/screenshot/package.json b/packages/screenshot/package.json index d35cb3a2a0..9d7c98ec5c 100644 --- a/packages/screenshot/package.json +++ b/packages/screenshot/package.json @@ -30,6 +30,8 @@ "@browserless/goto": "^6.4.2", "got": "~10.5.5", "is-url-http": "~2.1.1", + "mime-types": "~2.1.26", + "prism-themes": "~1.3.0", "sharp": "~0.24.0", "svg-gradient": "~1.0.2" }, diff --git a/packages/screenshot/src/browser/dark.png b/packages/screenshot/src/browser/dark.png deleted file mode 100644 index cab284eee0..0000000000 Binary files a/packages/screenshot/src/browser/dark.png and /dev/null differ diff --git a/packages/screenshot/src/browser/light.png b/packages/screenshot/src/browser/light.png deleted file mode 100644 index 13ad4b4f57..0000000000 Binary files a/packages/screenshot/src/browser/light.png and /dev/null differ diff --git a/packages/screenshot/src/prepare.js b/packages/screenshot/src/goto.js similarity index 71% rename from packages/screenshot/src/prepare.js rename to packages/screenshot/src/goto.js index 13b786c388..49ef6d0295 100644 --- a/packages/screenshot/src/prepare.js +++ b/packages/screenshot/src/goto.js @@ -10,14 +10,12 @@ const getBoundingClientRect = element => { module.exports = ({ goto, ...gotoOpts } = {}) => { goto = goto || createGoto(gotoOpts) - return async (page, url, opts = {}) => { - const { device: deviceId = 'macbook pro 13', overlay, element, fullPage, ...args } = opts - + return async (page, url, { device = 'macbook pro 13', element, ...opts } = {}) => { page.on('dialog', async dialog => { await dialog.dismiss() }) - const { device } = await goto(page, { url, device: deviceId, ...args }) + const { response } = await goto(page, { url, device, ...opts }) const screenshotOptions = {} @@ -27,6 +25,6 @@ module.exports = ({ goto, ...gotoOpts } = {}) => { screenshotOptions.fullPage = false } - return { device, ...screenshotOptions } + return [screenshotOptions, response] } } diff --git a/packages/screenshot/src/index.js b/packages/screenshot/src/index.js index 961d3fbc41..187821e3df 100644 --- a/packages/screenshot/src/index.js +++ b/packages/screenshot/src/index.js @@ -1,53 +1,33 @@ 'use strict' -const svgGradient = require('svg-gradient') -const isHttpUrl = require('is-url-http') -const sharp = require('sharp') -const path = require('path') -const got = require('got') +const { extension } = require('mime-types') -const createPreparePage = require('./prepare') +const createGoto = require('./goto') +const overlay = require('./overlay') +const pretty = require('./pretty') -const BROWSER_THEMES = { - dark: path.resolve(__dirname, 'browser/dark.png'), - light: path.resolve(__dirname, 'browser/light.png') -} - -const getBackground = async (bg = 'transparent') => { - if (isHttpUrl(bg)) return got(bg).buffer() - - if (!bg.includes('gradient')) { - bg = `linear-gradient(45deg, ${bg} 0%, ${bg} 100%)` - } - - return Buffer.from(createSvgBackground(bg)) -} - -const createSvgBackground = css => svgGradient(css, { width: '2776px', height: '1910px' }) +const isJSON = headers => extension(headers['content-type']) === 'json' module.exports = gotoOpts => { - const preparePage = createPreparePage(gotoOpts) + const goto = createGoto(gotoOpts) - return page => async (url, { type = 'png', overlay = {}, ...opts } = {}) => { - const screenshotOpts = { - ...opts, - ...(await preparePage(page, url, { overlay, ...opts })), - type - } - - const screenshot = await page.screenshot(screenshotOpts) - - if (Object.keys(overlay).length === 0) return screenshot + return page => async ( + url, + { codeScheme = 'atom-dark', overlay: overlayOpts = {}, ...opts } = {} + ) => { + const [screenshotOpts, response] = await goto(page, url, opts) - const { browser: theme, background } = overlay - const browserOverlay = BROWSER_THEMES[theme] - - const inputs = browserOverlay - ? [{ input: browserOverlay }, { input: screenshot }] - : [{ input: screenshot }] + if (codeScheme && isJSON(response.headers())) { + await pretty(page, response, { codeScheme, ...opts }) + } - const image = sharp(await getBackground(background)).composite(inputs) + const screenshot = await page.screenshot({ + ...opts, + ...screenshotOpts + }) - return opts.path ? image.toFile(opts.path) : image.toBuffer() + return Object.keys(overlayOpts).length === 0 + ? screenshot + : overlay(screenshot, { ...opts, ...overlayOpts }) } } diff --git a/packages/screenshot/src/overlay/dark.png b/packages/screenshot/src/overlay/dark.png new file mode 100644 index 0000000000..afc0df7e7d Binary files /dev/null and b/packages/screenshot/src/overlay/dark.png differ diff --git a/packages/screenshot/src/overlay/index.js b/packages/screenshot/src/overlay/index.js new file mode 100644 index 0000000000..f1318ea6c6 --- /dev/null +++ b/packages/screenshot/src/overlay/index.js @@ -0,0 +1,36 @@ +'use strict' + +const svgGradient = require('svg-gradient') +const isHttpUrl = require('is-url-http') +const sharp = require('sharp') +const path = require('path') +const got = require('got') + +const createSvgBackground = css => svgGradient(css, { width: '2776px', height: '1910px' }) + +const getBackground = async (bg = 'transparent') => { + if (isHttpUrl(bg)) return got(bg).buffer() + + if (!bg.includes('gradient')) { + bg = `linear-gradient(45deg, ${bg} 0%, ${bg} 100%)` + } + + return Buffer.from(createSvgBackground(bg)) +} + +const BROWSER_THEMES = { + dark: path.resolve(__dirname, 'dark.png'), + light: path.resolve(__dirname, 'light.png') +} + +module.exports = async (screenshot, { browser: theme, background, path }) => { + const browserOverlay = BROWSER_THEMES[theme] + + const inputs = browserOverlay + ? [{ input: browserOverlay }, { input: screenshot }] + : [{ input: screenshot }] + + const image = sharp(await getBackground(background)).composite(inputs) + + return path ? image.toFile(path) : image.toBuffer() +} diff --git a/packages/screenshot/src/overlay/light.png b/packages/screenshot/src/overlay/light.png new file mode 100644 index 0000000000..3e9c71c8c9 Binary files /dev/null and b/packages/screenshot/src/overlay/light.png differ diff --git a/packages/screenshot/src/pretty/html.js b/packages/screenshot/src/pretty/html.js new file mode 100644 index 0000000000..bc1dd13fe9 --- /dev/null +++ b/packages/screenshot/src/pretty/html.js @@ -0,0 +1,42 @@ +module.exports = (payload, { prism, theme }) => ` + +
+ ${theme} + + + +
+
+${JSON.stringify(payload, null, 2)}
+
+
+
+
+`
diff --git a/packages/screenshot/src/pretty/index.js b/packages/screenshot/src/pretty/index.js
new file mode 100644
index 0000000000..d79847b412
--- /dev/null
+++ b/packages/screenshot/src/pretty/index.js
@@ -0,0 +1,29 @@
+'use strict'
+
+const { readFile } = require('fs').promises
+const path = require('path')
+
+const getPrism = readFile(path.resolve(__dirname, 'prism.js'))
+const getTheme = require('./theme')
+const getHtml = require('./html')
+
+const { injectScripts, injectStyles } = require('@browserless/goto')
+
+module.exports = async (page, response, { codeScheme, styles, scripts, modules }) => {
+ const [theme, payload, prism] = await Promise.all([
+ getTheme(codeScheme),
+ response.json(),
+ getPrism
+ ])
+
+ const html = getHtml(payload, { prism, theme })
+ await page.setContent(html)
+
+ await Promise.all(
+ [
+ modules && injectScripts(page, modules, { type: 'modules' }),
+ scripts && injectScripts(page, scripts),
+ styles && injectStyles(page, styles)
+ ].filter(Boolean)
+ )
+}
diff --git a/packages/screenshot/src/pretty/prism.js b/packages/screenshot/src/pretty/prism.js
new file mode 100644
index 0000000000..c386ede3cc
--- /dev/null
+++ b/packages/screenshot/src/pretty/prism.js
@@ -0,0 +1,5 @@
+/* PrismJS 1.19.0
+https://prismjs.com/download.html#themes=prism&languages=clike+javascript */
+var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,n=0,C={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof _?new _(e.type,C.util.encode(e.content),e.alias):Array.isArray(e)?e.map(C.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(k instanceof _)){if(h&&y!=n.length-1){if(c.lastIndex=v,!(O=c.exec(e)))break;for(var b=O.index+(f&&O[1]?O[1].length:0),w=O.index+O[0].length,A=y,P=v,x=n.length;A