diff --git a/CHANGELOG.md b/CHANGELOG.md index a72d5f73..27d8eb12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Auto completion for [`size` global directive](https://github.com/marp-team/marp-core#size-global-directive) ([#276](https://github.com/marp-team/marp-vscode/pull/276)) - `unknown-size` diagnostic: Notify if the specified size preset was not defined in a theme ([#276](https://github.com/marp-team/marp-vscode/pull/276)) - Auto-trigger suggestion for value of supported directives ([#277](https://github.com/marp-team/marp-vscode/pull/277)) +- `markdown.marp.pdf.noteAnnotations` config: Add presenter notes to exported PDF as note annotations ([#278](https://github.com/marp-team/marp-vscode/pull/278)) ### Changed diff --git a/package.json b/package.json index aec5c743..e443d3b5 100644 --- a/package.json +++ b/package.json @@ -163,6 +163,11 @@ "default": true, "description": "Enables the outline extension for Marp Markdown. If enabled, VS Code's outline view will reflect slide splitters, and you can fold regions of the slide content in the editor." }, + "markdown.marp.pdf.noteAnnotations": { + "type": "boolean", + "default": false, + "markdownDescription": "Adds [presenter notes](https://marpit.marp.app/usage?id=presenter-notes) to exported PDF as note annotations." + }, "markdown.marp.themes": { "type": "array", "default": [], diff --git a/src/__mocks__/vscode.ts b/src/__mocks__/vscode.ts index f1cb9fa4..e12c8694 100644 --- a/src/__mocks__/vscode.ts +++ b/src/__mocks__/vscode.ts @@ -9,6 +9,7 @@ const defaultConf: MockedConf = { 'markdown.marp.enableHtml': false, 'markdown.marp.exportType': 'pdf', 'markdown.marp.outlineExtension': true, + 'markdown.marp.pdf.noteAnnotations': false, 'window.zoomLevel': 0, } diff --git a/src/commands/export.test.ts b/src/commands/export.test.ts index 08e2c258..b6f6bbd6 100644 --- a/src/commands/export.test.ts +++ b/src/commands/export.test.ts @@ -1,5 +1,6 @@ import { commands, env, window, workspace } from 'vscode' import * as marpCli from '../marp-cli' +import * as option from '../option' import { createWorkspaceProxyServer } from '../workspace-proxy-server' import * as exportModule from './export' @@ -111,7 +112,7 @@ describe('#saveDialog', () => { expect(window.showSaveDialog).toHaveBeenCalledWith( expect.objectContaining({ - defaultUri: expect.objectContaining({ fsPath: '/tmp/test' }), + defaultUri: expect.objectContaining({ fsPath: '/tmp/test.pdf' }), }) ) }) @@ -122,7 +123,12 @@ describe('#saveDialog', () => { await exportModule.saveDialog(document) expect(window.showSaveDialog).toHaveBeenCalled() - const { filters } = (window.showSaveDialog as jest.Mock).mock.calls[0][0] + const { defaultUri, filters } = (window.showSaveDialog as jest.Mock).mock + .calls[0][0] + + expect(defaultUri).toStrictEqual( + expect.objectContaining({ fsPath: '/tmp/test.pptx' }) + ) expect(Object.values(filters)[0]).toStrictEqual(['pptx']) }) @@ -149,43 +155,66 @@ describe('#saveDialog', () => { }) describe('#doExport', () => { - const saveURI: any = { - scheme: 'file', - path: '/tmp/to.pdf', - fsPath: '/tmp/to.pdf', + const saveURI = (scheme = 'file', ext = 'pdf'): any => { + const instance = { + scheme, + path: `/tmp/to.${ext}`, + fsPath: `/tmp/to.${ext}`, + toString: () => `${scheme}://${instance.path}`, + } + return instance } + const document: any = { uri: { scheme: 'file', path: '/tmp/md.md', fsPath: '/tmp/md.md' }, } it('exports passed document via Marp CLI and opens it', async () => { const runMarpCLI = jest.spyOn(marpCli, 'default').mockImplementation() + const uri = saveURI() - await exportModule.doExport(saveURI, document) + await exportModule.doExport(uri, document) expect(runMarpCLI).toHaveBeenCalled() - expect(env.openExternal).toHaveBeenCalledWith(saveURI) + expect(env.openExternal).toHaveBeenCalledWith(uri) }) it('shows warning when Marp CLI throws error', async () => { jest.spyOn(marpCli, 'default').mockRejectedValue(new Error('ERROR')) - await exportModule.doExport(saveURI, document) + await exportModule.doExport(saveURI(), document) expect(window.showErrorMessage).toHaveBeenCalledWith( expect.stringContaining('[Error] ERROR') ) }) - describe('when the save path has non-file scheme', () => { - const saveURI = (scheme: string, ext: string): any => { - const instance = { - scheme, - path: `/tmp/to.${ext}`, - fsPath: `/tmp/to.${ext}`, - toString: () => `${scheme}://${instance.path}`, - } - return instance - } + describe('when enabled markdown.marp.pdf.noteAnnotations', () => { + beforeEach(() => { + setConfiguration({ 'markdown.marp.pdf.noteAnnotations': true }) + jest.spyOn(marpCli, 'default').mockImplementation() + }) + + it('enables pdfNotes option while exporting PDF', async () => { + const optionGeneratorSpy = jest.spyOn(option, 'marpCoreOptionForCLI') + await exportModule.doExport(saveURI('file', 'pdf'), document) + expect(optionGeneratorSpy).toHaveBeenCalledWith( + document, + expect.objectContaining({ pdfNotes: true }) + ) + }) + + it('disables pdfNotes option while exporting to other extensions', async () => { + const optionGeneratorSpy = jest.spyOn(option, 'marpCoreOptionForCLI') + await exportModule.doExport(saveURI('file', 'pptx'), document) + + expect(optionGeneratorSpy).toHaveBeenCalledWith( + document, + expect.objectContaining({ pdfNotes: false }) + ) + }) + }) + + describe('when the save path has non-file scheme', () => { it('exports the document into temporally path and copy it to the save path', async () => { jest.spyOn(marpCli, 'default').mockImplementation() @@ -253,7 +282,7 @@ describe('#doExport', () => { .spyOn(workspace, 'getWorkspaceFolder') .mockReturnValue(virtualWorkspace) - await exportModule.doExport(saveURI, vfsDocument) + await exportModule.doExport(saveURI(), vfsDocument) expect(createWorkspaceProxyServer).toHaveBeenCalledWith(virtualWorkspace) // dispose method @@ -264,18 +293,12 @@ describe('#doExport', () => { }) it('does not open workspace proxy server while exporting to html', async () => { - const saveURIHtml: any = { - scheme: 'file', - path: '/tmp/to.html', - fsPath: '/tmp/to.html', - } - jest.spyOn(marpCli, 'default').mockImplementation() jest .spyOn(workspace, 'getWorkspaceFolder') .mockReturnValue(virtualWorkspace) - await exportModule.doExport(saveURIHtml, vfsDocument) + await exportModule.doExport(saveURI('file', 'html'), vfsDocument) expect(createWorkspaceProxyServer).not.toHaveBeenCalled() }) }) diff --git a/src/commands/export.ts b/src/commands/export.ts index ae36e84d..eeaf60c1 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -99,6 +99,8 @@ export const doExport = async (uri: Uri, document: TextDocument) => { try { let outputPath = uri.fsPath + + const ouputExt = path.extname(uri.path) const outputToLocalFS = uri.scheme === 'file' // NOTE: It may return `undefined` if VS Code does not know about the @@ -110,13 +112,16 @@ export const doExport = async (uri: Uri, document: TextDocument) => { if (!outputToLocalFS) { outputPath = path.join( os.tmpdir(), - `marp-vscode-tmp-${nanoid()}${path.extname(uri.path)}` + `marp-vscode-tmp-${nanoid()}${ouputExt}` ) } // Run Marp CLI const conf = await createConfigFile(document, { allowLocalFiles: !proxyServer, + pdfNotes: + ouputExt === '.pdf' && + marpConfiguration().get('pdf.noteAnnotations'), }) try { @@ -168,16 +173,18 @@ export const saveDialog = async (document: TextDocument) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const defaultType = marpConfiguration().get('exportType')! + const ext = extensions[defaultType] ? `.${extensions[defaultType][0]}` : '' const baseTypes = Object.keys(extensions) const types = [...new Set([defaultType, ...baseTypes])] const saveURI = await window.showSaveDialog({ - defaultUri: Uri.file(fsPath.slice(0, -path.extname(fsPath).length)), + defaultUri: Uri.file(fsPath.slice(0, -path.extname(fsPath).length) + ext), filters: types.reduce((f, t) => { if (baseTypes.includes(t)) f[descriptions[t]] = extensions[t] return f }, {}), saveLabel: 'Export', + title: 'Export slide deck', }) if (saveURI) { diff --git a/src/option.test.ts b/src/option.test.ts index bb5d26ad..df239a41 100644 --- a/src/option.test.ts +++ b/src/option.test.ts @@ -23,8 +23,16 @@ describe('Option', () => { it('returns basic options', async () => { const opts = await subject({ uri: untitledUri }) - // --allow-local-files expect(opts.allowLocalFiles).toBe(true) + expect(opts.pdfNotes).toBeUndefined() + + const custom = await subject( + { uri: untitledUri }, + { allowLocalFiles: false, pdfNotes: true } + ) + + expect(custom.allowLocalFiles).toBe(false) + expect(custom.pdfNotes).toBe(true) }) it('enables HTML by preference', async () => { diff --git a/src/option.ts b/src/option.ts index bec05a22..77f6aba1 100644 --- a/src/option.ts +++ b/src/option.ts @@ -56,12 +56,16 @@ export const marpCoreOptionForPreview = ( export const marpCoreOptionForCLI = async ( { uri }: TextDocument, - { allowLocalFiles = true }: { allowLocalFiles?: boolean } = {} + { + allowLocalFiles = true, + pdfNotes, + }: { allowLocalFiles?: boolean; pdfNotes?: boolean } = {} ) => { const confMdPreview = workspace.getConfiguration('markdown.preview', uri) const baseOpts = { allowLocalFiles, + pdfNotes, html: enableHtml() || undefined, options: { markdown: {