Skip to content

Commit a013665

Browse files
authored
Merge pull request #386 from marp-team/expose-marp-to-functional-engine
Expose Marp Core instance to functional engine
2 parents c089f8a + e273d31 commit a013665

File tree

10 files changed

+195
-52
lines changed

10 files changed

+195
-52
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
- Experimental transitions for bespoke template ([#382](https://github.com/marp-team/marp-cli/issues/382), [#381](https://github.com/marp-team/marp-cli/pull/381))
8+
- Expose Marp Core instance to functional engine via `marp` getter ([#386](https://github.com/marp-team/marp-cli/pull/386)))
89

910
### Changed
1011

README.md

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -372,29 +372,44 @@ Notice that Marpit has not provided theme. It would be good to include inline st
372372

373373
### Functional engine
374374

375-
When you specify the path to JavaScript file in `--engine` option, you may use more customized engine by JS.
375+
When you specified the path to JavaScript file in `--engine` option, you may use more customized engine by a JavaScript function.
376376

377-
It would be useful to convert with a customized engine for supporting the additional syntax that is out of Marp Markdown specification.
377+
#### Spec
378+
379+
The functional engine should export a function with one parameter, which is a constructor option of Marpit. The function must return an instance of Marpit-based engine made by the passed parameter.
380+
381+
```javascript
382+
module.exports = function (constructorOption) {
383+
return new MarpitBasedEngine(constructorOption)
384+
}
385+
```
386+
387+
Marp CLI also exposes `marp` getter property to the parameter. It returns a prepared instance of the built-in Marp Core engine, so you can apply several customizations to Marp engine with simple declarations.
388+
389+
```javascript
390+
module.exports = ({ marp }) => marp.use(marpPlugin).use(andMorePlugin)
391+
```
392+
393+
It allows converting Markdown with additional syntaxes that were provided by Marp (or compatible markdown-it) plugins.
378394

379395
#### Example: [markdown-it-mark](https://github.com/markdown-it/markdown-it-mark)
380396

381397
```javascript
382398
// engine.js
383-
const { Marp } = require('@marp-team/marp-core')
384399
const markdownItMark = require('markdown-it-mark')
385400

386-
module.exports = (opts) => new Marp(opts).use(markdownItMark)
401+
module.exports = ({ marp }) => marp.use(markdownItMark)
387402
```
388403

389404
```bash
390-
# Install Marp Core and markdown-it-mark
391-
npm install @marp-team/marp-core markdown-it-mark --save-dev
405+
# Install markdown-it-mark
406+
npm install markdown-it-mark --save
392407

393408
# Specify the path to functional engine
394409
marp --engine ./engine.js slide-deck.md
395410
```
396411

397-
The customized engine would convert `==marked==` to `<mark>marked</mark>`.
412+
The customized engine will convert `==marked==` to `<mark>marked</mark>`.
398413

399414
### Confirm engine version
400415

@@ -434,12 +449,11 @@ pdf: true
434449
435450
```javascript
436451
// marp.config.js
437-
const { Marp } = require('@marp-team/marp-core')
438-
const container = require('markdown-it-container')
452+
const markdownItContainer = require('markdown-it-container')
439453

440454
module.exports = {
441455
// Customize engine on configuration file directly
442-
engine: (opts) => new Marp(opts).use(container, 'custom'),
456+
engine: ({ marp }) => marp.use(markdownItContainer, 'custom'),
443457
}
444458
```
445459

@@ -491,7 +505,9 @@ The advanced options that cannot specify through CLI options can be configured b
491505

492506
`options` can set the base options for the constructor of the used engine. You can fine-tune constructor options for [Marp Core](https://github.com/marp-team/marp-core#constructor-options) / [Marpit](https://marpit-api.marp.app/marpit).
493507

494-
For example, the below configuration will set constructor option for Marp Core as specified:
508+
##### Example
509+
510+
The below configuration will set constructor option for Marp Core as specified:
495511

496512
- Disables [Marp Core's line breaks conversion](https://github.com/marp-team/marp-core#marp-markdown) (`\n` to `<br />`) to match for CommonMark, by passing [markdown-it's `breaks` option](https://markdown-it.github.io/markdown-it/#MarkdownIt.new) as `false`.
497513
- Disable minification for rendered theme CSS to make debug your style easily, by passing [`minifyCSS`](https://github.com/marp-team/marp-core#minifycss-boolean) as `false`.

src/config.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import fs from 'fs'
33
import path from 'path'
4-
import { Marp } from '@marp-team/marp-core'
54
import chalk from 'chalk'
65
import { cosmiconfig } from 'cosmiconfig'
76
import { osLocale } from 'os-locale'
87
import { info, warn } from './cli'
98
import { ConverterOption, ConvertType } from './converter'
10-
import resolveEngine, { ResolvableEngine, ResolvedEngine } from './engine'
9+
import { ResolvableEngine, ResolvedEngine } from './engine'
1110
import { keywordsAsArray } from './engine/meta-plugin'
1211
import { error } from './error'
1312
import { TemplateOption } from './templates'
@@ -77,11 +76,11 @@ export class MarpCLIConfig {
7776
if (args.configFile !== false) await conf.loadConf(args.configFile)
7877

7978
conf.engine = await (() => {
80-
if (conf.args.engine) return resolveEngine(conf.args.engine)
79+
if (conf.args.engine) return ResolvedEngine.resolve(conf.args.engine)
8180
if (conf.conf.engine)
82-
return resolveEngine(conf.conf.engine, conf.confPath)
81+
return ResolvedEngine.resolve(conf.conf.engine, conf.confPath)
8382

84-
return resolveEngine(['@marp-team/marp-core', Marp])
83+
return ResolvedEngine.resolveDefaultEngine()
8584
})()
8685

8786
return conf

src/converter.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import { URL } from 'url'
3-
import { MarpOptions } from '@marp-team/marp-core'
3+
import type { MarpOptions } from '@marp-team/marp-core'
44
import { Marpit, Options as MarpitOptions } from '@marp-team/marpit'
55
import chalk from 'chalk'
66
import puppeteer from 'puppeteer-core'
77
import { silence, warn } from './cli'
8-
import { Engine } from './engine'
8+
import { Engine, ResolvedEngine } from './engine'
99
import infoPlugin, { engineInfo, EngineInfo } from './engine/info-plugin'
1010
import metaPlugin from './engine/meta-plugin'
1111
import { error } from './error'
@@ -149,8 +149,8 @@ export class Converter {
149149
isFile(file) && this.options.watch && type === ConvertType.html
150150
? await notifier.register(file.absolutePath)
151151
: undefined,
152-
renderer: (tplOpts) => {
153-
const engine = this.generateEngine(tplOpts)
152+
renderer: async (tplOpts) => {
153+
const engine = await this.generateEngine(tplOpts)
154154
tplOpts.modifier?.(engine)
155155

156156
const ret = engine.render(stripBOM(`${markdown}${additionals}`))
@@ -420,28 +420,41 @@ export class Converter {
420420
return ret
421421
}
422422

423-
private generateEngine(
423+
private async generateEngine(
424424
mergeOptions: MarpitOptions
425-
): Marpit & { [engineInfo]: EngineInfo | undefined } {
425+
): Promise<Marpit & { [engineInfo]?: EngineInfo }> {
426426
const { html, options } = this.options
427427
const { prototype } = this.options.engine
428428
const opts = { ...options, ...mergeOptions, html }
429429

430-
const engine =
430+
let engine: any
431+
432+
if (
431433
prototype &&
432434
Object.prototype.hasOwnProperty.call(prototype, 'constructor')
433-
? new this.options.engine(opts)
434-
: (<any>this.options.engine)(opts)
435+
) {
436+
engine = new this.options.engine(opts)
437+
} else {
438+
// Expose "marp" getter to allow accessing a bundled Marp Core instance
439+
const defaultEngine = await ResolvedEngine.resolveDefaultEngine()
440+
441+
Object.defineProperty(opts, 'marp', {
442+
get: () => new defaultEngine.klass(opts),
443+
})
444+
445+
engine = (this.options.engine as any)(opts)
446+
}
435447

436448
if (typeof engine.render !== 'function')
437449
error('Specified engine has not implemented render() method.')
438450

451+
// Enable HTML tags
439452
if (html !== undefined) engine.markdown.set({ html })
440453

441454
// Marpit plugins
442455
engine.use(metaPlugin).use(infoPlugin)
443456

444-
// Additional themes
457+
// Themes
445458
this.options.themeSet.registerTo(engine)
446459

447460
return engine

src/engine.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,72 @@ import pkgUp from 'pkg-up'
55
import { error } from './error'
66

77
export type Engine = typeof Marpit
8-
export type ResolvableEngine = Engine | string
8+
export type ResolvableEngine = Engine | DelayedEngineResolver | string
9+
10+
const delayedEngineResolverSymbol = Symbol('delayedEngineResolver')
11+
12+
type DelayedEngineResolver = {
13+
[delayedEngineResolverSymbol]: () => Promise<Engine>
14+
}
15+
16+
const delayedEngineResolver = (
17+
fn: () => Promise<Engine>
18+
): DelayedEngineResolver => ({ [delayedEngineResolverSymbol]: fn })
919

1020
export class ResolvedEngine {
1121
klass: Engine
1222
package?: Record<string, any>
1323

24+
private static _defaultEngine: ResolvedEngine | undefined
25+
1426
static async resolve(
1527
engine: ResolvableEngine | ResolvableEngine[],
1628
from?: string
1729
): Promise<ResolvedEngine> {
1830
const resolvedEngine = new ResolvedEngine(
19-
ResolvedEngine.resolveModule(engine, from)
31+
await ResolvedEngine.resolveModule(engine, from)
2032
)
2133

2234
await resolvedEngine.resolvePackage()
2335
return resolvedEngine
2436
}
2537

26-
private static resolveModule(
38+
static async resolveDefaultEngine(): Promise<ResolvedEngine> {
39+
if (
40+
ResolvedEngine._defaultEngine === undefined ||
41+
process.env.NODE_ENV === 'test'
42+
) {
43+
ResolvedEngine._defaultEngine = await ResolvedEngine.resolve([
44+
'@marp-team/marp-core',
45+
delayedEngineResolver(
46+
async () => (await import('@marp-team/marp-core')).Marp
47+
),
48+
])
49+
}
50+
return ResolvedEngine._defaultEngine
51+
}
52+
53+
private static async resolveModule(
2754
engine: ResolvableEngine | ResolvableEngine[],
2855
from?: string
2956
) {
3057
let resolved
31-
;(Array.isArray(engine) ? engine : [engine]).some((eng) => {
58+
59+
for (const eng of ([] as ResolvableEngine[]).concat(engine)) {
3260
if (typeof eng === 'string') {
3361
resolved =
3462
(from && importFrom.silent(path.dirname(path.resolve(from)), eng)) ||
3563
importFrom.silent(process.cwd(), eng)
3664

3765
if (resolved?.__esModule) resolved = resolved.default
66+
} else if (typeof eng === 'object' && eng[delayedEngineResolverSymbol]) {
67+
resolved = await eng[delayedEngineResolverSymbol]()
3868
} else {
3969
resolved = eng
4070
}
41-
return resolved
42-
})
71+
72+
if (resolved) break
73+
}
4374

4475
if (!resolved) error(`The specified engine has not resolved.`)
4576
return resolved
@@ -73,5 +104,3 @@ export class ResolvedEngine {
73104
return undefined
74105
}
75106
}
76-
77-
export default ResolvedEngine.resolve

src/templates/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ interface TemplateCoreOption {
2121
base?: string
2222
lang: string
2323
notifyWS?: string
24-
renderer: (tplOpts: TemplateRendererOptions) => RendererResult
24+
renderer: (
25+
tplOpts: TemplateRendererOptions
26+
) => RendererResult | Promise<RendererResult>
2527
}
2628

2729
export interface TemplateMeta {
@@ -60,7 +62,7 @@ export type Template<T = TemplateOption> = ((
6062
}
6163

6264
export const bare: Template<TemplateBareOption> = async (opts) => {
63-
const rendered = opts.renderer({
65+
const rendered = await opts.renderer({
6466
container: [],
6567
inlineSVG: true,
6668
slideContainer: [],
@@ -80,15 +82,21 @@ export const bare: Template<TemplateBareOption> = async (opts) => {
8082
Object.defineProperty(bare, 'printable', { value: true })
8183

8284
export const bespoke: Template<TemplateBespokeOption> = async (opts) => {
83-
const rendered = opts.renderer({
85+
const rendererOptions = {
8486
container: new Element('div', { id: 'p' }),
8587
inlineSVG: true,
8688
slideContainer: [],
87-
modifier: (marpit) => {
89+
}
90+
91+
// Hide template-specific modifier from options which have exposed to the functional engine
92+
Object.defineProperty(rendererOptions, 'modifier', {
93+
value: (marpit) => {
8894
if (opts.transition) marpit.use(transitionPlugin)
8995
},
9096
})
9197

98+
const rendered = await opts.renderer(rendererOptions)
99+
92100
return {
93101
rendered,
94102
result: bespokePug({

src/version.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
import { Marp } from '@marp-team/marp-core'
21
import { version as bundledCoreVer } from '@marp-team/marp-core/package.json'
32
import { name, version } from '../package.json'
43
import { MarpCLIConfig } from './config'
4+
import { ResolvedEngine } from './engine'
55

6-
export const isMarpCore = (klass: any): boolean => klass === Marp
6+
export const isMarpCore = async (engine: ResolvedEngine): Promise<boolean> =>
7+
engine.package?.name === '@marp-team/marp-core' ||
8+
engine === (await ResolvedEngine.resolveDefaultEngine())
79

810
export default async function outputVersion(config: MarpCLIConfig): Promise<0> {
911
let engineVer = ''
1012
const { engine } = config
1113

12-
if (isMarpCore(engine.klass)) {
14+
if (await isMarpCore(engine)) {
1315
engineVer = `@marp-team/marp-core v${bundledCoreVer}`
1416

1517
if (engine.package && engine.package.version !== bundledCoreVer) {
1618
engineVer = `user-installed @marp-team/marp-core v${engine.package.version}`
1719
}
18-
} else if (engine.package && engine.package.name && engine.package.version) {
20+
} else if (engine.package?.name && engine.package.version) {
1921
engineVer = `customized engine in ${engine.package.name} v${engine.package.version}`
2022
} else {
2123
engineVer = `customized engine`

0 commit comments

Comments
 (0)