Skip to content

Commit b640bb1

Browse files
committed
feat: added code snippet features with tests
1 parent 0d56a99 commit b640bb1

12 files changed

+226
-22
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ exports[`snippet import snippet 1`] = `
77
</code></pre>
88
`;
99

10+
exports[`snippet import snippet with comment/block transclusion => ::: 1`] = `
11+
<pre><code class="language-vue">export default {
12+
mounted() {
13+
alert(&quot;yay!&quot;);
14+
}
15+
};
16+
</code></pre>
17+
`;
18+
1019
exports[`snippet import snippet with highlight multiple lines 1`] = `
1120
<div class="highlight-lines">
1221
<div class="highlighted">&nbsp;</div>
@@ -24,3 +33,30 @@ exports[`snippet import snippet with highlight single line 1`] = `
2433
<br>
2534
</div>export default function () { // .. }
2635
`;
36+
37+
exports[`snippet import snippet with lang option 1`] = `
38+
<pre><code class="language-ruby">def snippet
39+
puts 'hello'
40+
puts 'from'
41+
puts 'vue'
42+
end
43+
</code></pre>
44+
`;
45+
46+
exports[`snippet import snippet with line transclusion 1`] = `
47+
<pre><code class="language-vue">&lt;style lang=&quot;scss&quot; scoped&gt;
48+
.component {
49+
display: flex;
50+
}
51+
&lt;/style&gt;
52+
</code></pre>
53+
`;
54+
55+
exports[`snippet import snippet with tag transclusion => style 1`] = `
56+
<pre><code class="language-vue">&lt;style lang=&quot;scss&quot; scoped&gt;
57+
.component {
58+
display: flex;
59+
}
60+
&lt;/style&gt;
61+
</code></pre>
62+
`;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js{1-3}
1+
@[code highlight={1-3}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js{1,3}
1+
@[code highlight={1,3}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@[code transclude={15-19}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@[code transcludeTag=style](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@[code transcludeWith=:::](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@[code lang=ruby](@/packages/@vuepress/markdown/__tests__/fragments/snippet.rb)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js
1+
@[code](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def snippet
2+
puts 'hello'
3+
puts 'from'
4+
puts 'vue'
5+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<template>
2+
<div class="component"></div>
3+
</template>
4+
5+
<script>
6+
// :::
7+
export default {
8+
mounted () {
9+
alert('yay!')
10+
}
11+
}
12+
// :::
13+
</script>
14+
15+
<style lang="scss" scoped>
16+
.component {
17+
display: flex;
18+
}
19+
</style>

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,48 @@ describe('snippet', () => {
1212
expect(output).toMatchSnapshot()
1313
})
1414

15+
test('import snippet with lang option', async () => {
16+
const input = await getFragment('code-snippet-with-lang')
17+
const output = md.render(input)
18+
expect(output).toMatchSnapshot()
19+
expect(output).toMatch(/language-ruby/)
20+
})
21+
22+
test('import snippet with line transclusion', async () => {
23+
const input = await getFragment('code-snippet-transclude-line')
24+
const output = md.render(input)
25+
expect(output).toMatchSnapshot()
26+
expect(output).not.toMatch(/template|script/)
27+
expect(output).toMatch(/style/)
28+
})
29+
30+
test('import snippet with comment/block transclusion => :::', async () => {
31+
const input = await getFragment('code-snippet-transclude-with')
32+
const output = md.render(input)
33+
expect(output).toMatchSnapshot()
34+
expect(output).not.toMatch(/template|script|style/)
35+
expect(output).toMatch(/export default/)
36+
})
37+
38+
test('import snippet with tag transclusion => style', async () => {
39+
const input = await getFragment('code-snippet-transclude-tag')
40+
const output = md.render(input)
41+
expect(output).toMatchSnapshot()
42+
expect(output).not.toMatch(/template|script/)
43+
expect(output).toMatch(/style/)
44+
})
45+
1546
test('import snippet with highlight single line', async () => {
1647
const input = await getFragment('code-snippet-highlightLines-single')
1748
const output = mdH.render(input)
1849
expect(output).toMatchSnapshot()
50+
expect(output).toMatch(/highlighted/)
1951
})
2052

2153
test('import snippet with highlight multiple lines', async () => {
2254
const input = await getFragment('code-snippet-highlightLines-multiple')
2355
const output = mdH.render(input)
2456
expect(output).toMatchSnapshot()
57+
expect(output).toMatch(/highlighted/)
2558
})
2659
})

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

Lines changed: 126 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,148 @@
11
const { fs } = require('@vuepress/shared-utils')
22

3-
module.exports = function snippet (md, options = {}) {
4-
const root = options.root || process.cwd()
3+
const TRANSCLUDE_WITH = 'TRANSCLUDE_WITH'
4+
const TRANSCLUDE_LINE = 'TRANSCLUDE_LINE'
5+
const TRANSCLUDE_TAG = 'TRANSCLUDE_TAG'
6+
7+
module.exports = function (md, options) {
8+
const _root = options && options.root ? options.root : process.cwd()
9+
10+
const fileExists = f => {
11+
return fs.existsSync(f)
12+
}
13+
14+
const readFileSync = f => {
15+
return fileExists(f) ? fs.readFileSync(f).toString() : `Not Found: ${f}`
16+
}
17+
18+
const parseOptions = opts => {
19+
const _t = {}
20+
opts.trim().split(' ').forEach(pair => {
21+
const [opt, value] = pair.split('=')
22+
_t[opt] = value
23+
})
24+
return _t
25+
}
26+
27+
const dataFactory = (state, pos, max) => {
28+
const start = pos + 6
29+
const end = state.skipSpacesBack(max, pos) - 1
30+
const [opts, fullpathWithAtSym] = state.src.slice(start, end).trim().split('](')
31+
const fullpath = fullpathWithAtSym.replace(/^@/, _root).trim()
32+
const pathParts = fullpath.split('/')
33+
const fileParts = pathParts[pathParts.length - 1].split('.')
34+
35+
return {
36+
file: {
37+
resolve: fullpath,
38+
path: pathParts.slice(0, pathParts.length - 1).join('/'),
39+
name: fileParts.slice(0, fileParts.length - 1).join('.'),
40+
ext: fileParts[fileParts.length - 1]
41+
},
42+
options: parseOptions(opts),
43+
content: readFileSync(fullpath),
44+
fileExists: fileExists(fullpath)
45+
}
46+
}
47+
48+
const optionsMap = ({
49+
options
50+
}) => ({
51+
hasHighlight: options.highlight || false,
52+
hasTransclusion: options.transclude || options.transcludeWith || options.transcludeTag || false,
53+
get transclusionType () {
54+
if (options.transcludeWith) return TRANSCLUDE_WITH
55+
if (options.transcludeTag) return TRANSCLUDE_TAG
56+
if (options.transclude) return TRANSCLUDE_LINE
57+
},
58+
get meta () {
59+
return this.hasHighlight ? options.highlight : ''
60+
}
61+
})
62+
63+
const contentTransclusion = ({
64+
content,
65+
options
66+
}, transcludeType) => {
67+
const lines = content.split('\n')
68+
let _content = ''
69+
70+
if (transcludeType === TRANSCLUDE_LINE) {
71+
const [tStart, tEnd] = options.transclude.replace(/[^\d|-]/g, '').split('-')
72+
73+
lines.forEach((line, idx) => {
74+
const i = idx + 1
75+
if (i >= tStart && i <= tEnd) {
76+
_content += line + '\n'
77+
}
78+
})
79+
} else if (transcludeType === TRANSCLUDE_TAG) {
80+
const t = options.transcludeTag
81+
const tag = new RegExp(`${t}>$|^<${t}`)
82+
let matched = false
83+
84+
for (let i = 0; i < lines.length; i++) {
85+
const line = lines[i]
86+
87+
if (matched && tag.test(line)) {
88+
_content += line + '\n'
89+
break
90+
} else if (matched) {
91+
_content += line + '\n'
92+
} else if (tag.test(line)) {
93+
_content += line + '\n'
94+
matched = true
95+
}
96+
}
97+
} else if (transcludeType === TRANSCLUDE_WITH) {
98+
const t = options.transcludeWith
99+
const tag = new RegExp(t)
100+
let matched = false
101+
102+
for (let i = 0; i < lines.length; i++) {
103+
const line = lines[i]
104+
105+
if (tag.test(line)) {
106+
matched = !matched
107+
continue
108+
}
109+
110+
if (matched) {
111+
_content += line + '\n'
112+
}
113+
}
114+
}
115+
116+
return _content === '' ? 'No lines matched.' : _content
117+
}
118+
5119
function parser (state, startLine, endLine, silent) {
6-
const CH = '<'.charCodeAt(0)
120+
const matcher = [64, 91, 99, 111, 100, 101]
7121
const pos = state.bMarks[startLine] + state.tShift[startLine]
8122
const max = state.eMarks[startLine]
9123

10-
// if it's indented more than 3 spaces, it should be a code block
11124
if (state.sCount[startLine] - state.blkIndent >= 4) {
12125
return false
13126
}
14127

15-
for (let i = 0; i < 3; ++i) {
128+
for (let i = 0; i < 6; ++i) {
16129
const ch = state.src.charCodeAt(pos + i)
17-
if (ch !== CH || pos + i >= max) return false
130+
if (ch !== matcher[i] || pos + i >= max) return false
18131
}
19132

20-
if (silent) {
21-
return true
22-
}
133+
if (silent) return true
23134

24-
const start = pos + 3
25-
const end = state.skipSpacesBack(max, pos)
26-
const rawPath = state.src.slice(start, end).trim().replace(/^@/, root)
27-
const filename = rawPath.split(/[{\s]/).shift()
28-
const content = fs.existsSync(filename) ? fs.readFileSync(filename).toString() : 'Not found: ' + filename
29-
const meta = rawPath.replace(filename, '')
30-
31-
state.line = startLine + 1
135+
// handle code snippet include
136+
const d = dataFactory(state, pos, max)
137+
const opts = optionsMap(d)
32138

33139
const token = state.push('fence', 'code', 0)
34-
token.info = filename.split('.').pop() + meta
35-
token.content = content
140+
token.info = (d.options.lang || d.file.ext) + opts.meta
141+
token.content = d.fileExists && opts.hasTransclusion ? contentTransclusion(d, opts.transclusionType) : d.content
36142
token.markup = '```'
37143
token.map = [startLine, startLine + 1]
38144

145+
state.line = startLine + 1
39146
return true
40147
}
41148

0 commit comments

Comments
 (0)