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: import code snippet with region (#237) #238

Merged
merged 6 commits into from
Apr 8, 2021
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
64 changes: 64 additions & 0 deletions docs/guide/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,70 @@ module.exports = {
}
</style>

## Import Code Snippets

You can import code snippets from existing files via following syntax:

```md
<<< @/filepath
```

It also supports [line highlighting](#line-highlighting-in-code-blocks):

```md
<<< @/filepath{highlightLines}
```

**Input**

```md
<<< @/snippets/snippet.js{2}
```

**Code file**

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

<<< @/snippets/snippet.js

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

**Output**

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

<<< @/snippets/snippet.js{2}

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

::: tip
The value of `@` corresponds to `process.cwd()`.
:::

You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) 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
<<< @/snippets/snippet-with-region.js{1}
```

**Code file**

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

<<< @/snippets/snippet-with-region.js

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

**Output**

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

<<< @/snippets/snippet-with-region.js#snippet{1}

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

## Advanced Configuration

VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`:
Expand Down
7 changes: 7 additions & 0 deletions docs/snippets/snippet-with-region.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// #region snippet
function foo() {
// ..
}
// #endregion snippet

export default foo
3 changes: 3 additions & 0 deletions docs/snippets/snippet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function () {
// ..
}
147 changes: 139 additions & 8 deletions src/node/markdown/plugins/snippet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,83 @@
import fs from 'fs'
import path from 'path'
import MarkdownIt from 'markdown-it'
import { RuleBlock } from 'markdown-it/lib/parser_block'

function dedent(text: string) {
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: string,
regexp: RegExp,
regionName: string,
end: boolean = 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: Array<string>, regionName: string) {
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
}

export const snippetPlugin = (md: MarkdownIt, root: string) => {
const parser: RuleBlock = (state, startLine, endLine, silent) => {
const CH = '<'.charCodeAt(0)
Expand All @@ -24,23 +100,78 @@ export const snippetPlugin = (md: MarkdownIt, root: string) => {

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 content = fs.existsSync(filename)
? fs.readFileSync(filename).toString()
: 'Not found: ' + filename
const meta = rawPath.replace(filename, '')

/**
* raw path format: "/path/to/file.extension#region {meta}"
* where #region and {meta} are optional
*
* 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.content = content
token.info = extension + meta

// @ts-ignore
token.src = path.resolve(filename) + region
token.markup = '```'
token.map = [startLine, startLine + 1]

return true
}

const fence = md.renderer.rules.fence!

md.renderer.rules.fence = (...args) => {
const [tokens, idx, , { loader }] = args
const token = tokens[idx]
// @ts-ignore
const tokenSrc = token.src
const [src, regionName] = tokenSrc ? tokenSrc.split('#') : ['']

if (src) {
if (loader) {
loader.addDependency(src)
}
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: string) => !region.regexp.test(line.trim()))
.join('\n')
)
}
}

token.content = content
} else {
token.content = isAFile
? `Code snippet path not found: ${src}`
: `Invalid code snippet option`
token.info = ''
}
}
return fence(...args)
}

md.block.ruler.before('fence', 'snippet', parser)
}