Skip to content

Commit

Permalink
Merge pull request #489 from marp-team/experimental-pptx-editable-option
Browse files Browse the repository at this point in the history
Add experimental `markdown.marp.pptx.editable` option
  • Loading branch information
yhatt authored Jan 23, 2025
2 parents 669710f + c9f4b65 commit d1e8538
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- `markdown.marp.exportAutoOpen` setting ([#464](https://github.com/marp-team/marp-vscode/pull/464) by [@rtfmkiesel](https://github.com/rtfmkiesel))
- Experimental `markdown.marp.pptx.editable` setting ([#489](https://github.com/marp-team/marp-vscode/pull/489))

### Changed

Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ We will enhance your VS Code as the slide deck writer. Mark `marp: true`, and wr

See the documentation of [Marpit Markdown](https://marpit.marp.app/markdown) and [the features of Marp Core](https://github.com/marp-team/marp-core#features) about how to write.

> Please refer **[https://marp.app/][marp]** for more details of Marp ecosystem. We have powerful tools for Marp Markdown: [Marpit Framework](https://marpit.marp.app/), [CLI tool](https://github.com/marp-team/marp-cli), [Web interface](https://web.marp.app/) and so on.
> Please refer **[https://marp.app/][marp]** for more details of Marp ecosystem. We have powerful tools for Marp Markdown: [Marpit Framework](https://marpit.marp.app/), [Marp Core](https://github.com/marp-team/marp-core), [CLI tool](https://github.com/marp-team/marp-cli) and so on.
[marp]: https://marp.app/

Expand Down Expand Up @@ -189,7 +189,9 @@ We extend the outline view to support slide pages in Marp Markdown.
<img src="https://raw.githubusercontent.com/marp-team/marp-vscode/main/docs/outline.png" alt="Outline view for each slide" width="400" />
</p>

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

Expand Down Expand Up @@ -220,6 +222,22 @@ In the untrusted workspace, HTML elements in Marp Markdown will be always ignore
> [!NOTE]
> A legacy setting `markdown.marp.enableHtml` is deprecated since v2. Please use `markdown.marp.html` instead.
## Experimental settings

Some of settings are marked as experimental. These feature may be unstable and change the spec in the future.

### [`markdown.marp.pptx.editable`](https://github.com/marp-team/marp-vscode/pull/489)

You can enable the experimental feature to export PPTX with editable contents, based on [Marp CLI's corresponding experimental option](https://github.com/marp-team/marp-cli#experimental-generate-editable-pptx---pptx-editable). This feature requires to install both of the compatible browser and [LibreOffice Impress](https://www.libreoffice.org/).

If set this setting as `smart`, Marp for VS Code will try to export into editable PPTX first, and then fallback to the regular PPTX export (non-editable) if failed.

### [`markdown.marp.strictPathResolutionDuringExport`](https://github.com/marp-team/marp-vscode/pull/367)

This experimental setting is useful to improve the export result compatibility with VS Code preview. If enabled, the export command will try to resolve relative paths in Markdown from VS Code workspace that a Markdown file belongs.

If disabled, or the Markdown does not belong to any workspace, the export command will resolve paths based on the local file system. This behavior is same as [Marp CLI](https://github.com/marp-team/marp-cli).

## Web extension

You can use Marp extension in VS Code for the Web environment like [vscode.dev](https://vscode.dev) and [github.dev](https://github.dev). Try opening https://github.dev/marp-team/marp-vscode/blob/main/docs/example.md, with an environment that has installed Marp extension.
Expand Down
18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,24 @@
"Add outlines based on both slide pages and Markdown headings."
]
},
"markdown.marp.pptx.editable": {
"type": "string",
"enum": [
"off",
"on",
"smart"
],
"default": "off",
"markdownDescription": "Sets whether make editable or not when exporting to PowerPoint document. You should install both of the browser and [LibreOffice Impress](https://libreoffice.org/) to export the editable PPTX. Please note that the editable PPTX output may fail to convert or be generated with an incomplete layout depending on the slide content.",
"markdownEnumDescriptions": [
"Always export non-editable PPTX. It has full visual reproducibillity and presenter note support.",
"Always export editable PPTX. Please note that the editable PPTX may fail to convert or be generated with an incomplete layout depending on the slide content.",
"Try to export editable PPTX first, and fallback to non-editable PPTX if failed."
],
"tags": [
"experimental"
]
},
"markdown.marp.strictPathResolutionDuringExport": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions src/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const defaultConf: MockedConf = {
'markdown.marp.outlineExtension': true,
'markdown.marp.pdf.noteAnnotations': false,
'markdown.marp.pdf.outlines': 'off',
'markdown.marp.pptx.editable': 'off',
'window.zoomLevel': 0,

// Legacy
Expand Down
85 changes: 85 additions & 0 deletions src/commands/export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,62 @@ describe('#doExport', () => {
})
})

describe('when enabled markdown.marp.pptx.editable', () => {
let marpCliMock: jest.SpyInstance

beforeEach(() => {
marpCliMock = jest.spyOn(marpCli, 'default').mockImplementation()
})

afterEach(() => marpCliMock?.mockRestore())

it('enables pptxEditable option while exporting PPTX when the setting is "on"', async () => {
setConfiguration({ 'markdown.marp.pptx.editable': 'on' })

const optionGeneratorSpy = jest.spyOn(option, 'marpCoreOptionForCLI')
await exportModule.doExport(saveURI('file', 'pptx'), document)
expect(optionGeneratorSpy).toHaveBeenCalledTimes(1)

const ret = await optionGeneratorSpy.mock.results[0].value
expect(ret).toStrictEqual(expect.objectContaining({ pptxEditable: true }))
})

it('enables pptxEditable option while exporting PPTX when the setting is "smart"', async () => {
setConfiguration({ 'markdown.marp.pptx.editable': 'smart' })

const optionGeneratorSpy = jest.spyOn(option, 'marpCoreOptionForCLI')
await exportModule.doExport(saveURI('file', 'pptx'), document)

expect(marpCliMock).toHaveBeenCalledTimes(1)
expect(optionGeneratorSpy).toHaveBeenCalledTimes(1)

const ret = await optionGeneratorSpy.mock.results[0].value
expect(ret).toStrictEqual(expect.objectContaining({ pptxEditable: true }))
})

it('retries exporting with disabling pptxEditable option when the setting is "smart" and failed the first export', async () => {
setConfiguration({ 'markdown.marp.pptx.editable': 'smart' })

marpCliMock.mockRejectedValueOnce(new Error('Failed to export'))

const optionGeneratorSpy = jest.spyOn(option, 'marpCoreOptionForCLI')
await exportModule.doExport(saveURI('file', 'pptx'), document)

expect(marpCliMock).toHaveBeenCalledTimes(2)
expect(optionGeneratorSpy).toHaveBeenCalledTimes(2)

const first = await optionGeneratorSpy.mock.results[0].value
expect(first).toStrictEqual(
expect.objectContaining({ pptxEditable: true }),
)

const second = await optionGeneratorSpy.mock.results[1].value
expect(second).toStrictEqual(
expect.objectContaining({ pptxEditable: false }),
)
})
})

describe('when CLI was thrown CLIError with BROWSER_NOT_FOUND error code', () => {
it.each`
browser | platform | expected
Expand Down Expand Up @@ -377,6 +433,35 @@ describe('#doExport', () => {
)
})

describe('when CLI was thrown CLIError with NOT_FOUND_SOFFICE error code', () => {
it('throws MarpCLIError with the user-friendly message to suggest installing LibreOffice', async () => {
const runMarpCLI = jest
.spyOn(marpCli, 'default')
.mockImplementation(async (_, __, opts) => {
opts?.onCLIError?.({
error: new marpCliModule.CLIError(
'mocked error',
marpCliModule.CLIErrorCode.NOT_FOUND_SOFFICE,
),
codes: marpCliModule.CLIErrorCode,
})
})

try {
await exportModule.doExport(saveURI(), document)

expect(window.showErrorMessage).toHaveBeenCalledTimes(1)
expect(window.showErrorMessage).toHaveBeenCalledWith(
expect.stringContaining(
'It requires to install LibreOffice Impress for exporting to the editable PowerPoint document.',
),
)
} finally {
runMarpCLI.mockRestore()
}
})
})

describe('when the save path has non-file scheme', () => {
it('exports the document into temporally path and copy it to the save path', async () => {
const marpCliMock = jest.spyOn(marpCli, 'default').mockImplementation()
Expand Down
132 changes: 79 additions & 53 deletions src/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,62 +138,88 @@ export const doExport = async (uri: Uri, document: TextDocument) => {
)
}

// Run Marp CLI
const conf = await createConfigFile(document, {
allowLocalFiles: !proxyServer,
pdfNotes:
ouputExt === '.pdf' &&
marpConfiguration().get<boolean>('pdf.noteAnnotations'),
})

try {
await marpCli(
['-c', conf.path, input.path, '-o', outputPath],
{ baseUrl },
{
onCLIError: ({ error, codes }) => {
if (error.errorCode === codes.NOT_FOUND_BROWSER) {
// Throw error with user-friendly instructions based on the current configuration
const browserOption = marpConfiguration().get<string>('browser')
const suggestBrowsers: string[] = []

switch (browserOption) {
case 'chrome':
suggestBrowsers.push(
...[
browsers.chrome,
process.platform === 'linux' ? browsers.chromium : '',
].filter((b) => !!b),
const pptxEditableSmart =
ouputExt === '.pptx' &&
marpConfiguration().get<string>('pptx.editable') === 'smart'

const runMarpCli = async ({
pptxEditable,
}: {
pptxEditable?: boolean
}) => {
const conf = await createConfigFile(document, {
allowLocalFiles: !proxyServer,
pdfNotes:
ouputExt === '.pdf' &&
marpConfiguration().get<boolean>('pdf.noteAnnotations'),
pptxEditable,
})

try {
await marpCli(
['-c', conf.path, input.path, '-o', outputPath],
{ baseUrl },
{
onCLIError: ({ error, codes }) => {
switch (error.errorCode) {
case codes.NOT_FOUND_BROWSER: {
// Throw error with user-friendly instructions based on the current configuration
const suggestBrowsers: string[] = []

switch (marpConfiguration().get<string>('browser')) {
case 'chrome':
suggestBrowsers.push(
...[
browsers.chrome,
process.platform === 'linux'
? browsers.chromium
: '',
].filter((b) => !!b),
)
break
case 'edge':
suggestBrowsers.push(browsers.edge)
break
case 'firefox':
suggestBrowsers.push(browsers.firefox)
break
default:
suggestBrowsers.push(
...[
browsers.chrome,
process.platform === 'linux'
? browsers.chromium
: '',
browsers.edge,
browsers.firefox,
].filter((b) => !!b),
)
}

throw new MarpCLIError(
`It requires to install a suitable browser, ${suggestBrowsers
.join(', ')
.replace(/, ([^,]*)$/, ' or $1')} for exporting.`,
)
break
case 'edge':
suggestBrowsers.push(browsers.edge)
break
case 'firefox':
suggestBrowsers.push(browsers.firefox)
break
default:
suggestBrowsers.push(
...[
browsers.chrome,
process.platform === 'linux' ? browsers.chromium : '',
browsers.edge,
browsers.firefox,
].filter((b) => !!b),
}
case codes.NOT_FOUND_SOFFICE:
throw new MarpCLIError(
'It requires to install LibreOffice Impress for exporting to the editable PowerPoint document.',
)
}

throw new MarpCLIError(
`It requires to install a suitable browser, ${suggestBrowsers
.join(', ')
.replace(/, ([^,]*)$/, ' or $1')} for exporting.`,
)
}
},
},
},
)
)
} catch (e) {
if (pptxEditableSmart && pptxEditable === true) {
return await runMarpCli({ pptxEditable: false })
}
throw e
} finally {
conf.cleanup()
}

const shouldOpen = marpConfiguration().get<boolean>('exportAutoOpen')!
const shouldOpen = marpConfiguration().get<boolean>('exportAutoOpen')

if (outputToLocalFS && shouldOpen) {
env.openExternal(uri)
Expand All @@ -214,9 +240,9 @@ export const doExport = async (uri: Uri, document: TextDocument) => {
`Marp slide deck was successfully exported to ${uri.toString()}.`,
)
}
} finally {
conf.cleanup()
}

await runMarpCli({ pptxEditable: pptxEditableSmart ? true : undefined })
} catch (e) {
window.showErrorMessage(
`Failure to export${(() => {
Expand Down
18 changes: 16 additions & 2 deletions src/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,23 @@ export const marpCoreOptionForCLI = async (
{
allowLocalFiles = true,
pdfNotes,
}: { allowLocalFiles?: boolean; pdfNotes?: boolean } = {},
pptxEditable: _pptxEditable,
}: {
allowLocalFiles?: boolean
pdfNotes?: boolean
pptxEditable?: boolean
} = {},
) => {
const confMdPreview = workspace.getConfiguration('markdown.preview', uri)

const pptxEditable = (() => {
if (_pptxEditable !== undefined) return _pptxEditable

const v = marpConfiguration().get<'off' | 'on' | 'smart'>('pptx.editable')
if (v === 'on') return true
if (v === 'off') return false
})()

let browser = marpConfiguration().get<'auto' | 'chrome' | 'edge' | 'firefox'>(
'browser',
)
Expand Down Expand Up @@ -180,8 +193,9 @@ export const marpCoreOptionForCLI = async (
},
math: math(),
},
pptxEditable,
themeSet: [] as string[],
} as ConfigForCLI
} satisfies Config as ConfigForCLI

const workspaceFolder = workspace.getWorkspaceFolder(uri)
const parentFolder = uri.scheme === 'file' && path.dirname(uri.fsPath)
Expand Down

0 comments on commit d1e8538

Please sign in to comment.