Skip to content

Commit d1a62e1

Browse files
mysticmindkiaking
andauthored
feat: import code snippet with region (#237) (#238)
close #237 Co-authored-by: Kia King Ishii <[email protected]>
1 parent a43933c commit d1a62e1

File tree

4 files changed

+213
-8
lines changed

4 files changed

+213
-8
lines changed

docs/guide/markdown.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,70 @@ module.exports = {
323323
}
324324
</style>
325325

326+
## Import Code Snippets
327+
328+
You can import code snippets from existing files via following syntax:
329+
330+
```md
331+
<<< @/filepath
332+
```
333+
334+
It also supports [line highlighting](#line-highlighting-in-code-blocks):
335+
336+
```md
337+
<<< @/filepath{highlightLines}
338+
```
339+
340+
**Input**
341+
342+
```md
343+
<<< @/snippets/snippet.js{2}
344+
```
345+
346+
**Code file**
347+
348+
<!--lint disable strong-marker-->
349+
350+
<<< @/snippets/snippet.js
351+
352+
<!--lint enable strong-marker-->
353+
354+
**Output**
355+
356+
<!--lint disable strong-marker-->
357+
358+
<<< @/snippets/snippet.js{2}
359+
360+
<!--lint enable strong-marker-->
361+
362+
::: tip
363+
The value of `@` corresponds to `process.cwd()`.
364+
:::
365+
366+
You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) to only include the corresponding part of the code file. You can provide a custom region name after a `#` following the filepath (`snippet` by default):
367+
368+
**Input**
369+
370+
```md
371+
<<< @/snippets/snippet-with-region.js{1}
372+
```
373+
374+
**Code file**
375+
376+
<!--lint disable strong-marker-->
377+
378+
<<< @/snippets/snippet-with-region.js
379+
380+
<!--lint enable strong-marker-->
381+
382+
**Output**
383+
384+
<!--lint disable strong-marker-->
385+
386+
<<< @/snippets/snippet-with-region.js#snippet{1}
387+
388+
<!--lint enable strong-marker-->
389+
326390
## Advanced Configuration
327391

328392
VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`:
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// #region snippet
2+
function foo() {
3+
// ..
4+
}
5+
// #endregion snippet
6+
7+
export default foo

docs/snippets/snippet.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function () {
2+
// ..
3+
}

src/node/markdown/plugins/snippet.ts

Lines changed: 139 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,83 @@
11
import fs from 'fs'
2+
import path from 'path'
23
import MarkdownIt from 'markdown-it'
34
import { RuleBlock } from 'markdown-it/lib/parser_block'
45

6+
function dedent(text: string) {
7+
const wRegexp = /^([ \t]*)(.*)\n/gm
8+
let match
9+
let minIndentLength = null
10+
11+
while ((match = wRegexp.exec(text)) !== null) {
12+
const [indentation, content] = match.slice(1)
13+
if (!content) continue
14+
15+
const indentLength = indentation.length
16+
if (indentLength > 0) {
17+
minIndentLength =
18+
minIndentLength !== null
19+
? Math.min(minIndentLength, indentLength)
20+
: indentLength
21+
} else break
22+
}
23+
24+
if (minIndentLength) {
25+
text = text.replace(
26+
new RegExp(`^[ \t]{${minIndentLength}}(.*)`, 'gm'),
27+
'$1'
28+
)
29+
}
30+
31+
return text
32+
}
33+
34+
function testLine(
35+
line: string,
36+
regexp: RegExp,
37+
regionName: string,
38+
end: boolean = false
39+
) {
40+
const [full, tag, name] = regexp.exec(line.trim()) || []
41+
42+
return (
43+
full &&
44+
tag &&
45+
name === regionName &&
46+
tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/)
47+
)
48+
}
49+
50+
function findRegion(lines: Array<string>, regionName: string) {
51+
const regionRegexps = [
52+
/^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java
53+
/^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss
54+
/^#pragma ((?:end)?region) ([\w*-]+)$/, // C, C++
55+
/^<!-- #?((?:end)?region) ([\w*-]+) -->$/, // HTML, markdown
56+
/^#((?:End )Region) ([\w*-]+)$/, // Visual Basic
57+
/^::#((?:end)region) ([\w*-]+)$/, // Bat
58+
/^# ?((?:end)?region) ([\w*-]+)$/ // C#, PHP, Powershell, Python, perl & misc
59+
]
60+
61+
let regexp = null
62+
let start = -1
63+
64+
for (const [lineId, line] of lines.entries()) {
65+
if (regexp === null) {
66+
for (const reg of regionRegexps) {
67+
if (testLine(line, reg, regionName)) {
68+
start = lineId + 1
69+
regexp = reg
70+
break
71+
}
72+
}
73+
} else if (testLine(line, regexp, regionName, true)) {
74+
return { start, end: lineId, regexp }
75+
}
76+
}
77+
78+
return null
79+
}
80+
581
export const snippetPlugin = (md: MarkdownIt, root: string) => {
682
const parser: RuleBlock = (state, startLine, endLine, silent) => {
783
const CH = '<'.charCodeAt(0)
@@ -24,23 +100,78 @@ export const snippetPlugin = (md: MarkdownIt, root: string) => {
24100

25101
const start = pos + 3
26102
const end = state.skipSpacesBack(max, pos)
27-
const rawPath = state.src.slice(start, end).trim().replace(/^@/, root)
28-
const filename = rawPath.split(/{/).shift()!.trim()
29-
const content = fs.existsSync(filename)
30-
? fs.readFileSync(filename).toString()
31-
: 'Not found: ' + filename
32-
const meta = rawPath.replace(filename, '')
103+
104+
/**
105+
* raw path format: "/path/to/file.extension#region {meta}"
106+
* where #region and {meta} are optional
107+
*
108+
* captures: ['/path/to/file.extension', 'extension', '#region', '{meta}']
109+
*/
110+
const rawPathRegexp = /^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d+(?:[,-]\d+)*}))?$/
111+
112+
const rawPath = state.src
113+
.slice(start, end)
114+
.trim()
115+
.replace(/^@/, root)
116+
.trim()
117+
const [filename = '', extension = '', region = '', meta = ''] = (
118+
rawPathRegexp.exec(rawPath) || []
119+
).slice(1)
33120

34121
state.line = startLine + 1
35122

36123
const token = state.push('fence', 'code', 0)
37-
token.info = filename.split('.').pop() + meta
38-
token.content = content
124+
token.info = extension + meta
125+
126+
// @ts-ignore
127+
token.src = path.resolve(filename) + region
39128
token.markup = '```'
40129
token.map = [startLine, startLine + 1]
41130

42131
return true
43132
}
44133

