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

Support configuration file #9

Merged
merged 11 commits into from
Sep 5, 2018
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
### Added

- Add `--html` option ([#7](https://github.com/marp-team/marp-cli/pull/7))
- Render local resources in converting PDF by `--allow-local-files option` ([#10](https://github.com/marp-team/marp-cli/pull/10))
- Support configuration file (`.marprc` / `marp.config.js`) ([#9](https://github.com/marp-team/marp-cli/pull/9))
- Come back `--engine` option that can specify Marpit based module ([#9](https://github.com/marp-team/marp-cli/pull/9))
- Render local resources in converting PDF by `--allow-local-files` option ([#10](https://github.com/marp-team/marp-cli/pull/10))

### Changed

- Upgrade dependent package versions to latest ([#8](https://github.com/marp-team/marp-cli/pull/8))
- Create directories for the output path recursively ([#9](https://github.com/marp-team/marp-cli/pull/9))

## v0.0.5 - 2018-08-29

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@types/cosmiconfig": "^5.0.3",
"@types/get-stdin": "^5.0.1",
"@types/jest": "^23.3.1",
"@types/node": "^10.9.4",
Expand Down Expand Up @@ -87,10 +88,14 @@
"@marp-team/marpit": "^0.0.14",
"chalk": "^2.4.1",
"chrome-launcher": "^0.10.2",
"cosmiconfig": "^5.0.6",
"get-stdin": "^6.0.0",
"globby": "^8.0.1",
"import-from": "^2.1.0",
"is-wsl": "^1.1.0",
"mkdirp": "^0.5.1",
"os-locale": "^3.0.1",
"pkg-up": "^2.0.0",
"puppeteer-core": "^1.7.0",
"tmp": "^0.0.33",
"yargs": "^12.0.1"
Expand Down
7 changes: 7 additions & 0 deletions src/__mocks__/engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const engine = require.requireActual('../engine')

export const { ResolvedEngine } = engine

ResolvedEngine.prototype.findClassPath = jest.fn()

export default ResolvedEngine.resolve
113 changes: 113 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Marp } from '@marp-team/marp-core'
import cosmiconfig from 'cosmiconfig'
import osLocale from 'os-locale'
import { ConverterOption, ConvertType } from './converter'
import resolveEngine, { ResolvableEngine } from './engine'
import { CLIError } from './error'

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
type Overwrite<T, U> = Omit<T, Extract<keyof T, keyof U>> & U

export interface IMarpCLIArguments {
allowLocalFiles?: boolean
configFile?: string
engine?: string
html?: boolean
output?: string
pdf?: boolean
template?: string
theme?: string
}

export type IMarpCLIConfig = Overwrite<
Omit<IMarpCLIArguments, 'configFile'>,
{
engine?: ResolvableEngine
html?: ConverterOption['html']
lang?: string
options?: ConverterOption['options']
}
>

export class MarpCLIConfig {
args: IMarpCLIArguments = {}
conf: IMarpCLIConfig = {}
confPath?: string

static moduleName = 'marp'

static async fromArguments(args: IMarpCLIArguments) {
const conf = new MarpCLIConfig()
conf.args = args

await conf.loadConf(args.configFile)
return conf
}

async converterOption(): Promise<ConverterOption> {
const engine = await (async () => {
if (this.args.engine) return resolveEngine(this.args.engine)
if (this.conf.engine)
return resolveEngine(this.conf.engine, this.confPath)

return resolveEngine(Marp)
})()

const output = this.args.output || this.conf.output

return {
output,
allowLocalFiles:
this.pickDefined(
this.args.allowLocalFiles,
this.conf.allowLocalFiles
) || false,
engine: engine.klass,
html: this.pickDefined(this.args.html, this.conf.html),
lang: this.conf.lang || (await osLocale()).replace(/[_@]/g, '-'),
options: this.conf.options || {},
readyScript: engine.browserScript
? `<script defer>${engine.browserScript}</script>`
: undefined,
template: this.args.template || this.conf.template || 'bespoke',
theme: this.args.theme || this.conf.theme,
type:
this.args.pdf ||
this.conf.pdf ||
`${output}`.toLowerCase().endsWith('.pdf')
? ConvertType.pdf
: ConvertType.html,
}
}

private pickDefined<T>(...args: T[]): T | undefined {
return args.find(v => v !== undefined)
}

private async loadConf(confPath?: string) {
const explorer = cosmiconfig(MarpCLIConfig.moduleName)

try {
const ret = await (confPath === undefined
? explorer.search()
: explorer.load(confPath))

if (ret) {
this.confPath = ret.filepath
this.conf = ret.config
}
} catch (e) {
throw new CLIError(
[
'Could not find or parse configuration file.',
e.name !== 'Error' && `(${e.name})`,
confPath !== undefined && `[${confPath}]`,
]
.filter(m => m)
.join(' ')
)
}
}
}

export default MarpCLIConfig.fromArguments
12 changes: 7 additions & 5 deletions src/converter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Marpit, MarpitOptions } from '@marp-team/marpit'
import { Marp, MarpOptions } from '@marp-team/marp-core'
import { MarpitOptions } from '@marp-team/marpit'
import * as chromeFinder from 'chrome-launcher/dist/chrome-finder'
import puppeteer, { PDFOptions } from 'puppeteer-core'
import { warn } from './cli'
import { Engine } from './engine'
import { error } from './error'
import { File, FileType } from './file'
import templates, { TemplateResult } from './templates/'
Expand All @@ -13,8 +15,8 @@ export enum ConvertType {

export interface ConverterOption {
allowLocalFiles: boolean
engine: typeof Marpit
html?: boolean
engine: Engine
html?: MarpOptions['html']
lang: string
options: MarpitOptions
output?: string
Expand Down Expand Up @@ -128,15 +130,15 @@ export class Converter {
const opts: any = { ...options, ...mergeOptions }

// for marp-core
if (html !== undefined) opts.html = !!html
if (html !== undefined) opts.html = html

const engine = new this.options.engine(opts)

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

// for Marpit engine
engine.markdown.set({ html: !!html })
if (!(engine instanceof Marp)) engine.markdown.set({ html: !!html })

return engine
}
Expand Down
83 changes: 83 additions & 0 deletions src/engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Marpit } from '@marp-team/marpit'
import fs from 'fs'
import path from 'path'
import pkgUp from 'pkg-up'
import importFrom from 'import-from'
import { CLIError } from './error'

export type Engine = typeof Marpit
export type ResolvableEngine = Engine | string

export class ResolvedEngine {
browserScript?: string
klass: Engine

private static browserScriptKey = 'marpBrowser'

static async resolve(
engine: ResolvableEngine,
from?: string
): Promise<ResolvedEngine> {
const resolvedEngine = new ResolvedEngine(
typeof engine === 'string'
? ResolvedEngine.resolveModule(engine, from)
: engine
)

await resolvedEngine.resolveBrowserScript()
return resolvedEngine
}

private static resolveModule(moduleId: string, from?: string) {
try {
const resolved =
(from &&
importFrom.silent(path.dirname(path.resolve(from)), moduleId)) ||
importFrom(process.cwd(), moduleId)

return resolved && resolved.__esModule ? resolved.default : resolved
} catch (e) {
throw new CLIError(
`The specified engine "${moduleId}" has not resolved. (${e.message})`
)
}
}

private constructor(klass: Engine) {
this.klass = klass
}

private async resolveBrowserScript(): Promise<void> {
const classPath = this.findClassPath(this.klass)
if (!classPath) return

const pkgPath = await pkgUp(path.dirname(classPath))
if (!pkgPath) return

const scriptPath = require(pkgPath)[ResolvedEngine.browserScriptKey]
if (!scriptPath) return undefined

this.browserScript = await new Promise<string>((res, rej) =>
fs.readFile(
path.resolve(path.dirname(pkgPath), scriptPath),
(e, buf) => (e ? rej(e) : res(buf.toString()))
)
)
}

// NOTE: It cannot test because of overriding `require` in Jest context.
private findClassPath(klass) {
for (const moduleId in require.cache) {
const expt = require.cache[moduleId].exports

if (
expt === klass ||
(expt && expt.__esModule && Object.values(expt).includes(klass))
)
return moduleId
}
return undefined
}
}

export default ResolvedEngine.resolve
11 changes: 9 additions & 2 deletions src/file.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs'
import getStdin from 'get-stdin'
import globby from 'globby'
import mkdirp from 'mkdirp'
import path from 'path'
import { tmpName } from 'tmp'

Expand Down Expand Up @@ -91,9 +92,15 @@ export class File {
)
}

private async saveToFile(path: string = this.path) {
private async saveToFile(savePath: string = this.path) {
await new Promise<void>((resolve, reject) =>
mkdirp(
path.dirname(path.resolve(savePath)),
e => (e ? reject(e) : resolve())
)
)
return new Promise<void>((resolve, reject) =>
fs.writeFile(path, this.buffer, e => (e ? reject(e) : resolve()))
fs.writeFile(savePath, this.buffer, e => (e ? reject(e) : resolve()))
)
}

Expand Down
43 changes: 18 additions & 25 deletions src/marp-cli.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Marp } from '@marp-team/marp-core'
import { version as coreVersion } from '@marp-team/marp-core/package.json'
import osLocale from 'os-locale'
import { Argv } from 'yargs'
import yargs from 'yargs/yargs'
import * as cli from './cli'
import { Converter, ConvertType } from './converter'
import fromArguments, { IMarpCLIArguments } from './config'
import { Converter } from './converter'
import { CLIError, error } from './error'
import { File, FileType } from './file'
import { MarpReadyScript } from './ready'
import templates from './templates'
import { name, version } from '../package.json'

Expand Down Expand Up @@ -44,30 +42,38 @@ export default async function(argv: string[] = []): Promise<number> {
},
output: {
alias: 'o',
describe: 'Output file name',
describe: 'Output file path',
group: OptionGroup.Basic,
type: 'string',
},
'config-file': {
alias: 'c',
describe: 'Specify path to configuration file',
group: OptionGroup.Basic,
type: 'string',
},
pdf: {
default: false,
describe: 'Convert slide deck into PDF',
group: OptionGroup.Converter,
type: 'boolean',
},
template: {
default: 'bespoke',
describe: 'Template name',
describe: 'Select template',
group: OptionGroup.Converter,
choices: Object.keys(templates),
type: 'string',
},
'allow-local-files': {
default: false,
describe:
'Allow to access local files from Markdown while converting PDF (INSECURE)',
'Allow to access local files from Markdown while converting PDF (NOT SECURE)',
group: OptionGroup.Converter,
type: 'boolean',
},
engine: {
describe: 'Select Marpit based engine by module name or path',
group: OptionGroup.Marp,
type: 'string',
},
html: {
describe: 'Enable or disable HTML tag',
group: OptionGroup.Marp,
Expand All @@ -88,21 +94,8 @@ export default async function(argv: string[] = []): Promise<number> {
}

// Initialize converter
const converter = new Converter({
allowLocalFiles: args.allowLocalFiles,
engine: Marp,
html: args.html,
lang: (await osLocale()).replace(/[_@]/g, '-'),
options: {},
output: args.output,
readyScript: await MarpReadyScript.bundled(),
template: args.template,
theme: args.theme,
type:
args.pdf || `${args.output}`.toLowerCase().endsWith('.pdf')
? ConvertType.pdf
: ConvertType.html,
})
const config = await fromArguments(<IMarpCLIArguments>args)
const converter = new Converter(await config.converterOption())

// Find target markdown files
const files = <File[]>(
Expand Down
Loading