From 563c09361a1225936b328066b2f5adc9e5da4a02 Mon Sep 17 00:00:00 2001 From: Noel Mace Date: Tue, 17 Mar 2020 01:10:31 +0100 Subject: [PATCH 1/3] feat($markdown): snippet partial import Only import a code file region (VS Code `#region ` comments, instead of the whole file content) using a new `#region` parameter: `<<< @/path/file.ext#region{1-2}` --- .../__snapshots__/snippet.spec.js.snap | 19 ++++ .../code-snippet-with-indented-region.md | 1 + .../code-snippet-with-region-and-highlight.md | 1 + .../fragments/code-snippet-with-region.md | 1 + .../snippet-with-indented-region.html | 16 +++ .../fragments/snippet-with-region.js | 7 ++ .../markdown/__tests__/snippet.spec.js | 18 +++ packages/@vuepress/markdown/lib/snippet.js | 104 ++++++++++++++++-- packages/docs/docs/guide/markdown.md | 23 ++++ 9 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-indented-region.md create mode 100644 packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region-and-highlight.md create mode 100644 packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region.md create mode 100644 packages/@vuepress/markdown/__tests__/fragments/snippet-with-indented-region.html create mode 100644 packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js diff --git a/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap b/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap index 6514898292..59629e895b 100644 --- a/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap +++ b/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap @@ -17,6 +17,12 @@ exports[`snippet import snippet 1`] = ` `; +exports[`snippet import snippet with region and highlight 1`] = ` +
function foo () {
+  // ..
+}
+`; + exports[`snippet import snippet with highlight multiple lines 1`] = `
 
@@ -35,3 +41,16 @@ 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 () {
+  // ..
+}
+`; 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.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 + + + +
+

Hello World

+
+
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..2dce78a1db --- /dev/null +++ b/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js @@ -0,0 +1,7 @@ +// #region snippet +function foo () { + // .. +} +// #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..435ede266b 100644 --- a/packages/@vuepress/markdown/__tests__/snippet.spec.js +++ b/packages/@vuepress/markdown/__tests__/snippet.spec.js @@ -30,4 +30,22 @@ 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 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..fc84104d16 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 } + } + } + + return null +} + module.exports = function snippet (md, options = {}) { const fence = md.renderer.rules.fence const root = options.root || process.cwd() @@ -7,13 +76,26 @@ 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') + 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).join('\n') + ) + } + } + + token.content = content } else { token.content = `Code snippet path not found: ${src}` token.info = '' @@ -44,15 +126,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 From da909e8ca3ac6a51eb21e3ba4a664e8865f52e07 Mon Sep 17 00:00:00 2001 From: Noel Mace Date: Sun, 22 Mar 2020 00:50:24 +0100 Subject: [PATCH 2/3] fix($markdown): remove region comment when importing snippets permit nesting regions for partial code snippets import --- packages/@vuepress/markdown/lib/snippet.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/@vuepress/markdown/lib/snippet.js b/packages/@vuepress/markdown/lib/snippet.js index fc84104d16..73707903f0 100644 --- a/packages/@vuepress/markdown/lib/snippet.js +++ b/packages/@vuepress/markdown/lib/snippet.js @@ -62,7 +62,7 @@ function findRegion (lines, regionName) { } } } else if (testLine(line, regexp, regionName, true)) { - return { start, end: lineId } + return { start, end: lineId, regexp } } } @@ -90,7 +90,10 @@ module.exports = function snippet (md, options = {}) { if (region) { content = dedent( - lines.slice(region.start, region.end).join('\n') + lines + .slice(region.start, region.end) + .filter(line => !region.regexp.test(line.trim())) + .join('\n') ) } } From 1d3fac71a1f20d3a1045f945533ca6120c30262b Mon Sep 17 00:00:00 2001 From: Noel Mace Date: Tue, 31 Mar 2020 10:28:55 +0200 Subject: [PATCH 3/3] fix($markdown): code highlight on line > 10 A bad regexp was introducing a regression, as only one digit was accepted for code highlight. This error permitted to identify that when an invalid path is given, "src" falls down to CWD. This was leading to an ESDIR "invalid operation on a directory" error. --- .../__snapshots__/snippet.spec.js.snap | 85 ++++++++++++++++++- ...nippet-with-region-and-single-highlight.md | 1 + .../fragments/snippet-with-region.js | 27 +++++- .../markdown/__tests__/snippet.spec.js | 6 ++ packages/@vuepress/markdown/lib/snippet.js | 7 +- 5 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-region-and-single-highlight.md diff --git a/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap b/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap index 59629e895b..bae5b09699 100644 --- a/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap +++ b/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap @@ -19,7 +19,32 @@ exports[`snippet import snippet 1`] = ` exports[`snippet import snippet with region and highlight 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' }]
+    ]
+  })
 }
`; @@ -51,6 +76,62 @@ exports[`snippet import snippet with indented region 1`] = ` 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-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/snippet-with-region.js b/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js index 2dce78a1db..bacd171ddd 100644 --- a/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js +++ b/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js @@ -1,6 +1,31 @@ // #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 diff --git a/packages/@vuepress/markdown/__tests__/snippet.spec.js b/packages/@vuepress/markdown/__tests__/snippet.spec.js index 435ede266b..af018be673 100644 --- a/packages/@vuepress/markdown/__tests__/snippet.spec.js +++ b/packages/@vuepress/markdown/__tests__/snippet.spec.js @@ -43,6 +43,12 @@ describe('snippet', () => { 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) diff --git a/packages/@vuepress/markdown/lib/snippet.js b/packages/@vuepress/markdown/lib/snippet.js index 73707903f0..037498d336 100644 --- a/packages/@vuepress/markdown/lib/snippet.js +++ b/packages/@vuepress/markdown/lib/snippet.js @@ -81,7 +81,8 @@ module.exports = function snippet (md, options = {}) { if (loader) { loader.addDependency(src) } - if (fs.existsSync(src)) { + const isAFile = fs.lstatSync(src).isFile() + if (fs.existsSync(src) && isAFile) { let content = fs.readFileSync(src, 'utf8') if (regionName) { @@ -100,7 +101,7 @@ module.exports = function snippet (md, options = {}) { 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) } @@ -136,7 +137,7 @@ module.exports = function snippet (md, options = {}) { * * captures: ['/path/to/file.extension', 'extension', '#region', '{meta}'] */ - const rawPathRegexp = /^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d(?:[,-]\d)?}))?$/ + 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)