Skip to content

Commit

Permalink
Merge pull request #63 from marp-team/toggle-marp-preview
Browse files Browse the repository at this point in the history
Add markdown.marp.toggleMarpPreview command
  • Loading branch information
yhatt authored Aug 28, 2019
2 parents 1ded5fc + 9ca9853 commit 033de90
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 16 deletions.
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

- `markdown.marp.toggleMarpPreview` command to toggle Marp preview ([#52](https://github.com/marp-team/marp-vscode/issues/52), [#63](https://github.com/marp-team/marp-vscode/pull/63))

## v0.8.2 - 2019-08-23

### Fixed
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ See the documentation of [Marpit Markdown](https://marpit.marp.app/markdown) and

## Usage

Marp preview will only be enabled when `marp: true` is written in front-matter.
Marp preview for Markdown document will be enabled when `marp: true` is written in front-matter.

```markdown
---
Expand All @@ -33,6 +33,12 @@ marp: true
Start writing!
```

It also can toggle by opening the quick picker from toolbar icon <img src="https://raw.githubusercontent.com/marp-team/marp-vscode/master/images/icon.png" width="16" height="16" /> and selecting **"Toggle Marp preview for current Markdown"**. (`markdown.marp.toggleMarpPreview`).

<p align="center">
<img src="https://raw.githubusercontent.com/marp-team/marp-vscode/master/images/toggle.gif" alt="Toggle Marp preview" width="600" />
</p>

## Features

### Preview Marp Markdown
Expand Down
Binary file added images/toggle.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
},
"activationEvents": [
"onCommand:markdown.marp.export",
"onCommand:markdown.marp.showQuickPick"
"onCommand:markdown.marp.showQuickPick",
"onCommand:markdown.marp.toggleMarpPreview"
],
"contributes": {
"commands": [
Expand All @@ -57,6 +58,11 @@
"dark": "./images/icon.png",
"light": "./images/icon.png"
}
},
{
"category": "Marp",
"command": "markdown.marp.toggleMarpPreview",
"title": "Toggle Marp preview for current Markdown"
}
],
"configuration": {
Expand Down
12 changes: 12 additions & 0 deletions src/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ const uriInstance = (path: string) =>
return uri
})()

export class Position {
constructor(readonly line: number, readonly character: number) {}
}

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

export class Range {
constructor(readonly start: Position, readonly end: Position) {}
}

export const RelativePattern = jest.fn()

export const Uri = {
Expand All @@ -40,6 +48,10 @@ export const env = {
openExternal: jest.fn(),
}

export const languages = {
setTextDocumentLanguage: jest.fn(),
}

export let version: string = defaultVSCodeVersion
export const _setVSCodeVersion = (value: string) => {
version = value
Expand Down
2 changes: 1 addition & 1 deletion src/commands/export.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path'
import { env, ProgressLocation, TextDocument, Uri, window } from 'vscode'
import { marpConfiguration } from './../option'
import { marpConfiguration } from '../utils'
import marpCli, {
createConfigFile,
createWorkFile,
Expand Down
127 changes: 127 additions & 0 deletions src/commands/toggle-marp-preview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Position, Range, languages, window } from 'vscode'
import * as toggleMarpPreview from './toggle-marp-preview'

const toggleMarpPreviewCommand = toggleMarpPreview.default

jest.mock('vscode')

describe('toggleMarpPreview command', () => {
let toggleFunc: jest.SpyInstance

beforeEach(() => {
toggleFunc = jest.spyOn(toggleMarpPreview, 'toggle').mockImplementation()
})

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

await toggleMarpPreviewCommand()
expect(toggleFunc).not.toBeCalled()
})

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

await toggleMarpPreviewCommand()
expect(toggleFunc).toBeCalledWith(window.activeTextEditor)
})

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

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

it('changes editor language and continues process when reacted on the notification', async () => {
const { showWarningMessage }: any = window
showWarningMessage.mockResolvedValue(
toggleMarpPreview.ITEM_CONTINUE_BY_CHANGING_LANGUAGE
)

await toggleMarpPreviewCommand()
expect(languages.setTextDocumentLanguage).toBeCalledWith(
window.activeTextEditor!.document,
'markdown'
)
expect(toggleFunc).toBeCalledWith(window.activeTextEditor)
})
})
})

