Skip to content

Commit a90b2a2

Browse files
committed
feat($markdown): snippet partial import
Only import a code file region (VS Code `#region <name>` comments, instead of the whole file content) using a new `#region` parameter: `<<< @/path/file.ext#region{1-2}`
1 parent 76da780 commit a90b2a2

9 files changed

+179
-7
lines changed

packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap

+19
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ exports[`snippet import snippet 1`] = `
1717
</code></pre>
1818
`;
1919
20+
exports[`snippet import snippet with region and highlight 1`] = `
21+
<pre><code class="language-js{1,3}">function foo () {
22+
// ..
23+
}</code></pre>
24+
`;
25+
2026
exports[`snippet import snippet with highlight multiple lines 1`] = `
2127
<div class="highlight-lines">
2228
<div class="highlighted">&nbsp;</div>
@@ -35,3 +41,16 @@ exports[`snippet import snippet with highlight single line 1`] = `
3541
// ..
3642
}
3743
`;
44+
45+
exports[`snippet import snippet with indented region 1`] = `
46+
<pre><code class="language-html">&lt;section&gt;
47+
&lt;h1&gt;Hello World&lt;/h1&gt;
48+
&lt;/section&gt;
49+
&lt;div&gt;Lorem Ipsum&lt;/div&gt;</code></pre>
50+
`;
51+
52+
exports[`snippet import snippet with region 1`] = `
53+
<pre><code class="language-js">function foo () {
54+
// ..
55+
}</code></pre>
56+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-indented-region.html#body
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1,3}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
</head>
8+
<body>
9+
<!-- #region body -->
10+
<section>
11+
<h1>Hello World</h1>
12+
</section>
13+
<div>Lorem Ipsum</div>
14+
<!-- #endregion body -->
15+
</body>
16+
</html>
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

packages/@vuepress/markdown/__tests__/snippet.spec.js

+18
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,22 @@ describe('snippet', () => {
3030
const output = mdH.render(input)
3131
expect(output).toMatchSnapshot()
3232
})
33+
34+
test('import snippet with region', () => {
35+
const input = getFragment(__dirname, 'code-snippet-with-region.md')
36+
const output = md.render(input)
37+
expect(output).toMatchSnapshot()
38+
})
39+
40+
test('import snippet with region and highlight', () => {
41+
const input = getFragment(__dirname, 'code-snippet-with-region-and-highlight.md')
42+
const output = md.render(input)
43+
expect(output).toMatchSnapshot()
44+
})
45+
46+
test('import snippet with indented region', () => {
47+
const input = getFragment(__dirname, 'code-snippet-with-indented-region.md')
48+
const output = md.render(input)
49+
expect(output).toMatchSnapshot()
50+
})
3351
})

packages/@vuepress/markdown/lib/snippet.js

+97-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,101 @@
11
const { fs, logger, path } = require('@vuepress/shared-utils')
22

