Skip to content

Commit 2a8bdac

Browse files
kermanxantfu
andauthored
feat: support using external file as code snippet, fix #1193 (#1222)
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
1 parent d263506 commit 2a8bdac

File tree

9 files changed

+202
-7
lines changed

9 files changed

+202
-7
lines changed

demo/starter/slides.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,16 @@ doubled.value = 2
145145

146146
<arrow v-click="[4, 5]" x1="350" y1="310" x2="195" y2="334" color="#953" width="2" arrowSize="1" />
147147

148+
<!-- This allow you to embed external code blocks -->
149+
<<< @/snippets/external.ts#snippet
150+
151+
<!-- Footer -->
148152
[^1]: [Learn More](https://sli.dev/guide/syntax.html#line-highlighting)
149153

154+
<!-- Inline style -->
150155
<style>
151156
.footnotes-sep {
152-
@apply mt-20 opacity-10;
157+
@apply mt-5 opacity-10;
153158
}
154159
.footnotes {
155160
@apply text-sm opacity-75;

demo/starter/snippets/external.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* eslint-disable no-console */
2+
3+
// #region snippet
4+
function hello() {
5+
console.log('Hello from snippets/external.ts')
6+
}
7+
// #endregion snippet
8+
9+
export default hello

packages/slidev/node/plugins/loaders.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,15 @@ export function createSlidesLoader(
175175
&& a?.title?.trim() === b?.title?.trim()
176176
&& a?.note === b?.note
177177
&& equal(a.frontmatter, b.frontmatter)
178+
&& Object.entries(a.snippetsUsed ?? {}).every(([file, oldContent]) => {
179+
try {
180+
const newContent = fs.readFileSync(file, 'utf-8')
181+
return oldContent === newContent
182+
}
183+
catch {
184+
return false
185+
}
186+
})
178187
)
179188
continue
180189

@@ -297,11 +306,11 @@ export function createSlidesLoader(
297306
preload: computed(() => frontmatter.preload),
298307
slide: {
299308
...(${JSON.stringify({
300-
...prepareSlideInfo(slide),
301-
frontmatter: undefined,
302-
// remove raw content in build, optimize the bundle size
303-
...(mode === 'build' ? { raw: '', content: '', note: '' } : {}),
304-
})}),
309+
...prepareSlideInfo(slide),
310+
frontmatter: undefined,
311+
// remove raw content in build, optimize the bundle size
312+
...(mode === 'build' ? { raw: '', content: '', note: '' } : {}),
313+
})}),
305314
frontmatter,
306315
filepath: ${JSON.stringify(slide.source?.filepath || entry)},
307316
id: ${pageNo},

packages/slidev/node/plugins/markdown.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import type { ResolvedSlidevOptions, SlidevPluginOptions } from '../options'
1919
import Katex from './markdown-it-katex'
2020
import { loadSetups } from './setupNode'
2121
import Prism from './markdown-it-prism'
22+
import { transformSnippet } from './transformSnippet'
2223

