Skip to content

Commit 6293e10

Browse files
feat: allow to customize markdown transformers (#1767)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 9f109ba commit 6293e10

File tree

12 files changed

+161
-48
lines changed

12 files changed

+161
-48
lines changed

docs/.vitepress/customizations.ts

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export default [
2727
text: 'Configure Code Runners',
2828
link: '/custom/config-code-runners',
2929
},
30+
{
31+
text: 'Configure Transformers',
32+
link: '/custom/config-transformers',
33+
},
3034
{
3135
text: 'Configure Monaco',
3236
link: '/custom/config-monaco',

docs/custom/config-transformers.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Configure Transformers
2+
3+
<Environment type="node" />
4+
5+
This setup function allows you to define custom transformers for the markdown content of **each slide**. This is useful when you want to add custom Markdown syntax and render custom code blocks. To start, create a `./setup/transformers.ts` file with the following content:
6+
7+
````ts twoslash
8+
import type { MarkdownTransformContext } from '@slidev/types'
9+
import { defineTransformersSetup } from '@slidev/types'
10+
11+
function myCodeblock(ctx: MarkdownTransformContext) {
12+
console.log('index in presentation', ctx.slide.index)
13+
ctx.s.replace(
14+
/^```myblock *(\{[^\n]*\})?\n([\s\S]+?)\n```/gm,
15+
(full, options = '', code = '') => {
16+
return `...`
17+
},
18+
)
19+
}
20+
21+
export default defineTransformersSetup(() => {
22+
return {
23+
pre: [],
24+
preCodeblock: [myCodeblock],
25+
postCodeblock: [],
26+
post: [],
27+
}
28+
})
29+
````
30+
31+
The return value should be the custom options for the transformers. The `pre`, `preCodeblock`, `postCodeblock`, and `post` are arrays of functions that will be called in order to transform the markdown content. The order of the transformers is:
32+
33+
1. `pre` from your project
34+
2. `pre` from addons and themes
35+
3. Import snippets syntax and Shiki magic move
36+
4. `preCodeblock` from your project
37+
5. `preCodeblock` from addons and themes
38+
6. Built-in special code blocks like Mermaid, Monaco and PlantUML
39+
7. `postCodeblock` from addons and themes
40+
8. `postCodeblock` from your project
41+
9. Other built-in transformers like code block wrapping
42+
10. `post` from addons and themes
43+
11. `post` from your project
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { TransformersSetup, TransformersSetupReturn } from '@slidev/types'
2+
import { loadSetups } from './load'
3+
4+
export default async function setupTransformers(roots: string[]) {
5+
const returns = await loadSetups<TransformersSetup>(roots, 'transformers.ts', [])
6+
const result: TransformersSetupReturn = {
7+
pre: [],
8+
preCodeblock: [],
9+
postCodeblock: [],
10+
post: [],
11+
}
12+
for (const r of returns.toReversed()) {
13+
if (r.pre)
14+
result.pre.push(...r.pre)
15+
if (r.preCodeblock)
16+
result.preCodeblock.push(...r.preCodeblock)
17+
}
18+
for (const r of returns) {
19+
if (r.postCodeblock)
20+
result.postCodeblock.push(...r.postCodeblock)
21+
if (r.post)
22+
result.post.push(...r.post)
23+
}
24+
return result
25+
}

packages/slidev/node/syntax/transform/in-page-css.ts

-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import { getCodeBlocks } from './utils'
55
* Transform <style> in markdown to scoped style with page selector
66
*/
77
export function transformPageCSS(ctx: MarkdownTransformContext) {
8-
const page = ctx.id.match(/(\d+)\.md$/)?.[1]
9-
if (!page)
10-
return
11-
128
const codeBlocks = getCodeBlocks(ctx.s.original)
139

1410
ctx.s.replace(
+18-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { MarkdownTransformContext, MarkdownTransformer } from '@slidev/types'
1+
import type { MarkdownTransformer, ResolvedSlidevOptions } from '@slidev/types'
2+
import setupTransformers from '../../setups/transformers'
23
import { transformCodeWrapper } from './code-wrapper'
34
import { transformPageCSS } from './in-page-css'
45
import { transformKaTexWrapper } from './katex-wrapper'
@@ -9,27 +10,27 @@ import { transformPlantUml } from './plant-uml'
910
import { transformSlotSugar } from './slot-sugar'
1011
import { transformSnippet } from './snippet'
1112

12-
export function applyMarkdownTransform(ctx: MarkdownTransformContext) {
13-
const transformers: (MarkdownTransformer | false)[] = [
13+
export async function getMarkdownTransformers(options: ResolvedSlidevOptions): Promise<(false | MarkdownTransformer)[]> {
14+
const extras = await setupTransformers(options.roots)
15+
return [
16+
...extras.pre,
17+
1418
transformSnippet,
15-
ctx.options.data.config.highlighter === 'shiki'
16-
&& transformMagicMove,
19+
options.data.config.highlighter === 'shiki' && transformMagicMove,
20+
21+
...extras.preCodeblock,
22+
1723
transformMermaid,
1824
transformPlantUml,
19-
ctx.options.data.features.monaco
20-
&& transformMonaco,
25+
options.data.features.monaco && transformMonaco,
26+
27+
...extras.postCodeblock,
28+
2129
transformCodeWrapper,
22-
ctx.options.data.features.katex
23-
&& transformKaTexWrapper,
30+
options.data.features.katex && transformKaTexWrapper,
2431
transformPageCSS,
2532
transformSlotSugar,
26-
]
2733

28-
for (const transformer of transformers) {
29-
if (!transformer)
30-
continue
31-
transformer(ctx)
32-
if (!ctx.s.isEmpty())
33-
ctx.s.commit()
34-
}
34+
...extras.post,
35+
]
3536
}

packages/slidev/node/syntax/transform/snippet.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,11 @@ function findRegion(lines: Array<string>, regionName: string) {
8383
*
8484
* captures: ['/path/to/file.extension', '#region', 'language', '{meta}']
8585
*/
86-
export function transformSnippet(ctx: MarkdownTransformContext) {
87-
const options = ctx.options
88-
const slideId = (ctx.id as string).match(/(\d+)\.md$/)?.[1]
89-
if (!slideId)
90-
return
91-
86+
export function transformSnippet({ s, slide, options }: MarkdownTransformContext) {
9287
const data = options.data
93-
const slideInfo = data.slides[+slideId - 1]
94-
const dir = path.dirname(slideInfo.source?.filepath ?? options.entry ?? options.userRoot)
88+
const dir = path.dirname(slide.source?.filepath ?? options.entry ?? options.userRoot)
9589

96-
ctx.s.replace(
90+
s.replace(
9791
// eslint-disable-next-line regexp/no-super-linear-backtracking
9892
/^<<<\s*(\S.*?)(#[\w-]+)?\s*(?:\s(\S+?))?\s*(\{.*)?$/gm,
9993
(full, filepath = '', regionName = '', lang = '', meta = '') => {
@@ -114,8 +108,8 @@ export function transformSnippet(ctx: MarkdownTransformContext) {
114108

115109
let content = fs.readFileSync(src, 'utf8')
116110

117-
slideInfo.snippetsUsed ??= {}
118-
slideInfo.snippetsUsed[src] = content
111+
slide.snippetsUsed ??= {}
112+
slide.snippetsUsed[src] = content
119113

120114
if (regionName) {
121115
const lines = content.split(/\r?\n/)

packages/slidev/node/vite/hmrPatch.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ export function createHmrPatchPlugin(): Plugin {
1010
transform(code, id) {
1111
if (!id.match(regexSlideSourceId))
1212
return
13-
const replaced = code.replace('if (_rerender_only)', 'if (false)')
14-
if (replaced !== code)
15-
return replaced
13+
return code.replace('if (_rerender_only)', 'if (false)')
1614
},
1715
}
1816
}

packages/slidev/node/vite/markdown.ts

+20-9
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import Markdown from 'unplugin-vue-markdown/vite'
22
import type { Plugin } from 'vite'
33
import type { MarkdownTransformContext, ResolvedSlidevOptions, SlidevPluginOptions } from '@slidev/types'
44
import MagicString from 'magic-string-stack'
5-
import { applyMarkdownTransform } from '../syntax/transform'
5+
import { getMarkdownTransformers } from '../syntax/transform'
66
import { useMarkdownItPlugins } from '../syntax/markdown-it'
7+
import { regexSlideSourceId } from './common'
78

89
export async function createMarkdownPlugin(
910
options: ResolvedSlidevOptions,
1011
{ markdown: mdOptions }: SlidevPluginOptions,
1112
): Promise<Plugin> {
1213
const markdownTransformMap = new Map<string, MagicString>()
14+
const transformers = await getMarkdownTransformers(options)
1315

1416
return Markdown({
1517
include: [/\.md$/],
@@ -32,20 +34,29 @@ export async function createMarkdownPlugin(
3234
transforms: {
3335
...mdOptions?.transforms,
3436
before(code, id) {
35-
if (id === options.entry)
36-
return ''
37+
code = mdOptions?.transforms?.before?.(code, id) ?? code
3738

39+
const match = id.match(regexSlideSourceId)
40+
if (!match)
41+
return code
42+
43+
const s = new MagicString(code)
44+
markdownTransformMap.set(id, s)
3845
const ctx: MarkdownTransformContext = {
39-
s: new MagicString(code),
40-
id,
46+
s,
47+
slide: options.data.slides[+match[1] - 1],
4148
options,
4249
}
4350

44-
applyMarkdownTransform(ctx)
45-
markdownTransformMap.set(id, ctx.s)
51+
for (const transformer of transformers) {
52+
if (!transformer)
53+
continue
54+
transformer(ctx)
55+
if (!ctx.s.isEmpty())
56+
ctx.s.commit()
57+
}
4658

47-
const s = ctx.s.toString()
48-
return mdOptions?.transforms?.before?.(s, id) ?? s
59+
return s.toString()
4960
},
5061
},
5162
}) as Plugin

packages/types/src/setups.ts

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { MermaidConfig } from 'mermaid'
99
import type { SlidevPreparserExtension } from './types'
1010
import type { CodeRunnerProviders } from './code-runner'
1111
import type { ContextMenuItem } from './context-menu'
12+
import type { MarkdownTransformer } from './transform'
1213

1314
export interface AppContext {
1415
app: App
@@ -61,10 +62,18 @@ export type ShikiSetupReturn =
6162
}
6263
>
6364

65+
export interface TransformersSetupReturn {
66+
pre: (MarkdownTransformer | false)[]
67+
preCodeblock: (MarkdownTransformer | false)[]
68+
postCodeblock: (MarkdownTransformer | false)[]
69+
post: (MarkdownTransformer | false)[]
70+
}
71+
6472
// node side
6573
export type ShikiSetup = (shiki: ShikiContext) => Awaitable<ShikiSetupReturn | void>
6674
export type KatexSetup = () => Awaitable<Partial<KatexOptions> | void>
6775
export type UnoSetup = () => Awaitable<Partial<UnoCssConfig> | void>
76+
export type TransformersSetup = () => Awaitable<Partial<TransformersSetupReturn>>
6877
export type PreparserSetup = (context: {
6978
filepath: string
7079
headmatter: Record<string, unknown>
@@ -94,6 +103,7 @@ export const defineRoutesSetup = defineSetup<RoutesSetup>
94103
export const defineMermaidSetup = defineSetup<MermaidSetup>
95104
export const defineKatexSetup = defineSetup<KatexSetup>
96105
export const defineShortcutsSetup = defineSetup<ShortcutsSetup>
106+
export const defineTransformersSetup = defineSetup<TransformersSetup>
97107
export const definePreparserSetup = defineSetup<PreparserSetup>
98108
export const defineCodeRunnersSetup = defineSetup<CodeRunnersSetup>
99109
export const defineContextMenuSetup = defineSetup<ContextMenuSetup>

packages/types/src/transform.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import type MagicString from 'magic-string-stack'
22
import type { ResolvedSlidevOptions } from './options'
3+
import type { SlideInfo } from './types'
34

45
export interface MarkdownTransformContext {
6+
/**
7+
* The magic string instance for the current markdown content
8+
*/
59
s: MagicString
6-
id: string
10+
11+
/**
12+
* The slide info of the current slide
13+
*/
14+
slide: SlideInfo
15+
16+
/**
17+
* Resolved Slidev options
18+
*/
719
options: ResolvedSlidevOptions
820
}
921

test/_tutils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function createTransformContext(code: string, shiki?: any): MarkdownTrans
66
const s = new MagicString(code)
77
return {
88
s,
9-
id: '1.md',
9+
slide: { } as any,
1010
options: {
1111
userRoot: path.join(__dirname, './fixtures/'),
1212
data: {

test/transform-all.test.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, it } from 'vitest'
2-
import { applyMarkdownTransform } from '../packages/slidev/node/syntax/transform'
2+
import { getMarkdownTransformers } from '../packages/slidev/node/syntax/transform'
33
import { createTransformContext } from './_tutils'
44

55
it('transform-all', async () => {
@@ -30,7 +30,26 @@ Foo \`{{ code }}\`
3030
<<< ./fixtures/snippets/snippet.ts#snippet
3131
`)
3232

33-
applyMarkdownTransform(ctx)
33+
const transformers = await getMarkdownTransformers({
34+
roots: [],
35+
data: {
36+
config: {
37+
highlighter: 'shiki',
38+
},
39+
features: {
40+
monaco: true,
41+
katex: true,
42+
},
43+
},
44+
} as any)
45+
46+
for (const transformer of transformers) {
47+
if (!transformer)
48+
continue
49+
transformer(ctx)
50+
if (!ctx.s.isEmpty())
51+
ctx.s.commit()
52+
}
3453

3554
expect(ctx.s.toString()).toMatchSnapshot()
3655
})

0 commit comments

Comments
 (0)