3+
function dedent (text) {
4+
const wRegexp = /^([ \t]*)(.*)\n/gm
5+
let match; let minIndentLength = null
6+
7+
while ((match = wRegexp.exec(text)) !== null) {
8+
const [indentation, content] = match.slice(1)
9+
if (!content) continue
10+
11+
const indentLength = indentation.length
12+
if (indentLength > 0) {
13+
minIndentLength
14+
= minIndentLength !== null
15+
? Math.min(minIndentLength, indentLength)
16+
: indentLength
17+
} else break
18+
}
19+
20+
if (minIndentLength) {
21+
text = text.replace(
22+
new RegExp(`^[ \t]{${minIndentLength}}(.*)`, 'gm'),
23+
'$1'
24+
)
25+
}
26+
27+
return text
28+
}
29+
30+
function testLine (line, regexp, regionName, end = false) {
31+
const [full, tag, name] = regexp.exec(line.trim()) || []
32+
33+
return (
34+
full
35+
&& tag
36+
&& name === regionName
37+
&& tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/)
38+
)
39+
}
40+
41+
function findRegion (lines, regionName) {
42+
const regionRegexps = [
43+
/^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java
44+
/^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss
45+
/^#pragma ((?:end)?region) ([\w*-]+)$/, // C, C++
46+
/^<!-- #?((?:end)?region) ([\w*-]+) -->$/, // HTML, markdown
47+
/^#((?:End )Region) ([\w*-]+)$/, // Visual Basic
48+
/^::#((?:end)region) ([\w*-]+)$/, // Bat
49+
/^# ?((?:end)?region) ([\w*-]+)$/ // C#, PHP, Powershell, Python, perl & misc
50+
]
51+
52+
let regexp = null
53+
let start = -1
54+
55+
for (const [lineId, line] of lines.entries()) {
56+
if (regexp === null) {
57+
for (const reg of regionRegexps) {
58+
if (testLine(line, reg, regionName)) {
59+
start = lineId + 1
60+
regexp = reg
61+
break
62+
}
63+
}
64+
} else if (testLine(line, regexp, regionName, true)) {
65+
return { start, end: lineId }
66+
}
67+
}
68+
69+
return null
70+
}
71+
372
module.exports = function snippet (md, options = {}) {
473
const fence = md.renderer.rules.fence
574
const root = options.root || process.cwd()
675

776
md.renderer.rules.fence = (...args) => {
877
const [tokens, idx, , { loader }] = args
978
const token = tokens[idx]
10-
const { src } = token
79+
const [src, regionName] = token.src ? token.src.split('#') : ['']
1180
if (src) {
1281
if (loader) {
1382
loader.addDependency(src)
1483
}
1584
if (fs.existsSync(src)) {
16-
token.content = fs.readFileSync(src, 'utf8')
85+
let content = fs.readFileSync(src, 'utf8')
86+
87+
if (regionName) {
88+
const lines = content.split(/\r?\n/)
89+
const region = findRegion(lines, regionName)
90+
91+
if (region) {
92+
content = dedent(
93+
lines.slice(region.start, region.end).join('\n')
94+
)
95+
}
96+
}
97+
98+
token.content = content
1799
} else {
18100
token.content = `Code snippet path not found: ${src}`
19101
token.info = ''
@@ -44,15 +126,23 @@ module.exports = function snippet (md, options = {}) {
44126

45127
const start = pos + 3
46128
const end = state.skipSpacesBack(max, pos)
47-
const rawPath = state.src.slice(start, end).trim().replace(/^@/, root)
48-
const filename = rawPath.split(/{/).shift().trim()
49-
const meta = rawPath.replace(filename, '')
129+
130+
/**
131+
* raw path format: "/path/to/file.extension#region {meta}"
132+
* where #region and {meta} are optionnal
133+
*
134+
* captures: ['/path/to/file.extension', 'extension', '#region', '{meta}']
135+
*/
136+
const rawPathRegexp = /^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d(?:[,-]\d)?}))?$/
137+
138+
const rawPath = state.src.slice(start, end).trim().replace(/^@/, root).trim()
139+
const [filename = '', extension = '', region = '', meta = ''] = (rawPathRegexp.exec(rawPath) || []).slice(1)
50140

51141
state.line = startLine + 1
52142

53143
const token = state.push('fence', 'code', 0)
54-
token.info = filename.split('.').pop() + meta
55-
token.src = path.resolve(filename)
144+
token.info = extension + meta
145+
token.src = path.resolve(filename) + region
56146
token.markup = '```'
57147
token.map = [startLine, startLine + 1]
58148

packages/docs/docs/guide/markdown.md

+19
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,25 @@ It also supports [line highlighting](#line-highlighting-in-code-blocks):
345345
Since the import of the code snippets will be executed before webpack compilation, you can’t use the path alias in webpack. The default value of `@` is `process.cwd()`.
346346
:::
347347

348+
You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) in order 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).
349+
350+
**Input**
351+
352+
``` md
353+
<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1}
354+
```
355+
356+
**Code file**
357+
358+
<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js
359+
360+
**Output**
361+
362+
<!--lint disable strong-marker-->
363+
364+
<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1}
365+
366+
<!--lint enable strong-marker-->
348367

349368
## Advanced Configuration
350369

0 commit comments

Comments
 (0)