134+
const fence = md.renderer.rules.fence!
135+
136+
md.renderer.rules.fence = (...args) => {
137+
const [tokens, idx, , { loader }] = args
138+
const token = tokens[idx]
139+
// @ts-ignore
140+
const tokenSrc = token.src
141+
const [src, regionName] = tokenSrc ? tokenSrc.split('#') : ['']
142+
143+
if (src) {
144+
if (loader) {
145+
loader.addDependency(src)
146+
}
147+
const isAFile = fs.lstatSync(src).isFile()
148+
if (fs.existsSync(src) && isAFile) {
149+
let content = fs.readFileSync(src, 'utf8')
150+
151+
if (regionName) {
152+
const lines = content.split(/\r?\n/)
153+
const region = findRegion(lines, regionName)
154+
155+
if (region) {
156+
content = dedent(
157+
lines
158+
.slice(region.start, region.end)
159+
.filter((line: string) => !region.regexp.test(line.trim()))
160+
.join('\n')
161+
)
162+
}
163+
}
164+
165+
token.content = content
166+
} else {
167+
token.content = isAFile
168+
? `Code snippet path not found: ${src}`
169+
: `Invalid code snippet option`
170+
token.info = ''
171+
}
172+
}
173+
return fence(...args)
174+
}
175+
45176
md.block.ruler.before('fence', 'snippet', parser)
46177
}

0 commit comments

Comments
 (0)