mirror of
https://github.com/jxnblk/mdx-deck.git
synced 2024-11-29 13:58:02 +03:00
Merge pull request #107 from jxnblk/head-component
Add Head component and support for OG metadata
This commit is contained in:
commit
4c7c89d463
@ -4,6 +4,7 @@ node_js:
|
||||
before_deploy:
|
||||
- npm install
|
||||
- npm run build
|
||||
- npm run screenshot
|
||||
deploy:
|
||||
provider: pages
|
||||
skip_cleanup: true
|
||||
|
15
README.md
15
README.md
@ -262,16 +262,8 @@ Add a `build` script to your `package.json` to export a presentation as HTML wit
|
||||
}
|
||||
```
|
||||
|
||||
### PDF Export
|
||||
See more exporting options in the [Exporting Documentation](docs/exporting.md)
|
||||
|
||||
Presentations can be exported as PDF using the CLI.
|
||||
This works well as a backup option for any unforeseen technical difficulties.
|
||||
|
||||
```json
|
||||
"script": {
|
||||
"pdf": "mdx-deck pdf deck.mdx"
|
||||
}
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
@ -279,7 +271,9 @@ This works well as a backup option for any unforeseen technical difficulties.
|
||||
-p --port Dev server port
|
||||
--no-open Prevent from opening in default browser
|
||||
-d --out-dir Output directory for exporting
|
||||
--title Title for the HTML document
|
||||
--out-file Filename for screenshot or PDF export
|
||||
--width Width in pixels
|
||||
--height Height in pixels
|
||||
```
|
||||
|
||||
## Docs
|
||||
@ -288,6 +282,7 @@ This works well as a backup option for any unforeseen technical difficulties.
|
||||
- [Built-in Themes](docs/themes.md)
|
||||
- [Layouts](docs/layouts.md)
|
||||
- [Components](docs/components.md)
|
||||
- [Exporting](docs/exporting.md)
|
||||
- [Advanced Usage](docs/advanced.md)
|
||||
- [React API](docs/react.md)
|
||||
|
||||
|
103
cli.js
103
cli.js
@ -3,7 +3,6 @@ const path = require('path')
|
||||
const meow = require('meow')
|
||||
const open = require('react-dev-utils/openBrowser')
|
||||
const chalk = require('chalk')
|
||||
const ok = require('ok-cli')
|
||||
const remark = {
|
||||
emoji: require('remark-emoji'),
|
||||
unwrapImages: require('remark-unwrap-images')
|
||||
@ -11,53 +10,7 @@ const remark = {
|
||||
const pkg = require('./package.json')
|
||||
|
||||
const config = require('pkg-conf').sync('mdx-deck')
|
||||
|
||||
const log = (...args) => {
|
||||
console.log(
|
||||
chalk.magenta('[mdx-deck]'),
|
||||
...args
|
||||
)
|
||||
}
|
||||
log.error = (...args) => {
|
||||
console.log(
|
||||
chalk.red('[err]'),
|
||||
...args
|
||||
)
|
||||
}
|
||||
|
||||
const getConfig = conf => {
|
||||
conf.module.rules = [
|
||||
...conf.module.rules
|
||||
.filter(rule => !rule.test.test('.mdx')),
|
||||
{
|
||||
test: /\.mdx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
presets: [
|
||||
'babel-preset-env',
|
||||
'babel-preset-stage-0',
|
||||
'babel-preset-react',
|
||||
].map(require.resolve)
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: require.resolve('./lib/loader.js'),
|
||||
options: {
|
||||
mdPlugins: [
|
||||
remark.emoji,
|
||||
remark.unwrapImages
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
return conf
|
||||
}
|
||||
const log = require('./lib/log')
|
||||
|
||||
const cli = meow(`
|
||||
${chalk.gray('Usage')}
|
||||
@ -68,9 +21,9 @@ const cli = meow(`
|
||||
|
||||
$ ${chalk.magenta('mdx-deck pdf deck.mdx')}
|
||||
|
||||
${chalk.gray('Options')}
|
||||
$ ${chalk.magenta('mdx-deck screenshot deck.mdx')}
|
||||
|
||||
--title Title for the HTML document
|
||||
${chalk.gray('Options')}
|
||||
|
||||
${chalk.gray('Dev server options')}
|
||||
|
||||
@ -81,11 +34,11 @@ const cli = meow(`
|
||||
|
||||
-d --out-dir Output directory for exporting
|
||||
|
||||
${chalk.gray('PDF options')}
|
||||
${chalk.gray('Export options')}
|
||||
|
||||
--out-file Filename for PDF export
|
||||
--out-file Filename for screenshot or PDF export
|
||||
--width Width in pixels
|
||||
--heigh Height in pixels
|
||||
--height Height in pixels
|
||||
|
||||
`, {
|
||||
description: chalk.magenta('[mdx-deck] ') + chalk.gray(pkg.description),
|
||||
@ -103,9 +56,6 @@ const cli = meow(`
|
||||
type: 'string',
|
||||
alias: 'd'
|
||||
},
|
||||
title: {
|
||||
type: 'string'
|
||||
},
|
||||
outFile: {
|
||||
type: 'string',
|
||||
}
|
||||
@ -118,26 +68,26 @@ const doc = file || cmd
|
||||
if (!doc) cli.showHelp(0)
|
||||
|
||||
const opts = Object.assign({
|
||||
entry: path.join(__dirname, './dist/entry.js'),
|
||||
dirname: path.dirname(path.resolve(doc)),
|
||||
globals: {
|
||||
DOC_FILENAME: JSON.stringify(path.resolve(doc))
|
||||
FILENAME: JSON.stringify(path.resolve(doc))
|
||||
},
|
||||
config: getConfig,
|
||||
title: 'mdx-deck',
|
||||
port: 8080,
|
||||
outDir: 'dist',
|
||||
outFile: 'presentation.pdf'
|
||||
}, config, cli.flags)
|
||||
|
||||
opts.outDir = path.resolve(opts.outDir)
|
||||
|
||||
let dev
|
||||
|
||||
switch (cmd) {
|
||||
case 'build':
|
||||
log('building')
|
||||
ok.build(opts)
|
||||
const build = require('./lib/build')
|
||||
build(opts)
|
||||
.then(res => {
|
||||
log('done')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
@ -147,7 +97,8 @@ switch (cmd) {
|
||||
case 'pdf':
|
||||
log('exporting to PDF')
|
||||
const pdf = require('./lib/pdf')
|
||||
ok(opts)
|
||||
dev = require('./lib/dev')
|
||||
dev(opts)
|
||||
.then(({ server }) => {
|
||||
log('rendering PDF')
|
||||
pdf(opts)
|
||||
@ -166,10 +117,34 @@ switch (cmd) {
|
||||
process.exit(1)
|
||||
})
|
||||
break
|
||||
case 'screenshot':
|
||||
log('exporting to PNG')
|
||||
const screenshot = require('./lib/screenshot')
|
||||
dev = require('./lib/dev')
|
||||
dev(opts)
|
||||
.then(({ server }) => {
|
||||
log('rendering screenshot')
|
||||
screenshot(opts)
|
||||
.then(filename => {
|
||||
server.close()
|
||||
log('done', filename)
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
break
|
||||
case 'dev':
|
||||
default:
|
||||
log('starting dev server')
|
||||
ok(opts)
|
||||
dev = require('./lib/dev')
|
||||
dev(opts)
|
||||
.then(res => {
|
||||
const url = 'http://localhost:' + res.port
|
||||
if (opts.open) open(url)
|
||||
|
@ -3,6 +3,24 @@
|
||||
|
||||
mdx-deck includes a few built-in components to help with creating presentations.
|
||||
|
||||
## Head
|
||||
|
||||
Use the `<Head />` component to set content in the document head.
|
||||
|
||||
```mdx
|
||||
// example for twitter cards
|
||||
import { Head } from 'mdx-deck'
|
||||
|
||||
<Head>
|
||||
<title>My Presentation</title>
|
||||
<meta name='twitter:card' content='summary_large_image' />
|
||||
<meta name='twitter:site' content='@jxnblk' />
|
||||
<meta name='twitter:title' content='My Presentation' />
|
||||
<meta name='twitter:description' content='A really great presentation' />
|
||||
<meta name='twitter:image' content='https://example.com/card.png' />
|
||||
</Head>
|
||||
```
|
||||
|
||||
## Image
|
||||
|
||||
Use the `<Image />` component to render a fullscreen image (using the CSS `background-image` property).
|
||||
|
49
docs/exporting.md
Normal file
49
docs/exporting.md
Normal file
@ -0,0 +1,49 @@
|
||||
|
||||
# Exporting
|
||||
|
||||
## Static Bundle
|
||||
|
||||
To export your deck as a static HTML page with JS bundle,
|
||||
add a `build` script to your `package.json` file.
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"build": "mdx-deck build deck.mdx"
|
||||
}
|
||||
```
|
||||
|
||||
## PDF Export
|
||||
|
||||
Presentations can be exported as PDF using the CLI.
|
||||
This works well as a backup option for any unforeseen technical difficulties.
|
||||
|
||||
```json
|
||||
"script": {
|
||||
"pdf": "mdx-deck pdf deck.mdx"
|
||||
}
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
A PNG image of the first slide can be exported with the `screenshot` command.
|
||||
This is useful for creating open graph images for Twitter, Facebook, or Slack.
|
||||
|
||||
```json
|
||||
"script": {
|
||||
"screenshot": "mdx-deck screenshot deck.mdx"
|
||||
}
|
||||
```
|
||||
|
||||
### OG Image
|
||||
|
||||
To use the image as an open graph image, use the [Head](components.md#Head) component to add a meta tag.
|
||||
Note that the meta tag should point to a full URL, including schema and domain name.
|
||||
|
||||
```mdx
|
||||
import { Head } from 'mdx-deck'
|
||||
|
||||
<Head>
|
||||
<meta name='og:image' content='https://example.com/card.png' />
|
||||
</Head>
|
||||
```
|
||||
|
@ -1,8 +1,17 @@
|
||||
export { future as theme } from '../themes'
|
||||
import { Image, Notes, Appear } from '../dist'
|
||||
import { Head, Image, Notes, Appear } from '../dist'
|
||||
import { Invert, Split, SplitRight, FullScreenCode } from '../layouts'
|
||||
import Counter from './Counter'
|
||||
|
||||
<Head>
|
||||
<title>mdx-deck</title>
|
||||
<meta name='twitter:card' content='summary_large_image' />
|
||||
<meta name='twitter:site' content='@jxnblk' />
|
||||
<meta name='twitter:title' content='mdx-deck' />
|
||||
<meta name='twitter:description' content='MDX-based presentation decks' />
|
||||
<meta name='twitter:image' content='https://jxnblk.com/mdx-deck/card.png' />
|
||||
</Head>
|
||||
|
||||
# mdx-deck
|
||||
|
||||
MDX-based presention decks
|
||||
|
33
lib/build.js
Normal file
33
lib/build.js
Normal file
@ -0,0 +1,33 @@
|
||||
const webpack = require('webpack')
|
||||
const createConfig = require('./config')
|
||||
const renderHTML = require('./html')
|
||||
const log = require('./log')
|
||||
|
||||
const build = async (opts = {}) => {
|
||||
log('rendering static html')
|
||||
const { body, head } = await renderHTML(opts)
|
||||
opts.head = head
|
||||
opts.body = body
|
||||
|
||||
log('bundling js')
|
||||
const config = createConfig(opts)
|
||||
|
||||
config.mode = 'production'
|
||||
config.output = {
|
||||
path: opts.outDir
|
||||
}
|
||||
|
||||
const compiler = webpack(config)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(stats)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = build
|
139
lib/config.js
Normal file
139
lib/config.js
Normal file
@ -0,0 +1,139 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const HTMLPlugin = require('mini-html-webpack-plugin')
|
||||
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
|
||||
const chalk = require('chalk')
|
||||
const remark = {
|
||||
emoji: require('remark-emoji'),
|
||||
unwrapImages: require('remark-unwrap-images')
|
||||
}
|
||||
|
||||
const babel = {
|
||||
presets: [
|
||||
'babel-preset-env',
|
||||
'babel-preset-stage-0',
|
||||
'babel-preset-react',
|
||||
].map(require.resolve)
|
||||
}
|
||||
|
||||
const rules = [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: babel
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: path.resolve(__dirname, '../node_modules'),
|
||||
include: [
|
||||
path.resolve(__dirname, '..'),
|
||||
],
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: babel
|
||||
},
|
||||
{
|
||||
test: /\.mdx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: babel
|
||||
},
|
||||
{
|
||||
loader: require.resolve('./loader.js'),
|
||||
options: {
|
||||
mdPlugins: [
|
||||
remark.emoji,
|
||||
remark.unwrapImages
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const template = ({
|
||||
head = '<title>mdx-deck</title>',
|
||||
body = '',
|
||||
js,
|
||||
publicPath
|
||||
}) => `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<style>*{box-sizing:border-box}body{font-family:system-ui,sans-serif;margin:0}</style>
|
||||
<meta name='generator' content='mdx-deck'>
|
||||
${head}
|
||||
</head>
|
||||
<body>
|
||||
<div id=root>${body}</div>
|
||||
${HTMLPlugin.generateJSReferences(js, publicPath)}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
const baseConfig = {
|
||||
stats: 'errors-only',
|
||||
mode: 'development',
|
||||
module: {
|
||||
rules
|
||||
},
|
||||
resolve: {
|
||||
modules: [
|
||||
path.relative(process.cwd(), path.join(__dirname, '../node_modules')),
|
||||
'node_modules'
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new ProgressBarPlugin({
|
||||
width: '24',
|
||||
complete: '█',
|
||||
incomplete: chalk.gray('░'),
|
||||
format: [
|
||||
chalk.magenta('[mdx-deck] :bar'),
|
||||
chalk.magenta(':percent'),
|
||||
chalk.gray(':elapseds :msg'),
|
||||
].join(' '),
|
||||
summary: false,
|
||||
customSummary: () => {},
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
const createConfig = (opts = {}) => {
|
||||
const config = Object.assign({}, baseConfig)
|
||||
config.context = opts.dirname
|
||||
|
||||
config.resolve.modules.push(
|
||||
opts.dirname,
|
||||
path.join(opts.dirname, 'node_modules')
|
||||
)
|
||||
|
||||
config.entry = [
|
||||
path.join(__dirname, '../dist/entry.js')
|
||||
]
|
||||
|
||||
const defs = Object.assign({}, opts.globals, {
|
||||
OPTIONS: JSON.stringify(opts),
|
||||
HOT_PORT: JSON.stringify(opts.hotPort)
|
||||
})
|
||||
|
||||
config.plugins.push(
|
||||
new webpack.DefinePlugin(defs),
|
||||
new HTMLPlugin({ template, context: opts })
|
||||
)
|
||||
|
||||
if (config.resolve.alias) {
|
||||
const hotAlias = config.resolve.alias['webpack-hot-client/client']
|
||||
if (!fs.existsSync(hotAlias)) {
|
||||
const hotPath = path.dirname(require.resolve('webpack-hot-client/client'))
|
||||
config.resolve.alias['webpack-hot-client/client'] = hotPath
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
module.exports = createConfig
|
45
lib/dev.js
Normal file
45
lib/dev.js
Normal file
@ -0,0 +1,45 @@
|
||||
const path = require('path')
|
||||
const Koa = require('koa')
|
||||
const getPort = require('get-port')
|
||||
const koaWebpack = require('koa-webpack')
|
||||
const koaStatic = require('koa-static')
|
||||
const createConfig = require('./config')
|
||||
|
||||
const devMiddleware = {
|
||||
publicPath: '/',
|
||||
clientLogLevel: 'error',
|
||||
stats: 'errors-only',
|
||||
logLevel: 'error',
|
||||
}
|
||||
|
||||
const start = async (opts = {}) => {
|
||||
const app = new Koa()
|
||||
opts.hotPort = await getPort()
|
||||
const hotClient = {
|
||||
port: opts.hotPort,
|
||||
logLevel: 'error'
|
||||
}
|
||||
opts.dirname = opts.dirname || path.dirname(opts.entry)
|
||||
const config = createConfig(opts)
|
||||
config.entry.push(
|
||||
path.join(__dirname, './overlay.js'),
|
||||
)
|
||||
|
||||
const middleware = await koaWebpack({
|
||||
config,
|
||||
devMiddleware,
|
||||
hotClient
|
||||
})
|
||||
const port = opts.port || await getPort()
|
||||
app.use(middleware)
|
||||
app.use(koaStatic(opts.dirname))
|
||||
|
||||
const server = app.listen(port)
|
||||
return new Promise((resolve) => {
|
||||
middleware.devMiddleware.waitUntilValid(() => {
|
||||
resolve({ server, app, middleware, port })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = start
|
59
lib/html.js
Normal file
59
lib/html.js
Normal file
@ -0,0 +1,59 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const React = require('react')
|
||||
const {
|
||||
renderToString,
|
||||
renderToStaticMarkup
|
||||
} = require('react-dom/server')
|
||||
const webpack = require('webpack')
|
||||
const rimraf = require('rimraf')
|
||||
const createConfig = require('./config')
|
||||
|
||||
const getApp = async opts => {
|
||||
opts.tempdir = path.join(opts.outDir, 'TEMP')
|
||||
|
||||
if (!fs.existsSync(opts.outDir)) fs.mkdirSync(opts.outDir)
|
||||
if (!fs.existsSync(opts.tempdir)) fs.mkdirSync(opts.tempdir)
|
||||
|
||||
const config = createConfig(opts)
|
||||
|
||||
config.output = {
|
||||
path: opts.tempdir,
|
||||
filename: '[name].js',
|
||||
libraryTarget: 'umd'
|
||||
}
|
||||
config.entry = {
|
||||
App: path.join(__dirname, '../dist/entry.js')
|
||||
}
|
||||
config.target = 'node'
|
||||
|
||||
const compiler = webpack(config)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
const App = require(
|
||||
path.resolve(opts.tempdir, './App.js')
|
||||
).default
|
||||
rimraf(opts.tempdir, err => {
|
||||
if (err) console.error(err)
|
||||
})
|
||||
resolve(App)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const renderHTML = async opts => {
|
||||
const App = await getApp(opts)
|
||||
const headTags = []
|
||||
const body = renderToString(
|
||||
React.createElement(App, { headTags })
|
||||
)
|
||||
const head = renderToStaticMarkup(headTags)
|
||||
return { body, head }
|
||||
}
|
||||
|
||||
module.exports = renderHTML
|
16
lib/log.js
Normal file
16
lib/log.js
Normal file
@ -0,0 +1,16 @@
|
||||
const chalk = require('chalk')
|
||||
|
||||
const log = (...args) => {
|
||||
console.log(
|
||||
chalk.magenta('[mdx-deck]'),
|
||||
...args
|
||||
)
|
||||
}
|
||||
log.error = (...args) => {
|
||||
console.log(
|
||||
chalk.red('[err]'),
|
||||
...args
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = log
|
81
lib/overlay.js
Normal file
81
lib/overlay.js
Normal file
@ -0,0 +1,81 @@
|
||||
const ansiHTML = require('ansi-html')
|
||||
const Entities = require('html-entities').AllHtmlEntities
|
||||
const entities = new Entities()
|
||||
|
||||
const colors = {
|
||||
reset: ['transparent', 'transparent'],
|
||||
black: '000000',
|
||||
red: 'FF0000',
|
||||
green: '00FF00',
|
||||
yellow: 'FFFF00',
|
||||
blue: '0000FF',
|
||||
magenta: 'FF00FF',
|
||||
cyan: '00FFFF',
|
||||
lightgrey: 'EEEEEE',
|
||||
darkgrey: '666666'
|
||||
};
|
||||
ansiHTML.setColors(colors)
|
||||
|
||||
let overlay
|
||||
|
||||
const style = (el, styles) => {
|
||||
for (const key in styles) {
|
||||
el.style[key] = styles[key]
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
const show = ({
|
||||
title = '',
|
||||
text = ''
|
||||
}) => {
|
||||
overlay = document.body.appendChild(
|
||||
document.createElement('pre')
|
||||
)
|
||||
style(overlay, {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: 'Menlo, monospace',
|
||||
fontSize: '12px',
|
||||
overflow: 'auto',
|
||||
lineHeight: 1.5,
|
||||
padding: '8px',
|
||||
margin: 0,
|
||||
color: 'magenta',
|
||||
backgroundColor: 'black'
|
||||
})
|
||||
const code = ansiHTML(entities.encode(text))
|
||||
overlay.innerHTML = `<span>${title}</span>
|
||||
<br />
|
||||
<br />${code}
|
||||
`
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
if (!overlay) return
|
||||
document.body.removeChild(overlay)
|
||||
}
|
||||
|
||||
const ws = new WebSocket('ws://localhost:' + HOT_PORT)
|
||||
|
||||
ws.addEventListener('message', msg => {
|
||||
const data = JSON.parse(msg.data)
|
||||
switch (data.type) {
|
||||
case 'errors':
|
||||
const [ text ] = data.data.errors
|
||||
console.error(data.data.errors)
|
||||
show({ title: 'failed to compile', text })
|
||||
break
|
||||
case 'ok':
|
||||
destroy()
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
show({ title: 'disconnected' })
|
||||
})
|
@ -6,7 +6,7 @@ module.exports = async (opts = {}) => {
|
||||
const page = await browser.newPage()
|
||||
const {
|
||||
outDir,
|
||||
outFile,
|
||||
outFile = 'presentation.pdf',
|
||||
port,
|
||||
width = 1280,
|
||||
height = 960,
|
||||
|
38
lib/screenshot.js
Normal file
38
lib/screenshot.js
Normal file
@ -0,0 +1,38 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const puppeteer = require('puppeteer')
|
||||
|
||||
module.exports = async (opts) => {
|
||||
const {
|
||||
width = 1280,
|
||||
height = 720,
|
||||
outFile = 'card.png'
|
||||
} = opts
|
||||
const file = path.join(opts.outDir, outFile)
|
||||
|
||||
if (!fs.existsSync(opts.outDir)) fs.mkdirSync(opts.outDir)
|
||||
|
||||
const browser = await puppeteer.launch()
|
||||
const page = await browser.newPage()
|
||||
|
||||
await page.setViewport({ width, height })
|
||||
|
||||
await page.goto('http://localhost:' + opts.port, {
|
||||
waitUntil: 'networkidle2'
|
||||
})
|
||||
|
||||
await page.screenshot({
|
||||
path: file,
|
||||
type: 'png',
|
||||
clip: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
})
|
||||
|
||||
await browser.close()
|
||||
|
||||
return outFile
|
||||
}
|
21
package.json
21
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mdx-deck",
|
||||
"version": "1.5.15",
|
||||
"description": "MDX-based slide deck presentations",
|
||||
"description": "MDX-based presentation decks",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"mdx-deck": "./cli.js"
|
||||
@ -13,6 +13,7 @@
|
||||
"start": "./cli.js docs/index.mdx -p 8989",
|
||||
"build": "./cli.js build docs/index.mdx -d site",
|
||||
"pdf": "./cli.js pdf docs/index.mdx -d site",
|
||||
"screenshot": "./cli.js screenshot docs/index.mdx -d site",
|
||||
"help": "./cli.js",
|
||||
"test": "jest"
|
||||
},
|
||||
@ -23,29 +24,43 @@
|
||||
"@compositor/webfont": "^1.0.39",
|
||||
"@mdx-js/mdx": "^0.15.0-1",
|
||||
"@mdx-js/tag": "^0.14.1",
|
||||
"ansi-html": "0.0.7",
|
||||
"babel-loader": "^7.1.5",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-0": "^6.24.1",
|
||||
"chalk": "^2.4.1",
|
||||
"clipboardy": "^1.2.3",
|
||||
"get-port": "^4.0.0",
|
||||
"gray-matter": "^4.0.1",
|
||||
"hhmmss": "^1.0.0",
|
||||
"html-entities": "^1.2.1",
|
||||
"koa": "^2.5.2",
|
||||
"koa-static": "^5.0.0",
|
||||
"koa-webpack": "^5.1.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"meow": "^5.0.0",
|
||||
"mini-html-webpack-plugin": "^0.2.3",
|
||||
"normalize-newline": "^3.0.0",
|
||||
"ok-cli": "^3.1.1",
|
||||
"pkg-conf": "^2.1.0",
|
||||
"progress-bar-webpack-plugin": "^1.11.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"puppeteer": "^1.6.1",
|
||||
"querystring": "^0.2.0",
|
||||
"react": "^16.4.1",
|
||||
"react": "^16.4.2",
|
||||
"react-dev-utils": "^5.0.1",
|
||||
"react-dom": "^16.4.2",
|
||||
"react-swipeable": "^4.3.0",
|
||||
"react-syntax-highlighter": "^8.0.1",
|
||||
"remark-emoji": "^2.0.1",
|
||||
"remark-unwrap-images": "0.0.2-0",
|
||||
"rimraf": "^2.6.2",
|
||||
"stringify-object": "^3.2.2",
|
||||
"styled-components": ">=3.0.0",
|
||||
"styled-system": "^3.0.2",
|
||||
"superbox": "^2.1.0",
|
||||
"webpack": "^4.16.5",
|
||||
"webpack-hot-client": "^4.1.1"
|
||||
},
|
||||
"peerDependencies": {},
|
||||
|
96
src/Head.js
Normal file
96
src/Head.js
Normal file
@ -0,0 +1,96 @@
|
||||
import React from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
const noop = () => {
|
||||
console.warn('Missing HeadProvider')
|
||||
}
|
||||
|
||||
export const Context = React.createContext({
|
||||
tags: [],
|
||||
push: noop
|
||||
})
|
||||
|
||||
export class HeadProvider extends React.Component {
|
||||
static defaultProps = {
|
||||
tags: []
|
||||
}
|
||||
|
||||
push = (elements) => {
|
||||
this.props.tags.push(...elements)
|
||||
}
|
||||
|
||||
render () {
|
||||
const context = {
|
||||
...this.props,
|
||||
push: this.push
|
||||
}
|
||||
|
||||
return (
|
||||
<Context.Provider value={context}>
|
||||
{this.props.children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class Head extends React.Component {
|
||||
state = {
|
||||
didMount: false
|
||||
}
|
||||
|
||||
rehydrate = () => {
|
||||
const children = React.Children.toArray(this.props.children)
|
||||
const nodes = [
|
||||
...document.head.querySelectorAll('[data-head]')
|
||||
]
|
||||
|
||||
nodes.forEach(node => {
|
||||
node.remove()
|
||||
})
|
||||
children.forEach(child => {
|
||||
if (child.type === 'title') {
|
||||
const title = document.head.querySelector('title')
|
||||
if (title) title.remove()
|
||||
}
|
||||
if (child.type === 'meta') {
|
||||
const { name } = child.props
|
||||
let meta
|
||||
if (name) meta = document.head.querySelector(`meta[name="${name}"]`)
|
||||
if (meta) meta.remove()
|
||||
}
|
||||
})
|
||||
|
||||
this.setState({
|
||||
didMount: true
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.rehydrate()
|
||||
}
|
||||
|
||||
render () {
|
||||
const children = React.Children.toArray(this.props.children)
|
||||
.map(child => React.cloneElement(child, {
|
||||
'data-head': true
|
||||
}))
|
||||
|
||||
const { didMount } = this.state
|
||||
|
||||
if (!didMount) {
|
||||
return (
|
||||
<Context.Consumer
|
||||
children={({ push }) => {
|
||||
push(children)
|
||||
return false
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
children,
|
||||
document.head
|
||||
)
|
||||
}
|
||||
}
|
13
src/entry.js
13
src/entry.js
@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import SlideDeck from './index'
|
||||
|
||||
const mod = require(DOC_FILENAME)
|
||||
const mod = require(FILENAME)
|
||||
const slides = mod.default
|
||||
const { theme, components, Provider } = mod
|
||||
|
||||
@ -10,6 +11,7 @@ export default class App extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<SlideDeck
|
||||
{...this.props}
|
||||
slides={slides}
|
||||
theme={theme}
|
||||
components={components}
|
||||
@ -18,3 +20,12 @@ export default class App extends React.Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
)
|
||||
}
|
||||
|
||||
if (module.hot) module.hot.accept()
|
||||
|
95
src/index.js
95
src/index.js
@ -6,6 +6,7 @@ import debounce from 'lodash.debounce'
|
||||
import querystring from 'querystring'
|
||||
import Swipeable from 'react-swipeable'
|
||||
import { Provider as ContextProvider } from './context'
|
||||
import { HeadProvider } from './Head'
|
||||
import DefaultProvider from './Provider'
|
||||
import Carousel from './Carousel'
|
||||
import Slide from './Slide'
|
||||
@ -19,6 +20,7 @@ import GoogleFonts from './GoogleFonts'
|
||||
import defaultTheme from './themes'
|
||||
import defaultComponents from './components'
|
||||
|
||||
export { Head } from './Head'
|
||||
export { default as Image } from './Image'
|
||||
export { default as Notes } from './Notes'
|
||||
export { default as Appear } from './Appear'
|
||||
@ -81,7 +83,8 @@ export class SlideDeck extends React.Component {
|
||||
Provider: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
ignoreKeyEvents: PropTypes.bool
|
||||
ignoreKeyEvents: PropTypes.bool,
|
||||
headTags: PropTypes.array.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@ -91,7 +94,8 @@ export class SlideDeck extends React.Component {
|
||||
Provider: DefaultProvider,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
ignoreKeyEvents: false
|
||||
ignoreKeyEvents: false,
|
||||
headTags: [],
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -222,7 +226,8 @@ export class SlideDeck extends React.Component {
|
||||
components: propsComponents,
|
||||
Provider: PropsProvider,
|
||||
width,
|
||||
height
|
||||
height,
|
||||
headTags
|
||||
} = this.props
|
||||
const { index, length, mode, step} = this.state
|
||||
|
||||
@ -247,48 +252,50 @@ export class SlideDeck extends React.Component {
|
||||
|
||||
return (
|
||||
<ContextProvider value={context}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<MDXProvider
|
||||
components={{
|
||||
...defaultComponents,
|
||||
...components
|
||||
}}>
|
||||
<Provider {...this.state} update={this.update}>
|
||||
{mode === modes.grid ? (
|
||||
<Grid
|
||||
slides={slides}
|
||||
update={this.update}
|
||||
/>
|
||||
) : (
|
||||
<Swipeable
|
||||
onSwipedLeft={() => this.update(inc)}
|
||||
onSwipedRight={() => this.update(dec)}
|
||||
trackMouse>
|
||||
<Wrapper
|
||||
{...this.state}
|
||||
<HeadProvider tags={headTags}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<MDXProvider
|
||||
components={{
|
||||
...defaultComponents,
|
||||
...components
|
||||
}}>
|
||||
<Provider {...this.state} update={this.update}>
|
||||
{mode === modes.grid ? (
|
||||
<Grid
|
||||
slides={slides}
|
||||
width={width}
|
||||
height={height}
|
||||
update={this.update}>
|
||||
<GoogleFonts />
|
||||
<Carousel index={index}>
|
||||
{slides.map((Component, i) => (
|
||||
<Slide
|
||||
key={i}
|
||||
id={'slide-' + i}
|
||||
index={i}
|
||||
className='Slide'
|
||||
>
|
||||
<Component />
|
||||
</Slide>
|
||||
))}
|
||||
</Carousel>
|
||||
</Wrapper>
|
||||
</Swipeable>
|
||||
)}
|
||||
</Provider>
|
||||
</MDXProvider>
|
||||
</ThemeProvider>
|
||||
update={this.update}
|
||||
/>
|
||||
) : (
|
||||
<Swipeable
|
||||
onSwipedLeft={() => this.update(inc)}
|
||||
onSwipedRight={() => this.update(dec)}
|
||||
trackMouse>
|
||||
<Wrapper
|
||||
{...this.state}
|
||||
slides={slides}
|
||||
width={width}
|
||||
height={height}
|
||||
update={this.update}>
|
||||
<GoogleFonts />
|
||||
<Carousel index={index}>
|
||||
{slides.map((Component, i) => (
|
||||
<Slide
|
||||
key={i}
|
||||
id={'slide-' + i}
|
||||
index={i}
|
||||
className='Slide'
|
||||
>
|
||||
<Component />
|
||||
</Slide>
|
||||
))}
|
||||
</Carousel>
|
||||
</Wrapper>
|
||||
</Swipeable>
|
||||
)}
|
||||
</Provider>
|
||||
</MDXProvider>
|
||||
</ThemeProvider>
|
||||
</HeadProvider>
|
||||
</ContextProvider>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user