feat(markdown): support including specific regions from markdown files (#3978)

Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com>
pull/3999/head
SuperLi 9 months ago committed by GitHub
parent b694900e69
commit 143b1e91f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -197,6 +197,22 @@ export default config
<!--@include: ./foo.md{6,}--> <!--@include: ./foo.md{6,}-->
## Markdown At File Region Snippet
<!--@include: ./region-include.md#snippet-->
## Markdown At File Range Region Snippet
<!--@include: ./region-include.md#range-region{3,4}-->
## Markdown At File Range Region Snippet without start
<!--@include: ./region-include.md#range-region{,2}-->
## Markdown At File Range Region Snippet without end
<!--@include: ./region-include.md#range-region{5,}-->
## Image Lazy Loading ## Image Lazy Loading
![vitepress logo](/vitepress.png) ![vitepress logo](/vitepress.png)

@ -65,7 +65,7 @@ describe('Table of Contents', () => {
test('render toc', async () => { test('render toc', async () => {
const items = page.locator('#table-of-contents + nav ul li') const items = page.locator('#table-of-contents + nav ul li')
const count = await items.count() const count = await items.count()
expect(count).toBe(36) expect(count).toBe(44)
}) })
}) })
@ -275,6 +275,26 @@ describe('Markdown File Inclusion', () => {
expect(trim(await p.nth(1).textContent())).toBe('This is after region') expect(trim(await p.nth(1).textContent())).toBe('This is after region')
}) })
test('support markdown region snippet', async () => {
const h2 = page.locator('#markdown-at-file-region-snippet + h2')
expect(await h2.getAttribute('id')).toBe('region-snippet')
const line = page.locator('#markdown-at-file-range-region-snippet + h2')
expect(await line.getAttribute('id')).toBe('range-region-line-2')
const lineWithoutStart = page.locator(
'#markdown-at-file-range-region-snippet-without-start + h2'
)
expect(await lineWithoutStart.getAttribute('id')).toBe(
'range-region-line-1'
)
const lineWithoutEnd = page.locator(
'#markdown-at-file-range-region-snippet-without-end + h2'
)
expect(await lineWithoutEnd.getAttribute('id')).toBe('range-region-line-3')
})
test('ignore frontmatter if range is not specified', async () => { test('ignore frontmatter if range is not specified', async () => {
const p = page.locator('.vp-doc') const p = page.locator('.vp-doc')
expect(await p.textContent()).not.toContain('title') expect(await p.textContent()).not.toContain('title')

@ -0,0 +1,13 @@
<!-- #region range-region -->
## Range Region Line 1
## Range Region Line 2
## Range Region Line 3
<!-- #endregion range-region -->
<!-- #region snippet -->
## Region Snippet
<!-- #endregion snippet -->

@ -834,6 +834,43 @@ Can be created using `.foorc.json`.
The format of the selected line range can be: `{3,}`, `{,10}`, `{1,10}` The format of the selected line range can be: `{3,}`, `{,10}`, `{1,10}`
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:
**Input**
```md
# Docs
## Basics
<!--@include: ./parts/basics.md#basic-usage{,2}-->
<!--@include: ./parts/basics.md#basic-usage{5,}-->
```
**Part file** (`parts/basics.md`)
```md
<!-- #region basic-usage -->
## Usage Line 1
## Usage Line 2
## Usage Line 3
<!-- #endregion basic-usage -->
```
**Equivalent code**
```md
# Docs
## Basics
## Usage Line 1
## Usage Line 3
```
::: warning ::: warning
Note that this does not throw errors if your file is not present. Hence, when using this feature make sure that the contents are being rendered as expected. Note that this does not throw errors if your file is not present. Hence, when using this feature make sure that the contents are being rendered as expected.
::: :::

@ -66,7 +66,7 @@ function testLine(
) )
} }
function findRegion(lines: Array<string>, regionName: string) { export function findRegion(lines: Array<string>, regionName: string) {
const regionRegexps = [ const regionRegexps = [
/^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java /^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java
/^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss /^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss

@ -1,6 +1,8 @@
import fs from 'fs-extra' import fs from 'fs-extra'
import matter from 'gray-matter' import matter from 'gray-matter'
import path from 'path' import path from 'path'
import c from 'picocolors'
import { findRegion } from '../markdown/plugins/snippet'
import { slash } from '../shared' import { slash } from '../shared'
export function processIncludes( export function processIncludes(
@ -10,18 +12,37 @@ export function processIncludes(
includes: string[] includes: string[]
): string { ): string {
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
const regionRE = /(#[\w-]+)/
const rangeRE = /\{(\d*),(\d*)\}$/ const rangeRE = /\{(\d*),(\d*)\}$/
return src.replace(includesRE, (m: string, m1: string) => { return src.replace(includesRE, (m: string, m1: string) => {
if (!m1.length) return m if (!m1.length) return m
const range = m1.match(rangeRE) const range = m1.match(rangeRE)
range && (m1 = m1.slice(0, -range[0].length)) const region = m1.match(regionRE)
const hasMeta = !!(region || range)
if (hasMeta) {
const len = (region?.[0].length || 0) + (range?.[0].length || 0)
m1 = m1.slice(0, -len) // remove meta info from the include path
}
const atPresent = m1[0] === '@' const atPresent = m1[0] === '@'
try { try {
const includePath = atPresent const includePath = atPresent
? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1)) ? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1))
: path.join(path.dirname(file), m1) : path.join(path.dirname(file), m1)
let content = fs.readFileSync(includePath, 'utf-8') let content = fs.readFileSync(includePath, 'utf-8')
if (region) {
const [regionName] = region
const lines = content.split(/\r?\n/)
const regionLines = findRegion(lines, regionName.slice(1))
content = lines.slice(regionLines?.start, regionLines?.end).join('\n')
}
if (range) { if (range) {
const [, startLine, endLine] = range const [, startLine, endLine] = range
const lines = content.split(/\r?\n/) const lines = content.split(/\r?\n/)
@ -31,13 +52,22 @@ export function processIncludes(
endLine ? parseInt(endLine, 10) : undefined endLine ? parseInt(endLine, 10) : undefined
) )
.join('\n') .join('\n')
} else { }
if (!hasMeta && path.extname(includePath) === '.md') {
content = matter(content).content content = matter(content).content
} }
includes.push(slash(includePath)) includes.push(slash(includePath))
// recursively process includes in the content // recursively process includes in the content
return processIncludes(srcDir, content, includePath, includes) return processIncludes(srcDir, content, includePath, includes)
//
} catch (error) { } catch (error) {
if (process.env.DEBUG) {
process.stderr.write(c.yellow(`\nInclude file not found: ${m1}`))
}
return m // silently ignore error if file is not present return m // silently ignore error if file is not present
} }
}) })

Loading…
Cancel
Save