Skip to content

Commit

Permalink
Merge pull request #30 from marp-team/marp-cli-integration
Browse files Browse the repository at this point in the history
Add command to export PDF, HTML, and images via Marp CLI integration
  • Loading branch information
yhatt authored May 11, 2019
2 parents 8f27368 + c75d1f5 commit 37f1a73
Show file tree
Hide file tree
Showing 16 changed files with 1,282 additions and 121 deletions.
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v10.2.0
v10.15.3
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Add command to export PDF, HTML, and images via Marp CLI integration (`markdown.marp.export`) ([#4](https://github.com/marp-team/marp-vscode/issues/4), [#30](https://github.com/marp-team/marp-vscode/pull/30))

### Changed

- Upgrade Marp Core to [v0.9.0](https://github.com/marp-team/marp-core/releases/v0.9.0) ([#29](https://github.com/marp-team/marp-vscode/pull/29))
Expand Down
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
[![Visual Studio Marketplace](https://img.shields.io/visual-studio-marketplace/v/marp-team.marp-vscode.svg?style=flat-square&logo=visual-studio-code&label=VS%20Marketplace)](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode)
[![LICENSE](https://img.shields.io/github/license/marp-team/marp-vscode.svg?style=flat-square)](./LICENSE)

> ℹ️ Marp extension requires VS Code >= 1.31 ([January 2019 release](https://code.visualstudio.com/updates/v1_31)) to install.
**Preview [Marp] Markdown slide deck in VS Code.**
**Create slide deck written in [Marp] Markdown on VS Code.**

We will enhance your VS Code as the slide deck writer. Mark `marp: true`, and write your deck!

Expand Down Expand Up @@ -39,17 +37,33 @@ Start writing!

### Preview Marp Markdown

Marp for VS Code can preview your Marp Markdown with a same way as native Markdown preview.
Marp for VS Code can preview your Marp Markdown with the same way as [a native Markdown preview](https://code.visualstudio.com/docs/languages/markdown#_markdown-preview).

### Export slide deck to PDF, HTML, and image

We have integrated [Marp CLI][marp-cli] to export your deck into several formats.

To export the content of active Markdown editor, open the Command Palette (<kbd>F1</kbd> or <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd>) and select **"Marp: Export slide deck..."**. (`marp.markdown.export`)

[marp-cli]: https://github.com/marp-team/marp-cli/

#### Supported file types

- **PDF**: for publishing your deck
- **HTML**: for playing your deck on the browser
- **PNG**, **JPEG** (_First slide only)_: for creating an image of title slide

> ⚠️ Export to PDF and image formats requires to install [Google Chrome](https://www.google.com/chrome/) (or [Chromium](https://www.chromium.org/)).
### Outline view for each slide

We extend a native outline view to support slide pages in Marp Markdown.
We extend [a native outline view](https://code.visualstudio.com/docs/languages/markdown#_outline-view) to support slide pages in Marp Markdown.

<p align="center">
<img src="https://raw.githubusercontent.com/marp-team/marp-vscode/master/images/outline.png" alt="Outline view for each slide" width="480" />
</p>

> :information_source: Please choose `Sort By: Position` from context menu of its panel if you see incorrect slide order,
> ℹ️ Please choose `Sort By: Position` from context menu of its panel if you see incorrect slide order.
### Slide folding in editor

Expand Down
22 changes: 18 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Marp for VS Code",
"publisher": "marp-team",
"version": "0.2.1",
"description": "Preview Marp Markdown slide deck in VS Code",
"description": "Create slide deck written in Marp Markdown on VS Code",
"categories": [
"Other"
],
Expand Down Expand Up @@ -38,8 +38,17 @@
"color": "#d9edf8",
"theme": "light"
},
"activationEvents": [],
"activationEvents": [
"onCommand:markdown.marp.export"
],
"contributes": {
"commands": [
{
"category": "Marp",
"command": "markdown.marp.export",
"title": "Export slide deck..."
}
],
"configuration": {
"type": "object",
"title": "Marp for VS Code",
Expand Down Expand Up @@ -97,6 +106,7 @@
}
},
"jest": {
"clearMocks": true,
"collectCoverageFrom": [
"src/**/*.ts",
"src/**/*.js"
Expand All @@ -110,7 +120,9 @@
"lines": 95
}
},
"preset": "ts-jest"
"preset": "ts-jest",
"restoreMocks": true,
"testEnvironment": "node"
},
"scripts": {
"build": "yarn -s clean && rollup -c ./rollup.config.js",
Expand Down Expand Up @@ -163,7 +175,9 @@
"braces": "^3.0.2"
},
"dependencies": {
"@marp-team/marp-cli": "^0.9.2",
"@marp-team/marp-core": "^0.9.0",
"@marp-team/marpit-svg-polyfill": "^0.3.0"
"@marp-team/marpit-svg-polyfill": "^0.3.0",
"nanoid": "^2.0.1"
}
}
9 changes: 8 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ const sourcemap = !!process.env.ROLLUP_WATCH

export default [
{
external: [...Object.keys(pkg.dependencies), 'vscode'],
external: [
...Object.keys(pkg.dependencies),
'fs',
'os',
'path',
'util',
'vscode',
],
input: `src/${path.basename(pkg.main, '.js')}.ts`,
output: { exports: 'named', file: pkg.main, format: 'cjs', sourcemap },
plugins,
Expand Down
6 changes: 6 additions & 0 deletions src/__mocks__/option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = new Proxy(require.requireActual('../option'), {
get: (target, prop) => {
target.clearMarpCoreOptionCache() // Disable option cache while running test
return target[prop]
},
})
49 changes: 48 additions & 1 deletion src/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,55 @@
/* tslint:disable: variable-name */
type MockedConf = Record<string, any>

const defaultConf: MockedConf = {
'markdown.marp.breaks': 'on',
'markdown.marp.enableHtml': false,
'window.zoomLevel': 0,
}

let currentConf: MockedConf = {}

export const ProgressLocation = {
Notification: 'notification',
}

export const Uri = {
file: (path: string) => ({ fsPath: path }),
}

export const commands = {
executeCommand: jest.fn(),
registerCommand: jest.fn(),
}

export const env = {
openExternal: jest.fn(),
}

export const window = {
activeTextEditor: undefined,
showErrorMessage: jest.fn(),
showSaveDialog: jest.fn(),
showWarningMessage: jest.fn(),
withProgress: jest.fn(),
}

export const workspace = {
getConfiguration: jest.fn(() => new Map()),
getConfiguration: jest.fn((section?: string) => ({
get: jest.fn(
(subSection?: string) =>
currentConf[[section, subSection].filter(s => s).join('.')]
),
})),
getWorkspaceFolder: jest.fn(),
onDidChangeConfiguration: jest.fn(),

_setConfiguration: (conf: MockedConf = {}) => {
currentConf = { ...defaultConf, ...conf }
},
}

beforeEach(() => {
window.activeTextEditor = undefined
workspace._setConfiguration()
})
102 changes: 102 additions & 0 deletions src/commands/export.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { env, window } from 'vscode'
import * as exportModule from './export'
import * as marpCli from '../marp-cli'

const exportCommand = exportModule.default

jest.mock('vscode')

describe('Export command', () => {
let saveDialog: jest.Mock

beforeEach(() => {
saveDialog = jest.spyOn(exportModule, 'saveDialog').mockImplementation()
})

it('has no ops when active text editor is undefined', async () => {
window.activeTextEditor = undefined

await exportCommand()
expect(saveDialog).not.toBeCalled()
})

it('opens save dialog when active text editor is Markdown', async () => {
window.activeTextEditor = { document: { languageId: 'markdown' } } as any

await exportCommand()
expect(saveDialog).toBeCalledWith(window.activeTextEditor!.document)
})

describe('when active text editor is not Markdown', () => {
beforeEach(() => {
window.activeTextEditor = { document: { languageId: 'plaintext' } } as any
})

it('shows warning notification', async () => {
await exportCommand()
expect(saveDialog).not.toBeCalled()
expect(window.showWarningMessage).toBeCalled()
})

it('continues exporting when reacted on the notification to continue', async () => {
const { showWarningMessage }: any = window
showWarningMessage.mockResolvedValue(exportModule.ITEM_CONTINUE_TO_EXPORT)

await exportCommand()
expect(saveDialog).toBeCalledWith(window.activeTextEditor!.document)
})
})
})

describe('#saveDialog', () => {
const document: any = { uri: { fsPath: '/tmp/test.md' } }

it('opens save dialog with default URI', async () => {
await exportModule.saveDialog(document)

expect(window.showSaveDialog).toBeCalledWith(
expect.objectContaining({
defaultUri: expect.objectContaining({ fsPath: '/tmp/test' }),
})
)
})

it('runs exporting with notification when file path is specified', async () => {
const saveURI: any = { path: 'PATH', fsPath: '/tmp/saveTo.pdf' }
jest.spyOn(window, 'showSaveDialog').mockImplementation(() => saveURI)

const doExportMock: jest.Mock = jest
.spyOn(exportModule, 'doExport')
.mockImplementation()

await exportModule.saveDialog(document)
expect(window.withProgress).toBeCalledWith(
expect.objectContaining({ title: expect.stringContaining('PATH') }),
expect.any(Function)
)
;(window.withProgress as any).mock.calls[0][1]()
expect(doExportMock).toBeCalledWith(saveURI, document)
})
})

describe('#doExport', () => {
const saveURI: any = { fsPath: '/tmp/to.pdf' }
const document: any = { uri: { scheme: 'file', fsPath: '/tmp/md.md' } }

it('exports passed document via Marp CLI and opens it', async () => {
const runMarpCLI = jest.spyOn(marpCli, 'default').mockImplementation()

await exportModule.doExport(saveURI, document)
expect(runMarpCLI).toBeCalled()
expect(env.openExternal).toBeCalledWith(saveURI)
})

it('shows warning when Marp CLI throws error', async () => {
jest.spyOn(marpCli, 'default').mockRejectedValue(new Error('ERROR'))

await exportModule.doExport(saveURI, document)
expect(window.showErrorMessage).toBeCalledWith(
expect.stringContaining('Error: ERROR')
)
})
})
76 changes: 76 additions & 0 deletions src/commands/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import path from 'path'
import { env, ProgressLocation, TextDocument, Uri, window } from 'vscode'
import marpCli, {
createConfigFile,
createWorkFile,
MarpCLIError,
} from '../marp-cli'

export const ITEM_CONTINUE_TO_EXPORT = 'Continue to export...'

export const doExport = async (uri: Uri, document: TextDocument) => {
const input = await createWorkFile(document)

try {
const conf = await createConfigFile(document)

try {
await marpCli('-c', conf.path, input.path, '-o', uri.fsPath)
env.openExternal(uri)
} finally {
conf.cleanup()
}
} catch (e) {
window.showErrorMessage(
`Failure to export. ${
e instanceof MarpCLIError ? e.message : e.toString()
}`
)
} finally {
input.cleanup()
}
}

export const saveDialog = async (document: TextDocument) => {
const { fsPath } = document.uri

const saveURI = await window.showSaveDialog({
defaultUri: Uri.file(fsPath.slice(0, -path.extname(fsPath).length)),
filters: {
'PDF slide deck': ['pdf'],
'HTML slide deck': ['html'],
'PNG image (first slide only)': ['png'],
'JPEG image (first slide only)': ['jpg', 'jpeg'],
},
saveLabel: 'Export',
})

if (saveURI) {
await window.withProgress(
{
location: ProgressLocation.Notification,
title: `Exporting Marp slide deck to ${saveURI.path}...`,
},
() => doExport(saveURI, document)
)
}
}

export default async function exportCommand() {
const activeEditor = window.activeTextEditor

if (activeEditor) {
if (activeEditor.document.languageId === 'markdown') {
await saveDialog(activeEditor.document)
} else {
const acted = await window.showWarningMessage(
'A current document is not Markdown document.',
ITEM_CONTINUE_TO_EXPORT
)

if (acted === ITEM_CONTINUE_TO_EXPORT) {
await saveDialog(activeEditor.document)
}
}
}
}
Loading

0 comments on commit 37f1a73

Please sign in to comment.