Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(markdown): snippet partial import #2225

Merged
merged 3 commits into from
May 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@ exports[`snippet import snippet 1`] = `
</code></pre>
`;

exports[`snippet import snippet with region and highlight 1`] = `
<pre><code class="language-js{1,3}">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' }]
]
})
}</code></pre>
`;

exports[`snippet import snippet with highlight multiple lines 1`] = `
<div class="highlight-lines">
<div class="highlighted">&nbsp;</div>
Expand All @@ -35,3 +66,72 @@ exports[`snippet import snippet with highlight single line 1`] = `
// ..
}
`;

exports[`snippet import snippet with indented region 1`] = `
<pre><code class="language-html">&lt;section&gt;
&lt;h1&gt;Hello World&lt;/h1&gt;
&lt;/section&gt;
&lt;div&gt;Lorem Ipsum&lt;/div&gt;</code></pre>
`;

exports[`snippet import snippet with region 1`] = `
<pre><code class="language-js">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' }]
]
})
}</code></pre>
`;

exports[`snippet import snippet with region and single line highlight > 10 1`] = `
<pre><code class="language-js{11}">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' }]
]
})
}</code></pre>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-indented-region.html#body
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1,3}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{11}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<<< @/packages/@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- #region body -->
<section>
<h1>Hello World</h1>
</section>
<div>Lorem Ipsum</div>
<!-- #endregion body -->
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions packages/@vuepress/markdown/__tests__/snippet.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
112 changes: 103 additions & 9 deletions packages/@vuepress/markdown/lib/snippet.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,107 @@
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++
/^<!-- #?((?:end)?region) ([\w*-]+) -->$/, // 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()

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)
}
Expand Down Expand Up @@ -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]

Expand Down
23 changes: 23 additions & 0 deletions packages/docs/docs/guide/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

<!--lint disable strong-marker-->

<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js

<!--lint enable strong-marker-->

**Output**

<!--lint disable strong-marker-->

<<< @/../@vuepress/markdown/__tests__/fragments/snippet-with-region.js#snippet{1}

<!--lint enable strong-marker-->

## Advanced Configuration

Expand Down