Skip to content

Commit 2f1327b

Browse files
authored
feat($markdown): snippet partial import (#2225)
1 parent 1114ade commit 2f1327b

10 files changed

+302
-9
lines changed

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

+100
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,37 @@ 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+
return ({
23+
dest: '../../vuepress',
24+
locales: {
25+
'/': {
26+
lang: 'en-US',
27+
title: 'VuePress',
28+
description: 'Vue-powered Static Site Generator'
29+
},
30+
'/zh/': {
31+
lang: 'zh-CN',
32+
title: 'VuePress',
33+
description: 'Vue 驱动的静态网站生成器'
34+
}
35+
},
36+
head: [
37+
['link', { rel: 'icon', href: \`/logo.png\` }],
38+
['link', { rel: 'manifest', href: '/manifest.json' }],
39+
['meta', { name: 'theme-color', content: '#3eaf7c' }],
40+
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
41+
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
42+
['link', { rel: 'apple-touch-icon', href: \`/icons/apple-touch-icon-152x152.png\` }],
43+
['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }],
44+
['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }],
45+
['meta', { name: 'msapplication-TileColor', content: '#000000' }]
46+
]
47+
})
48+
}</code></pre>
49+
`;
50+
2051
exports[`snippet import snippet with highlight multiple lines 1`] = `
2152
<div class="highlight-lines">
2253
<div class="highlighted">&nbsp;</div>
@@ -35,3 +66,72 @@ exports[`snippet import snippet with highlight single line 1`] = `
3566
// ..
3667
}
3768
`;
69+
70+
exports[`snippet import snippet with indented region 1`] = `
71+
<pre><code class="language-html">&lt;section&gt;
72+
&lt;h1&gt;Hello World&lt;/h1&gt;
73+
&lt;/section&gt;
74+
&lt;div&gt;Lorem Ipsum&lt;/div&gt;</code></pre>
75+
`;
76+
77+
exports[`snippet import snippet with region 1`] = `
78+
<pre><code class="language-js">function foo () {
79+
return ({
80+
dest: '../../vuepress',
81+
locales: {
82+
'/': {
83+
lang: 'en-US',
84+
title: 'VuePress',
85+
description: 'Vue-powered Static Site Generator'
86+
},
87+
'/zh/': {
88+
lang: 'zh-CN',
89+
title: 'VuePress',
90+
description: 'Vue 驱动的静态网站生成器'
91+
}
92+
},
93+
head: [
94+
['link', { rel: 'icon', href: \`/logo.png\` }],
95+
['link', { rel: 'manifest', href: '/manifest.json' }],
96+
['meta', { name: 'theme-color', content: '#3eaf7c' }],
97+
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
98+
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
99+
['link', { rel: 'apple-touch-icon', href: \`/icons/apple-touch-icon-152x152.png\` }],
100+
['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }],
101+
['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }],
102+
['meta', { name: 'msapplication-TileColor', content: '#000000' }]
103+
]
104+
})
105+
}</code></pre>
106+
`;
107+
108+
exports[`snippet import snippet with region and single line highlight > 10 1`] = `
109+
<pre><code class="language-js{11}">function foo () {
110+
return ({
111+
dest: '../../vuepress',
112+
locales: {
113+
'/': {
114+
lang: 'en-US',
115+
title: 'VuePress',
116+
description: 'Vue-powered Static Site Generator'
117+
},
118+
'/zh/': {
119+
lang: 'zh-CN',
120+
title: 'VuePress',
121+
description: 'Vue 驱动的静态网站生成器'
122+
}
123+
},
124+
head: [
125+
['link', { rel: 'icon', href: \`/logo.png\` }],
126+
['link', { rel: 'manifest', href: '/manifest.json' }],
127+
['meta', { name: 'theme-color', content: '#3eaf7c' }],
128+
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
129+
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
130+
['link', { rel: 'apple-touch-icon', href: \`/icons/apple-touch-icon-152x152.png\` }],
131+
['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }],
132+
['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }],
133+
['meta', { name: 'msapplication-TileColor', content: '#000000' }]
134+
]
135+
})
136+
}</code></pre>
137+
`;
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{11}
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,32 @@
1+
// #region snippet
2+
function foo () {
3+
return ({
4+
dest: '../../vuepress',
5+
locales: {
6+
'/': {
7+
lang: 'en-US',
8+
title: 'VuePress',
9+
description: 'Vue-powered Static Site Generator'
10+
},
11+
'/zh/': {
12+
lang: 'zh-CN',
13+
title: 'VuePress',
14+
description: 'Vue 驱动的静态网站生成器'
15+
}
16+
},
17+
head: [
18+
['link', { rel: 'icon', href: `/logo.png` }],
19+
['link', { rel: 'manifest', href: '/manifest.json' }],
20+
['meta', { name: 'theme-color', content: '#3eaf7c' }],
21+
['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
22+
['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
23+
['link', { rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png` }],
24+
['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }],
25+
['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }],
26+
['meta', { name: 'msapplication-TileColor', content: '#000000' }]
27+
]
28+
})
29+
}
30+
// #endregion snippet
31+
32+
export default foo

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

+24
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,28 @@ 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 region and single line highlight > 10', () => {
47+
const input = getFragment(__dirname, 'code-snippet-with-region-and-single-highlight.md')
48+
const output = md.render(input)
49+
expect(output).toMatchSnapshot()
50+
})
51+
52+
test('import snippet with indented region', () => {
53+
const input = getFragment(__dirname, 'code-snippet-with-indented-region.md')
54+
const output = md.render(input)
55+
expect(output).toMatchSnapshot()
56+
})
3357
})

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

+103-9
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,107 @@
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, regexp }
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
}
15-
if (fs.existsSync(src)) {
16-
token.content = fs.readFileSync(src, 'utf8')
84+
const isAFile = fs.lstatSync(src).isFile()
85+
if (fs.existsSync(src) && isAFile) {
86+
let content = fs.readFileSync(src, 'utf8')
87+
88+
if (regionName) {
89+
const lines = content.split(/\r?\n/)
90+
const region = findRegion(lines, regionName)
91+
92+
if (region) {
93+
content = dedent(
94+
lines
95+
.slice(region.start, region.end)
96+
.filter(line => !region.regexp.test(line.trim()))
97+
.join('\n')
98+
)
99+
}
100+
}
101+
102+
token.content = content
17103
} else {
18-
token.content = `Code snippet path not found: ${src}`
104+
token.content = isAFile ? `Code snippet path not found: ${src}` : `Invalid code snippet option`
19105
token.info = ''
20106
logger.error(token.content)
21107
}
@@ -44,15 +130,23 @@ module.exports = function snippet (md, options = {}) {
44130

45131
const start = pos + 3
46132
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, '')
133+
134+
/**
135+
* raw path format: "/path/to/file.extension#region {meta}"
136+
* where #region and {meta} are optionnal
137+
*
138+
* captures: ['/path/to/file.extension', 'extension', '#region', '{meta}']
139+
*/
140+
const rawPathRegexp = /^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d+(?:[,-]\d+)?}))?$/
141+
142+
const rawPath = state.src.slice(start, end).trim().replace(/^@/, root).trim()
143+
const [filename = '', extension = '', region = '', meta = ''] = (rawPathRegexp.exec(rawPath) || []).slice(1)
50144

51145
state.line = startLine + 1
52146

53147
const token = state.push('fence', 'code', 0)
54-
token.info = filename.split('.').pop() + meta
55-
token.src = path.resolve(filename)
148+
token.info = extension + meta
149+
token.src = path.resolve(filename) + region
56150
token.markup = '```'
57151
token.map = [startLine, startLine + 1]
58152

packages/docs/docs/guide/markdown.md

+23
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,29 @@ 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+
<!--lint disable strong-marker-->
359+
360+
<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js
361+
362+
<!--lint enable strong-marker-->
363+
364+
**Output**
365+
366+
<!--lint disable strong-marker-->
367+
368+
<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1}
369+
370+
<!--lint enable strong-marker-->
348371

349372
## Advanced Configuration
350373

0 commit comments

Comments
 (0)