describe('#toggle', () => {
const editBuilder = () => ({
replace: jest.fn(),
insert: jest.fn(),
delete: jest.fn(),
})

const textEditor = (text: string): any => {
const textEditorMock: any = {
_editBuilders: [],
document: { getText: () => text },
}

textEditorMock.edit = jest.fn(callback => {
const builder = editBuilder()
textEditorMock._editBuilders.push(builder)

return Promise.resolve(callback(builder))
})

return textEditorMock
}

it('inserts frontmatter to the top when frontmatter was not detected', async () => {
const editor = textEditor('')
await toggleMarpPreview.toggle(editor)

expect(editor._editBuilders[0].insert).toBeCalledWith(
new Position(0, 0),
'---\nmarp: true\n---\n\n'
)
})

it('inserts `marp: true` to the last line of frontmatter when frontmatter without marp key was detected', async () => {
const editor = textEditor('---\ntest: abc\nfoo: bar\n---')
await toggleMarpPreview.toggle(editor)

expect(editor._editBuilders[0].insert).toBeCalledWith(
new Position(3, 0),
'marp: true\n'
)

// Empty frontmatter
const editorEmptyFm = textEditor('---\n---')
await toggleMarpPreview.toggle(editorEmptyFm)

expect(editorEmptyFm._editBuilders[0].insert).toBeCalledWith(
new Position(1, 0),
'marp: true\n'
)
})

it('toggles the value of marp key in frontmatter when frontmatter with marp key was detected', async () => {
// true => false
const editorEnabled = textEditor('---\nmarp: true\n---')
await toggleMarpPreview.toggle(editorEnabled)

expect(editorEnabled._editBuilders[0].replace).toBeCalledWith(
new Range(new Position(1, 6), new Position(1, 10)),
'false'
)

// false => true
const editorDisabled = textEditor('---\nfoo: bar\nmarp: false\n---')
await toggleMarpPreview.toggle(editorDisabled)

expect(editorDisabled._editBuilders[0].replace).toBeCalledWith(
new Range(new Position(2, 8), new Position(2, 13)),
'true'
)
})
})
65 changes: 65 additions & 0 deletions src/commands/toggle-marp-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Position, Range, TextEditor, languages, window } from 'vscode'
import { detectFrontMatter, marpDirectiveRegex } from '../utils'

export const ITEM_CONTINUE_BY_CHANGING_LANGUAGE =
'Continue by changing language to Markdown'

export const toggle = async (editor: TextEditor) => {
const originalText = editor.document.getText()
const frontmatter = detectFrontMatter(originalText)

if (frontmatter !== undefined) {
let line = 1
let targetRange: Range | undefined
let toggleValue: string | undefined

for (const lText of frontmatter.split('\n')) {
const matched = marpDirectiveRegex.exec(lText)

if (matched) {
targetRange = new Range(
new Position(line, matched[1].length),
new Position(line, matched[1].length + matched[2].length)
)
toggleValue = matched[2] === 'true' ? 'false' : 'true'
}

line += 1
}

if (targetRange && toggleValue) {
await editor.edit(e => e.replace(targetRange!, toggleValue!))
} else {
await editor.edit(e =>
e.insert(new Position(line - 1, 0), 'marp: true\n')
)
}
} else {
await editor.edit(e =>
e.insert(new Position(0, 0), '---\nmarp: true\n---\n\n')
)
}
}

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

if (activeEditor) {
if (activeEditor.document.languageId === 'markdown') {
await toggle(activeEditor)
} else {
const acted = await window.showWarningMessage(
'A current document is not Markdown document. Do you want to continue by changing language?',
ITEM_CONTINUE_BY_CHANGING_LANGUAGE
)

if (acted === ITEM_CONTINUE_BY_CHANGING_LANGUAGE) {
await languages.setTextDocumentLanguage(
activeEditor.document,
'markdown'
)
await toggle(activeEditor)
}
}
}
}
15 changes: 7 additions & 8 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Marp } from '@marp-team/marp-core'
import { ExtensionContext, Uri, commands, workspace } from 'vscode'
import exportCommand from './commands/export' // tslint:disable-line: import-name
import showQuickPick from './commands/show-quick-pick'
import toggleMarpPreview from './commands/toggle-marp-preview'
import customTheme from './plugins/custom-theme'
import lineNumber from './plugins/line-number'
import outline from './plugins/outline'
import { marpCoreOptionForPreview, clearMarpCoreOptionCache } from './option'
import themes from './themes'
import { detectMarpFromMarkdown } from './utils'

