diff --git a/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap b/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap index 3c97bcddcc..53893eef97 100644 --- a/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap +++ b/packages/@vuepress/markdown/__tests__/__snapshots__/snippet.spec.js.snap @@ -7,6 +7,15 @@ exports[`snippet import snippet 1`] = ` `; +exports[`snippet import snippet with comment/block transclusion => ::: 1`] = ` +
export default {
+  mounted() {
+    alert("yay!");
+  }
+};
+
+`; + exports[`snippet import snippet with highlight multiple lines 1`] = `
 
@@ -24,3 +33,30 @@ exports[`snippet import snippet with highlight single line 1`] = `
export default function () { // .. } `; + +exports[`snippet import snippet with lang option 1`] = ` +
def snippet
+  puts 'hello'
+  puts 'from'
+  puts 'vue'
+end
+
+`; + +exports[`snippet import snippet with line transclusion 1`] = ` +
<style lang="scss" scoped>
+.component {
+  display: flex;
+}
+</style>
+
+`; + +exports[`snippet import snippet with tag transclusion => style 1`] = ` +
<style lang="scss" scoped>
+.component {
+  display: flex;
+}
+</style>
+
+`; diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-highlightLines-multiple.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-highlightLines-multiple.md index c06bf83f85..8b858770f2 100644 --- a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-highlightLines-multiple.md +++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-highlightLines-multiple.md @@ -1 +1 @@ -<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js{1-3} +@[code highlight={1-3}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js) diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-highlightLines-single.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-highlightLines-single.md index d98c6ce859..31c5672695 100644 --- a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-highlightLines-single.md +++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-highlightLines-single.md @@ -1 +1 @@ -<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js{1,3} +@[code highlight={1,3}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js) diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-line.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-line.md new file mode 100644 index 0000000000..81d66b7a24 --- /dev/null +++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-line.md @@ -0,0 +1 @@ +@[code transclude={15-19}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue) diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-tag.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-tag.md new file mode 100644 index 0000000000..31efeeaa2a --- /dev/null +++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-tag.md @@ -0,0 +1 @@ +@[code transcludeTag=style](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue) diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-with.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-with.md new file mode 100644 index 0000000000..1a8eac99f0 --- /dev/null +++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-transclude-with.md @@ -0,0 +1 @@ +@[code transcludeWith=:::](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue) diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-lang.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-lang.md new file mode 100644 index 0000000000..ac11fc8659 --- /dev/null +++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet-with-lang.md @@ -0,0 +1 @@ +@[code lang=ruby](@/packages/@vuepress/markdown/__tests__/fragments/snippet.rb) diff --git a/packages/@vuepress/markdown/__tests__/fragments/code-snippet.md b/packages/@vuepress/markdown/__tests__/fragments/code-snippet.md index 5b87126e7f..1303113482 100644 --- a/packages/@vuepress/markdown/__tests__/fragments/code-snippet.md +++ b/packages/@vuepress/markdown/__tests__/fragments/code-snippet.md @@ -1 +1 @@ -<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js \ No newline at end of file +@[code](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js) diff --git a/packages/@vuepress/markdown/__tests__/fragments/snippet.rb b/packages/@vuepress/markdown/__tests__/fragments/snippet.rb new file mode 100644 index 0000000000..bd8ad3cee6 --- /dev/null +++ b/packages/@vuepress/markdown/__tests__/fragments/snippet.rb @@ -0,0 +1,5 @@ +def snippet + puts 'hello' + puts 'from' + puts 'vue' +end diff --git a/packages/@vuepress/markdown/__tests__/fragments/snippet.vue b/packages/@vuepress/markdown/__tests__/fragments/snippet.vue new file mode 100644 index 0000000000..7b4d097d7d --- /dev/null +++ b/packages/@vuepress/markdown/__tests__/fragments/snippet.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/@vuepress/markdown/__tests__/snippet.spec.js b/packages/@vuepress/markdown/__tests__/snippet.spec.js index 5e05b481ed..2cdb667f92 100644 --- a/packages/@vuepress/markdown/__tests__/snippet.spec.js +++ b/packages/@vuepress/markdown/__tests__/snippet.spec.js @@ -12,15 +12,48 @@ describe('snippet', () => { expect(output).toMatchSnapshot() }) + test('import snippet with lang option', async () => { + const input = await getFragment('code-snippet-with-lang') + const output = md.render(input) + expect(output).toMatchSnapshot() + expect(output).toMatch(/language-ruby/) + }) + + test('import snippet with line transclusion', async () => { + const input = await getFragment('code-snippet-transclude-line') + const output = md.render(input) + expect(output).toMatchSnapshot() + expect(output).not.toMatch(/template|script/) + expect(output).toMatch(/style/) + }) + + test('import snippet with comment/block transclusion => :::', async () => { + const input = await getFragment('code-snippet-transclude-with') + const output = md.render(input) + expect(output).toMatchSnapshot() + expect(output).not.toMatch(/template|script|style/) + expect(output).toMatch(/export default/) + }) + + test('import snippet with tag transclusion => style', async () => { + const input = await getFragment('code-snippet-transclude-tag') + const output = md.render(input) + expect(output).toMatchSnapshot() + expect(output).not.toMatch(/template|script/) + expect(output).toMatch(/style/) + }) + test('import snippet with highlight single line', async () => { const input = await getFragment('code-snippet-highlightLines-single') const output = mdH.render(input) expect(output).toMatchSnapshot() + expect(output).toMatch(/highlighted/) }) test('import snippet with highlight multiple lines', async () => { const input = await getFragment('code-snippet-highlightLines-multiple') const output = mdH.render(input) expect(output).toMatchSnapshot() + expect(output).toMatch(/highlighted/) }) }) diff --git a/packages/@vuepress/markdown/lib/snippet.js b/packages/@vuepress/markdown/lib/snippet.js index 895e7b2d5f..f4d37207b0 100644 --- a/packages/@vuepress/markdown/lib/snippet.js +++ b/packages/@vuepress/markdown/lib/snippet.js @@ -1,41 +1,148 @@ const { fs } = require('@vuepress/shared-utils') -module.exports = function snippet (md, options = {}) { - const root = options.root || process.cwd() +const TRANSCLUDE_WITH = 'TRANSCLUDE_WITH' +const TRANSCLUDE_LINE = 'TRANSCLUDE_LINE' +const TRANSCLUDE_TAG = 'TRANSCLUDE_TAG' + +module.exports = function (md, options) { + const _root = options && options.root ? options.root : process.cwd() + + const fileExists = f => { + return fs.existsSync(f) + } + + const readFileSync = f => { + return fileExists(f) ? fs.readFileSync(f).toString() : `Not Found: ${f}` + } + + const parseOptions = opts => { + const _t = {} + opts.trim().split(' ').forEach(pair => { + const [opt, value] = pair.split('=') + _t[opt] = value + }) + return _t + } + + const dataFactory = (state, pos, max) => { + const start = pos + 6 + const end = state.skipSpacesBack(max, pos) - 1 + const [opts, fullpathWithAtSym] = state.src.slice(start, end).trim().split('](') + const fullpath = fullpathWithAtSym.replace(/^@/, _root).trim() + const pathParts = fullpath.split('/') + const fileParts = pathParts[pathParts.length - 1].split('.') + + return { + file: { + resolve: fullpath, + path: pathParts.slice(0, pathParts.length - 1).join('/'), + name: fileParts.slice(0, fileParts.length - 1).join('.'), + ext: fileParts[fileParts.length - 1] + }, + options: parseOptions(opts), + content: readFileSync(fullpath), + fileExists: fileExists(fullpath) + } + } + + const optionsMap = ({ + options + }) => ({ + hasHighlight: options.highlight || false, + hasTransclusion: options.transclude || options.transcludeWith || options.transcludeTag || false, + get transclusionType () { + if (options.transcludeWith) return TRANSCLUDE_WITH + if (options.transcludeTag) return TRANSCLUDE_TAG + if (options.transclude) return TRANSCLUDE_LINE + }, + get meta () { + return this.hasHighlight ? options.highlight : '' + } + }) + + const contentTransclusion = ({ + content, + options + }, transcludeType) => { + const lines = content.split('\n') + let _content = '' + + if (transcludeType === TRANSCLUDE_LINE) { + const [tStart, tEnd] = options.transclude.replace(/[^\d|-]/g, '').split('-') + + lines.forEach((line, idx) => { + const i = idx + 1 + if (i >= tStart && i <= tEnd) { + _content += line + '\n' + } + }) + } else if (transcludeType === TRANSCLUDE_TAG) { + const t = options.transcludeTag + const tag = new RegExp(`${t}>$|^<${t}`) + let matched = false + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + if (matched && tag.test(line)) { + _content += line + '\n' + break + } else if (matched) { + _content += line + '\n' + } else if (tag.test(line)) { + _content += line + '\n' + matched = true + } + } + } else if (transcludeType === TRANSCLUDE_WITH) { + const t = options.transcludeWith + const tag = new RegExp(t) + let matched = false + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + if (tag.test(line)) { + matched = !matched + continue + } + + if (matched) { + _content += line + '\n' + } + } + } + + return _content === '' ? 'No lines matched.' : _content + } + function parser (state, startLine, endLine, silent) { - const CH = '<'.charCodeAt(0) + const matcher = [64, 91, 99, 111, 100, 101] const pos = state.bMarks[startLine] + state.tShift[startLine] const max = state.eMarks[startLine] - // if it's indented more than 3 spaces, it should be a code block if (state.sCount[startLine] - state.blkIndent >= 4) { return false } - for (let i = 0; i < 3; ++i) { + for (let i = 0; i < 6; ++i) { const ch = state.src.charCodeAt(pos + i) - if (ch !== CH || pos + i >= max) return false + if (ch !== matcher[i] || pos + i >= max) return false } - if (silent) { - return true - } + if (silent) return true - const start = pos + 3 - const end = state.skipSpacesBack(max, pos) - const rawPath = state.src.slice(start, end).trim().replace(/^@/, root) - const filename = rawPath.split(/[{\s]/).shift() - const content = fs.existsSync(filename) ? fs.readFileSync(filename).toString() : 'Not found: ' + filename - const meta = rawPath.replace(filename, '') - - state.line = startLine + 1 + // handle code snippet include + const d = dataFactory(state, pos, max) + const opts = optionsMap(d) const token = state.push('fence', 'code', 0) - token.info = filename.split('.').pop() + meta - token.content = content + token.info = (d.options.lang || d.file.ext) + opts.meta + token.content = d.fileExists && opts.hasTransclusion ? contentTransclusion(d, opts.transclusionType) : d.content token.markup = '```' token.map = [startLine, startLine + 1] + state.line = startLine + 1 return true }