diff --git a/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap b/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap
index 6514898292..bae5b09699 100644
--- a/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap
+++ b/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap
@@ -17,6 +17,37 @@ exports[`snippet import snippet 1`] = `
`;
+exports[`snippet import snippet with region and highlight 1`] = `
+
@@ -35,3 +66,72 @@ exports[`snippet import snippet with highlight single line 1`] = `
// ..
}
`;
+
+exports[`snippet import snippet with indented region 1`] = `
+
<section>
+ <h1>Hello World</h1>
+</section>
+<div>Lorem Ipsum</div>
+`;
+
+exports[`snippet import snippet with region 1`] = `
+
function foo () {
+ return ({
+ dest: '../../vuepress',
+ locales: {
+ '/': {
+ lang: 'en-US',
+ title: 'VuePress',
+ description: 'Vue-powered Static Site Generator'
+ },
+ '/zh/': {
+ lang: 'zh-CN',
+ title: 'VuePress',
+ description: 'Vue 驱动的静态网站生成器'
+ }
+ },
+ head: [
+ ['link', { rel: 'icon', href: \`/logo.png\` }],
+ ['link', { rel: 'manifest', href: '/manifest.json' }],
+ ['meta', { name: 'theme-color', content: '#3eaf7c' }],
+ ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
+ ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
+ ['link', { rel: 'apple-touch-icon', href: \`/icons/apple-touch-icon-152x152.png\` }],
+ ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }],
+ ['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }],
+ ['meta', { name: 'msapplication-TileColor', content: '#000000' }]
+ ]
+ })
+}
+`;
+
+exports[`snippet import snippet with region and single line highlight > 10 1`] = `
+
function foo () {
+ return ({
+ dest: '../../vuepress',
+ locales: {
+ '/': {
+ lang: 'en-US',
+ title: 'VuePress',
+ description: 'Vue-powered Static Site Generator'
+ },
+ '/zh/': {
+ lang: 'zh-CN',
+ title: 'VuePress',
+ description: 'Vue 驱动的静态网站生成器'
+ }
+ },
+ head: [
+ ['link', { rel: 'icon', href: \`/logo.png\` }],
+ ['link', { rel: 'manifest', href: '/manifest.json' }],
+ ['meta', { name: 'theme-color', content: '#3eaf7c' }],
+ ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
+ ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
+ ['link', { rel: 'apple-touch-icon', href: \`/icons/apple-touch-icon-152x152.png\` }],
+ ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }],
+ ['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }],
+ ['meta', { name: 'msapplication-TileColor', content: '#000000' }]
+ ]
+ })
+}
+`;
diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-indented-region.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-indented-region.md
new file mode 100644
index 0000000000..71e314122f
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-indented-region.md
@@ -0,0 +1 @@
+<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-indented-region.html#body
diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region-and-highlight.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region-and-highlight.md
new file mode 100644
index 0000000000..45f13d6f05
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region-and-highlight.md
@@ -0,0 +1 @@
+<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1,3}
diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region-and-single-highlight.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region-and-single-highlight.md
new file mode 100644
index 0000000000..c67cff011a
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region-and-single-highlight.md
@@ -0,0 +1 @@
+<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{11}
diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region.md
new file mode 100644
index 0000000000..a335dbec46
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region.md
@@ -0,0 +1 @@
+<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet
diff --git a/packages/@vuepress/markdown/__tests__/fragments/snippet-with-indented-region.html b/packages/@vuepress/markdown/__tests__/fragments/snippet-with-indented-region.html
new file mode 100644
index 0000000000..ff845c8ac5
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/fragments/snippet-with-indented-region.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
Document
+
+
+
+
+
Lorem Ipsum
+
+
+
\ No newline at end of file
diff --git a/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js b/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js
new file mode 100644
index 0000000000..bacd171ddd
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js
@@ -0,0 +1,32 @@
+// #region snippet
+function foo () {
+ return ({
+ dest: '../../vuepress',
+ locales: {
+ '/': {
+ lang: 'en-US',
+ title: 'VuePress',
+ description: 'Vue-powered Static Site Generator'
+ },
+ '/zh/': {
+ lang: 'zh-CN',
+ title: 'VuePress',
+ description: 'Vue 驱动的静态网站生成器'
+ }
+ },
+ head: [
+ ['link', { rel: 'icon', href: `/logo.png` }],
+ ['link', { rel: 'manifest', href: '/manifest.json' }],
+ ['meta', { name: 'theme-color', content: '#3eaf7c' }],
+ ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
+ ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
+ ['link', { rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png` }],
+ ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }],
+ ['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }],
+ ['meta', { name: 'msapplication-TileColor', content: '#000000' }]
+ ]
+ })
+}
+// #endregion snippet
+
+export default foo
diff --git a/packages/@vuepress/markdown/__tests__/snippet.spec.js b/packages/@vuepress/markdown/__tests__/snippet.spec.js
index e8e25ea723..af018be673 100644
--- a/packages/@vuepress/markdown/__tests__/snippet.spec.js
+++ b/packages/@vuepress/markdown/__tests__/snippet.spec.js
@@ -30,4 +30,28 @@ describe('snippet', () => {
const output = mdH.render(input)
expect(output).toMatchSnapshot()
})
+
+ test('import snippet with region', () => {
+ const input = getFragment(__dirname, 'code-snippet-with-region.md')
+ const output = md.render(input)
+ expect(output).toMatchSnapshot()
+ })
+
+ test('import snippet with region and highlight', () => {
+ const input = getFragment(__dirname, 'code-snippet-with-region-and-highlight.md')
+ const output = md.render(input)
+ expect(output).toMatchSnapshot()
+ })
+
+ test('import snippet with region and single line highlight > 10', () => {
+ const input = getFragment(__dirname, 'code-snippet-with-region-and-single-highlight.md')
+ const output = md.render(input)
+ expect(output).toMatchSnapshot()
+ })
+
+ test('import snippet with indented region', () => {
+ const input = getFragment(__dirname, 'code-snippet-with-indented-region.md')
+ const output = md.render(input)
+ expect(output).toMatchSnapshot()
+ })
})
diff --git a/packages/@vuepress/markdown/lib/snippet.js b/packages/@vuepress/markdown/lib/snippet.js
index 9ee4e726f3..037498d336 100644
--- a/packages/@vuepress/markdown/lib/snippet.js
+++ b/packages/@vuepress/markdown/lib/snippet.js
@@ -1,5 +1,74 @@
const { fs, logger, path } = require('@vuepress/shared-utils')
+function dedent (text) {
+ const wRegexp = /^([ \t]*)(.*)\n/gm
+ let match; let minIndentLength = null
+
+ while ((match = wRegexp.exec(text)) !== null) {
+ const [indentation, content] = match.slice(1)
+ if (!content) continue
+
+ const indentLength = indentation.length
+ if (indentLength > 0) {
+ minIndentLength
+ = minIndentLength !== null
+ ? Math.min(minIndentLength, indentLength)
+ : indentLength
+ } else break
+ }
+
+ if (minIndentLength) {
+ text = text.replace(
+ new RegExp(`^[ \t]{${minIndentLength}}(.*)`, 'gm'),
+ '$1'
+ )
+ }
+
+ return text
+}
+
+function testLine (line, regexp, regionName, end = false) {
+ const [full, tag, name] = regexp.exec(line.trim()) || []
+
+ return (
+ full
+ && tag
+ && name === regionName
+ && tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/)
+ )
+}
+
+function findRegion (lines, regionName) {
+ const regionRegexps = [
+ /^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java
+ /^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss
+ /^#pragma ((?:end)?region) ([\w*-]+)$/, // C, C++
+ /^$/, // HTML, markdown
+ /^#((?:End )Region) ([\w*-]+)$/, // Visual Basic
+ /^::#((?:end)region) ([\w*-]+)$/, // Bat
+ /^# ?((?:end)?region) ([\w*-]+)$/ // C#, PHP, Powershell, Python, perl & misc
+ ]
+
+ let regexp = null
+ let start = -1
+
+ for (const [lineId, line] of lines.entries()) {
+ if (regexp === null) {
+ for (const reg of regionRegexps) {
+ if (testLine(line, reg, regionName)) {
+ start = lineId + 1
+ regexp = reg
+ break
+ }
+ }
+ } else if (testLine(line, regexp, regionName, true)) {
+ return { start, end: lineId, regexp }
+ }
+ }
+
+ return null
+}
+
module.exports = function snippet (md, options = {}) {
const fence = md.renderer.rules.fence
const root = options.root || process.cwd()
@@ -7,15 +76,32 @@ module.exports = function snippet (md, options = {}) {
md.renderer.rules.fence = (...args) => {
const [tokens, idx, , { loader }] = args
const token = tokens[idx]
- const { src } = token
+ const [src, regionName] = token.src ? token.src.split('#') : ['']
if (src) {
if (loader) {
loader.addDependency(src)
}
- if (fs.existsSync(src)) {
- token.content = fs.readFileSync(src, 'utf8')
+ const isAFile = fs.lstatSync(src).isFile()
+ if (fs.existsSync(src) && isAFile) {
+ let content = fs.readFileSync(src, 'utf8')
+
+ if (regionName) {
+ const lines = content.split(/\r?\n/)
+ const region = findRegion(lines, regionName)
+
+ if (region) {
+ content = dedent(
+ lines
+ .slice(region.start, region.end)
+ .filter(line => !region.regexp.test(line.trim()))
+ .join('\n')
+ )
+ }
+ }
+
+ token.content = content
} else {
- token.content = `Code snippet path not found: ${src}`
+ token.content = isAFile ? `Code snippet path not found: ${src}` : `Invalid code snippet option`
token.info = ''
logger.error(token.content)
}
@@ -44,15 +130,23 @@ module.exports = function snippet (md, options = {}) {
const start = pos + 3
const end = state.skipSpacesBack(max, pos)
- const rawPath = state.src.slice(start, end).trim().replace(/^@/, root)
- const filename = rawPath.split(/{/).shift().trim()
- const meta = rawPath.replace(filename, '')
+
+ /**
+ * raw path format: "/path/to/file.extension#region {meta}"
+ * where #region and {meta} are optionnal
+ *
+ * captures: ['/path/to/file.extension', 'extension', '#region', '{meta}']
+ */
+ const rawPathRegexp = /^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d+(?:[,-]\d+)?}))?$/
+
+ const rawPath = state.src.slice(start, end).trim().replace(/^@/, root).trim()
+ const [filename = '', extension = '', region = '', meta = ''] = (rawPathRegexp.exec(rawPath) || []).slice(1)
state.line = startLine + 1
const token = state.push('fence', 'code', 0)
- token.info = filename.split('.').pop() + meta
- token.src = path.resolve(filename)
+ token.info = extension + meta
+ token.src = path.resolve(filename) + region
token.markup = '```'
token.map = [startLine, startLine + 1]
diff --git a/packages/docs/docs/guide/markdown.md b/packages/docs/docs/guide/markdown.md
index af2a7f13bc..41403951aa 100644
--- a/packages/docs/docs/guide/markdown.md
+++ b/packages/docs/docs/guide/markdown.md
@@ -345,6 +345,29 @@ It also supports [line highlighting](#line-highlighting-in-code-blocks):
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()`.
:::
+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).
+
+**Input**
+
+``` md
+<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1}
+```
+
+**Code file**
+
+
+
+<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js
+
+
+
+**Output**
+
+
+
+<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1}
+
+
## Advanced Configuration