const frontMatterRegex = /^-{3,}\s*([^]*?)^\s*-{3}/m
const marpDirectiveRegex = /^marp\s*:\s*true\s*$/m
const shouldRefreshConfs = [
'markdown.marp.breaks',
'markdown.marp.enableHtml',
Expand All @@ -18,11 +18,6 @@ const shouldRefreshConfs = [
'window.zoomLevel', // for WebKit polyfill
]

const detectMarpFromFrontMatter = (markdown: string): boolean => {
const m = markdown.match(frontMatterRegex)
return !!(m && m.index === 0 && marpDirectiveRegex.exec(m[0].trim()))
}

export const marpVscode = Symbol('marp-vscode')

export function extendMarkdownIt(md: any) {
Expand All @@ -31,7 +26,7 @@ export function extendMarkdownIt(md: any) {

md.parse = (markdown: string, env: any) => {
// Generate tokens by Marp if enabled
if (detectMarpFromFrontMatter(markdown)) {
if (detectMarpFromMarkdown(markdown)) {
const mdFolder = Uri.parse(md.normalizeLink('.')).with({ scheme: 'file' })
const workspaceFolder = workspace.getWorkspaceFolder(mdFolder)
const baseFolder = workspaceFolder ? workspaceFolder.uri : mdFolder
Expand Down Expand Up @@ -104,6 +99,10 @@ export const activate = ({ subscriptions }: ExtensionContext) => {
subscriptions.push(
commands.registerCommand('markdown.marp.export', exportCommand),
commands.registerCommand('markdown.marp.showQuickPick', showQuickPick),
commands.registerCommand(
'markdown.marp.toggleMarpPreview',
toggleMarpPreview
),
themes,
workspace.onDidChangeConfiguration(e => {
if (shouldRefreshConfs.some(c => e.affectsConfiguration(c))) {
Expand Down
3 changes: 2 additions & 1 deletion src/marp-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { tmpdir } from 'os'
import path from 'path'
import { promisify } from 'util'
import { TextDocument, workspace } from 'vscode'
import { WorkFile, marpConfiguration, marpCoreOptionForCLI } from './option'
import { WorkFile, marpCoreOptionForCLI } from './option'
import { marpConfiguration } from './utils'

const marpCliAsync = async (): Promise<typeof marpCli> =>
(await import('@marp-team/marp-cli')).default
Expand Down
4 changes: 1 addition & 3 deletions src/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { coerce, lt } from 'semver'
import { TextDocument, Uri, version, workspace } from 'vscode'
import { MarpOptions } from '@marp-team/marp-core'
import themes, { ThemeType } from './themes'
import { marpConfiguration } from './utils'

export interface WorkFile {
path: string
Expand All @@ -27,9 +28,6 @@ const coercedVer = coerce(version)
// Electron 6.
export const isRequiredPolyfill = coercedVer ? lt(coercedVer, '1.36.0') : false

export const marpConfiguration = () =>
workspace.getConfiguration('markdown.marp')

const breaks = (inheritedValue: boolean): boolean => {
switch (marpConfiguration().get<string>('breaks')) {
case 'off':
Expand Down
2 changes: 1 addition & 1 deletion src/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path'
import { promisify } from 'util'
import axios from 'axios'
import { Disposable, RelativePattern, Uri, commands, workspace } from 'vscode'
import { marpConfiguration } from './option'
import { marpConfiguration } from './utils'

export enum ThemeType {
File = 'File',
Expand Down
21 changes: 21 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { workspace } from 'vscode'

const frontMatterRegex = /^-{3,}\s*$\n([\s\S]*?)^\s*-{3}/m

export const marpDirectiveRegex = /^(marp\s*: +)(.*)\s*$/m

export const detectFrontMatter = (markdown: string): string | undefined => {
const m = markdown.match(frontMatterRegex)
return m && m.index === 0 ? m[1] : undefined
}

export const detectMarpFromMarkdown = (markdown: string): boolean => {
const frontmatter = detectFrontMatter(markdown)
if (!frontmatter) return false

const matched = marpDirectiveRegex.exec(frontmatter)
return matched ? matched[2] === 'true' : false
}

export const marpConfiguration = () =>
workspace.getConfiguration('markdown.marp')

0 comments on commit 033de90

Please sign in to comment.