Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inject browser script to rendered Markdown automatically #115

Merged
merged 13 commits into from
Oct 19, 2019
1 change: 1 addition & 0 deletions browser.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types/src/browser'
4 changes: 1 addition & 3 deletions browser.js
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import browser from './src/browser'

browser()
module.exports = require('./lib/browser.cjs')
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
"url": "https://github.com/marp-team/marp-core"
},
"main": "lib/marp.js",
"marpBrowser": "lib/browser.js",
"types": "types/marp.d.ts",
"types": "types/src/marp.d.ts",
"files": [
"lib/",
"types/"
"types/",
"browser.js",
"browser.d.ts"
],
"engines": {
"node": ">=8"
Expand Down Expand Up @@ -71,10 +72,12 @@
"prettier": "^1.18.2",
"rimraf": "^3.0.0",
"rollup": "^1.25.0",
"rollup-plugin-alias": "^2.1.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-postcss": "^2.0.3",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^5.1.2",
"rollup-plugin-typescript": "^1.0.1",
"sass": "^1.23.0",
Expand Down
25 changes: 18 additions & 7 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import autoprefixer from 'autoprefixer'
import path from 'path'
import postcssUrl from 'postcss-url'
import alias from 'rollup-plugin-alias'
import commonjs from 'rollup-plugin-commonjs'
import json from 'rollup-plugin-json'
import nodeResolve from 'rollup-plugin-node-resolve'
import postcss from 'rollup-plugin-postcss'
import { string } from 'rollup-plugin-string'
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript'
import pkg from './package.json'
Expand All @@ -14,6 +16,15 @@ const plugins = [
json({ preferConst: true }),
nodeResolve({ mainFields: ['module', 'jsnext:main', 'main'] }),
commonjs(),
string({ include: ['lib/*.js'] }),
alias({
entries: [
{
find: /^.+browser-script$/,
replacement: path.resolve(__dirname, './lib/browser.js'),
},
],
}),
typescript({ resolveJsonModule: false }),
postcss({
inject: false,
Expand Down Expand Up @@ -43,19 +54,19 @@ const plugins = [

export default [
{
external: Object.keys(pkg.dependencies),
input: `src/${path.basename(pkg.main, '.js')}.ts`,
output: { exports: 'named', file: pkg.main, format: 'cjs' },
input: 'scripts/browser.js',
output: { file: 'lib/browser.js', format: 'iife' },
plugins,
},
{
input: 'browser.js',
output: { file: pkg.marpBrowser, format: 'iife' },
input: 'src/browser.ts',
output: { exports: 'named', file: 'lib/browser.cjs.js', format: 'cjs' },
plugins,
},
{
input: 'src/browser.ts',
output: { file: 'lib/browser.cjs.js', format: 'cjs' },
external: Object.keys(pkg.dependencies),
input: `src/${path.basename(pkg.main, '.js')}.ts`,
output: { exports: 'named', file: pkg.main, format: 'cjs' },
plugins,
},
]
3 changes: 3 additions & 0 deletions scripts/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import browser from '../src/browser'

browser()
21 changes: 14 additions & 7 deletions src/browser.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { polyfills } from '@marp-team/marpit-svg-polyfill'
import fittingObserver from './fitting/observer'
import observer from './observer'

export default function browser(observe = true): void {
const observer = () => {
for (const polyfill of polyfills()) polyfill()
fittingObserver()
export { observer }

if (observe) window.requestAnimationFrame(observer)
export default function browser(): void {
if (typeof window === 'undefined') {
throw new Error(
"Marp Core's browser script is valid only in browser context."
)
}

if (window['marpCoreBrowserScript']) {
console.warn("Marp Core's browser script has already executed.")
return
}

Object.defineProperty(window, 'marpCoreBrowserScript', { value: true })
observer()
}
25 changes: 12 additions & 13 deletions src/marp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as emojiPlugin from './emoji/emoji'
import * as fittingPlugin from './fitting/fitting'
import * as htmlPlugin from './html/html'
import * as mathPlugin from './math/math'
import * as scriptPlugin from './script/script'
import * as sizePlugin from './size/size'
import defaultTheme from '../themes/default.scss'
import gaiaTheme from '../themes/gaia.scss'
Expand All @@ -28,13 +29,12 @@ export interface MarpOptions extends MarpitOptions {
markdown?: object
math?: mathPlugin.MathOptions
minifyCSS?: boolean
script?: boolean | scriptPlugin.ScriptOptions

/** @deprecated Dollar prefix for global directives is removed feature in Marpit framework, and Marp Core does not recommend too. Please use only for keeping compatibility in limited cases. */
dollarPrefixForGlobalDirectives?: boolean
}

const marpObservedSymbol = Symbol('marpObserved')

const styleMinifier = postcss([
postcssNormalizeWhitespace,
postcssMinifyParams,
Expand All @@ -56,9 +56,15 @@ export class Marp extends Marpit {
looseYAML: true,
math: true,
minifyCSS: true,
script: true,
html: Marp.html,
dollarPrefixForGlobalDirectives: false,
...opts,
emoji: {
shortcode: 'twemoji',
unicode: 'twemoji',
...(opts.emoji || {}),
},
markdown: [
'commonmark',
{
Expand All @@ -70,11 +76,6 @@ export class Marp extends Marpit {
html: opts.html !== undefined ? opts.html : Marp.html,
},
],
emoji: {
shortcode: 'twemoji',
unicode: 'twemoji',
...(opts.emoji || {}),
},
} as MarpOptions)

this.markdown.enable(['table', 'linkify'])
Expand All @@ -99,6 +100,7 @@ export class Marp extends Marpit {
.use(fittingPlugin.markdown)
.use(sizePlugin.markdown)
.use(dollarPlugin.markdown)
.use(scriptPlugin.markdown)
}

highlighter(code: string, lang: string): string {
Expand Down Expand Up @@ -143,13 +145,10 @@ export class Marp extends Marpit {
}

static ready() {
if (typeof window === 'undefined') {
throw new Error('Marp.ready() is only valid in browser context.')
}
if (window[marpObservedSymbol]) return

console.warn(
'[DEPRECATION WARNING] A script for the browser that is equivalent to Marp.ready() has injected into rendered Markdown by default. Marp.ready() will remove in future so you have to use "@marp-team/marp-core/browser" instead if you want to execute browser script in script-disabled HTML manually via using such as webpack.'
)
browser()
window[marpObservedSymbol] = true
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { polyfills } from '@marp-team/marpit-svg-polyfill'
import fittingObserver from './fitting/observer'

// Observer is divided for usage in Marp Web.
export default function observer(keep = true): void {
for (const polyfill of polyfills()) polyfill()
fittingObserver()

if (keep) window.requestAnimationFrame(() => observer(keep))
}
3 changes: 3 additions & 0 deletions src/script/browser-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const dummy = 'This is a placeholder for the content of built browser script.'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using a strange trick for bundling precompiled IIFE script as string by rollup, without any errors.
https://github.com/marp-team/marp-core/pull/115/files#diff-ff6e5f22a9c7e66987b19c0199636480R23


export default dummy
60 changes: 60 additions & 0 deletions src/script/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { name, version } from '../../package.json'
import browserScript from './browser-script'
import Marp from '../marp'

interface ScriptOptionsInternal {
nonce?: string
source: 'inline' | 'cdn'
}

export interface ScriptOptions extends Partial<ScriptOptionsInternal> {}

const defaultOptions: ScriptOptionsInternal = { source: 'inline' }

export function markdown(md): void {
const marp: Marp = md.marpit
const opts = (() => {
if (marp.options.script === false) return false
if (marp.options.script === true) return defaultOptions

return { ...defaultOptions, ...marp.options.script }
})()

md.core.ruler.before('marpit_collect', 'marp_core_script', state => {
if (opts === false) return

const lastSlideCloseIdxRev = [...state.tokens]
.reverse()
.findIndex(t => t.type === 'marpit_slide_close')

if (lastSlideCloseIdxRev < 0) return

// Inject script token to the last page
const { Token } = state
const scriptToken = new Token('marp_core_script', 'script', 0)

scriptToken.block = true
scriptToken.nesting = 0

if (opts.source === 'inline') {
scriptToken.content = browserScript
} else if (opts.source === 'cdn') {
scriptToken.attrSet(
'src',
`https://cdn.jsdelivr.net/npm/${name}@${version}/lib/browser.js`
)

// defer attribute would have no effect in inline script
scriptToken.attrSet('defer', '')
}

if (opts.nonce) scriptToken.attrSet('nonce', opts.nonce)

state.tokens.splice(-lastSlideCloseIdxRev - 1, 0, scriptToken)
})

md.renderer.rules.marp_core_script = (tokens, idx, _, __, self) => {
const token = tokens[idx]
return `<script${self.renderAttrs(token)}>${token.content || ''}</script>`
}
}
10 changes: 6 additions & 4 deletions test/browser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jest-environment jsdom */
import browser from '../src/browser'
import browser, { observer } from '../src/browser'
import fittingObserver from '../src/fitting/observer'

const polyfill = jest.fn()
Expand All @@ -13,7 +13,7 @@ jest.mock('../src/fitting/observer')
beforeEach(() => jest.clearAllMocks())
afterEach(() => jest.restoreAllMocks())

describe('Browser observers', () => {
describe('Browser script', () => {
it('executes observers for polyfill and fitting', () => {
const spy = jest.spyOn(window, 'requestAnimationFrame')

Expand All @@ -27,12 +27,14 @@ describe('Browser observers', () => {
expect(polyfill).toHaveBeenCalledTimes(2)
expect(fittingObserver).toHaveBeenCalledTimes(2)
})
})

context('with observe argument is false', () => {
describe('Observer', () => {
context('with passed false', () => {
it('does not call window.requestAnimationFrame', () => {
const spy = jest.spyOn(window, 'requestAnimationFrame')

browser(false)
observer(false)
expect(spy).not.toHaveBeenCalled()
})
})
Expand Down
Loading