2324
export async function createMarkdownPlugin(
24-
{ data: { config }, roots, mode, entry }: ResolvedSlidevOptions,
25+
options: ResolvedSlidevOptions,
2526
{ markdown: mdOptions }: SlidevPluginOptions,
2627
): Promise<Plugin> {
28+
const { data: { config }, roots, mode, entry } = options
29+
2730
const setups: ((md: MarkdownIt) => void)[] = []
2831
const entryPath = slash(entry)
2932

@@ -105,6 +108,7 @@ export async function createMarkdownPlugin(
105108
: truncateMancoMark
106109

107110
code = transformSlotSugar(code)
111+
code = transformSnippet(code, options, id)
108112
code = transformMermaid(code)
109113
code = transformPlantUml(code, config.plantUmlServer)
110114
code = monaco(code)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Ported from https://github.com/vuejs/vitepress/blob/main/src/node/markdown/plugins/snippet.ts
2+
3+
import path from 'node:path'
4+
import fs from 'fs-extra'
5+
import type { ResolvedSlidevOptions } from '../options'
6+
7+
function dedent(text: string): string {
8+
const lines = text.split('\n')
9+
10+
const minIndentLength = lines.reduce((acc, line) => {
11+
for (let i = 0; i < line.length; i++) {
12+
if (line[i] !== ' ' && line[i] !== '\t')
13+
return Math.min(i, acc)
14+
}
15+
return acc
16+
}, Number.POSITIVE_INFINITY)
17+
18+
if (minIndentLength < Number.POSITIVE_INFINITY)
19+
return lines.map(x => x.slice(minIndentLength)).join('\n')
20+
21+
return text
22+
}
23+
24+
function testLine(
25+
line: string,
26+
regexp: RegExp,
27+
regionName: string,
28+
end: boolean = false,
29+
) {
30+
const [full, tag, name] = regexp.exec(line.trim()) || []
31+
32+
return (
33+
full
34+
&& tag
35+
&& name === regionName
36+
&& tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/)
37+
)
38+
}
39+
40+
function findRegion(lines: Array<string>, regionName: string) {
41+
const regionRegexps = [
42+
/^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java
43+
/^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss
44+
/^#pragma ((?:end)?region) ([\w*-]+)$/, // C, C++
45+
/^<!-- #?((?:end)?region) ([\w*-]+) -->$/, // HTML, markdown
46+
/^#((?:End )Region) ([\w*-]+)$/, // Visual Basic
47+
/^::#((?:end)region) ([\w*-]+)$/, // Bat
48+
/^# ?((?:end)?region) ([\w*-]+)$/, // C#, PHP, Powershell, Python, perl & misc
49+
]
50+
51+
let regexp = null
52+
let start = -1
53+
54+
for (const [lineId, line] of lines.entries()) {
55+
if (regexp === null) {
56+
for (const reg of regionRegexps) {
57+
if (testLine(line, reg, regionName)) {
58+
start = lineId + 1
59+
regexp = reg
60+
break
61+
}
62+
}
63+
}
64+
else if (testLine(line, regexp, regionName, true)) {
65+
return { start, end: lineId, regexp }
66+
}
67+
}
68+
69+
return null
70+
}
71+
72+
/**
73+
* format: ">>> /path/to/file.extension#region language meta..."
74+
* where #region, language and meta are optional
75+
* meta should starts with {
76+
* lang can contain special characters like C++, C#, F#, etc.
77+
* path can be relative to the current file or absolute
78+
* file extension is optional
79+
* path can contain spaces and dots
80+
*
81+
* captures: ['/path/to/file.extension', '#region', 'language', '{meta}']
82+
*/
83+
export function transformSnippet(md: string, options: ResolvedSlidevOptions, id: string) {
84+
const slideId = (id as string).match(/(\d+)\.md$/)?.[1]
85+
if (!slideId)
86+
return md
87+
const data = options.data
88+
const slideInfo = data.slides[+slideId - 1]
89+
const dir = path.dirname(slideInfo.source?.filepath ?? options?.entry ?? options!.userRoot)
90+
return md.replace(
91+
/^<<< *(.+?)(#[\w-]+)? *(?: (\S+?))? *(\{.*)?$/mg,
92+
(full, filepath = '', regionName = '', lang = '', meta = '') => {
93+
const firstLine = `\`\`\`${lang || path.extname(filepath).slice(1)} ${meta}`
94+
95+
const src = /^\@[\/]/.test(filepath)
96+
? path.resolve(options!.userRoot, filepath.slice(2))
97+
: path.resolve(dir, filepath)
98+
99+
data.entries!.push(src)
100+
101+
const isAFile = fs.statSync(src).isFile()
102+
if (!fs.existsSync(src) || !isAFile) {
103+
throw new Error(isAFile
104+
? `Code snippet path not found: ${src}`
105+
: `Invalid code snippet option`)
106+
}
107+
108+
let content = fs.readFileSync(src, 'utf8')
109+
110+
slideInfo.snippetsUsed ??= {}
111+
slideInfo.snippetsUsed[src] = content
112+
113+
if (regionName) {
114+
const lines = content.split(/\r?\n/)
115+
const region = findRegion(lines, regionName.slice(1))
116+
117+
if (region) {
118+
content = dedent(
119+
lines
120+
.slice(region.start, region.end)
121+
.filter(line => !region.regexp.test(line.trim()))
122+
.join('\n'),
123+
)
124+
}
125+
}
126+
127+
return `${firstLine}\n${content}\n\`\`\``
128+
},
129+
)
130+
}

packages/types/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface SlideInfo extends SlideInfoBase {
1818
end: number
1919
inline?: SlideInfoBase
2020
source?: SlideInfoWithPath
21+
snippetsUsed?: LoadedSnippets
2122
}
2223

2324
export interface SlideInfoWithPath extends SlideInfoBase {
@@ -70,3 +71,5 @@ export type PreparserExtensionLoader = (headmatter?: Record<string, unknown>, fi
7071
export type PreparserExtensionFromHeadmatter = (headmatter: any, exts: SlidevPreparserExtension[], filepath?: string) => Promise<SlidevPreparserExtension[]>
7172

7273
export type RenderContext = 'slide' | 'overview' | 'presenter' | 'previewNext'
74+
75+
export type LoadedSnippets = Record<string, string>

test/__snapshots__/transform.test.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`markdown transform > external snippet 1`] = `
4+
"
5+
\`\`\`ts {2|3|4}{lines:true}
6+
function _foo() {
7+
// ...
8+
}
9+
\`\`\`
10+
"
11+
`;
12+
313
exports[`markdown transform > inline CSS 1`] = `
414
"
515
# Page

test/fixtures/snippets/snippet.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// line 1
2+
3+
// #region snippet
4+
function _foo() {
5+
// ...
6+
}
7+
// #endregion snippet
8+
9+
// line 9

test/transform.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import path from 'node:path'
12
import {
23
transformMermaid,
34
transformPageCSS,
45
transformPlantUml,
56
transformSlotSugar,
67
} from '@slidev/cli/node/plugins/markdown'
8+
import { transformSnippet } from 'packages/slidev/node/plugins/transformSnippet'
79
import { describe, expect, it } from 'vitest'
810

911
// const isMacOS = process.platform === 'darwin'
@@ -141,4 +143,18 @@ Alice <- Bob : Hello, too!
141143
// we may need to find a better way to test this
142144
// expect(result).toMatchSnapshot()
143145
})
146+
147+
it('external snippet', () => {
148+
expect(transformSnippet(`
149+
<<< @/snippets/snippet.ts#snippet ts {2|3|4}{lines:true}
150+
`, {
151+
userRoot: path.join(__dirname, './fixtures/'),
152+
data: {
153+
slides: [
154+
{} as any,
155+
],
156+
entries: [],
157+
},
158+
} as any, `/@slidev/slides/1.md`)).toMatchSnapshot()
159+
})
144160
})

0 commit comments

Comments
 (0)