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

New Code Snippet Syntax and features with tests #1336

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
@@ -7,6 +7,15 @@ exports[`snippet import snippet 1`] = `
</code></pre>
`;

exports[`snippet import snippet with comment/block transclusion => ::: 1`] = `
<pre><code class="language-vue">export default {
mounted() {
alert(&quot;yay!&quot;);
}
};
</code></pre>
`;

exports[`snippet import snippet with highlight multiple lines 1`] = `
<div class="highlight-lines">
<div class="highlighted">&nbsp;</div>
@@ -24,3 +33,30 @@ exports[`snippet import snippet with highlight single line 1`] = `
<br>
</div>export default function () { // .. }
`;

exports[`snippet import snippet with lang option 1`] = `
<pre><code class="language-ruby">def snippet
puts 'hello'
puts 'from'
puts 'vue'
end
</code></pre>
`;

exports[`snippet import snippet with line transclusion 1`] = `
<pre><code class="language-vue">&lt;style lang=&quot;scss&quot; scoped&gt;
.component {
display: flex;
}
&lt;/style&gt;
</code></pre>
`;

exports[`snippet import snippet with tag transclusion => style 1`] = `
<pre><code class="language-vue">&lt;style lang=&quot;scss&quot; scoped&gt;
.component {
display: flex;
}
&lt;/style&gt;
</code></pre>
`;
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js{1-3}
@[code highlight={1-3}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js)
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js{1,3}
@[code highlight={1,3}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@[code transclude={15-19}](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@[code transcludeTag=style](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@[code transcludeWith=:::](@/packages/@vuepress/markdown/__tests__/fragments/snippet.vue)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@[code lang=ruby](@/packages/@vuepress/markdown/__tests__/fragments/snippet.rb)
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<<< @/packages/@vuepress/core/__test__/markdown/fragments/snippet.js
@[code](@/packages/@vuepress/markdown/__tests__/fragments/snippet.js)
5 changes: 5 additions & 0 deletions packages/@vuepress/markdown/__tests__/fragments/snippet.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def snippet
puts 'hello'
puts 'from'
puts 'vue'
end
19 changes: 19 additions & 0 deletions packages/@vuepress/markdown/__tests__/fragments/snippet.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<div class="component"></div>
</template>

<script>
// :::
export default {
mounted () {
alert('yay!')
}
}
// :::
</script>

<style lang="scss" scoped>
.component {
display: flex;
}
</style>
33 changes: 33 additions & 0 deletions packages/@vuepress/markdown/__tests__/snippet.spec.js
Original file line number Diff line number Diff line change
@@ -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/)
})
})
145 changes: 126 additions & 19 deletions packages/@vuepress/markdown/lib/snippet.js
Original file line number Diff line number Diff line change
@@ -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
}