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: support using header anchors in markdown file inclusion #4608

Merged
merged 8 commits into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
27 changes: 27 additions & 0 deletions __tests__/e2e/markdown-extensions/header-include.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# header 1

header 1 content

## header 1.1

header 1.1 content

### header 1.1.1

header 1.1.1 content

### header 1.1.2

header 1.1.2 content

## header 1.2

header 1.2 content

### header 1.2.1

header 1.2.1 content

### header 1.2.2

header 1.2.2 content
6 changes: 5 additions & 1 deletion __tests__/e2e/markdown-extensions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ export default config

<!--@include: ./region-include.md#range-region{5,}-->

## Markdown File Inclusion with Header

<!--@include: ./header-include.md#header-1-1-->

## Image Lazy Loading

![vitepress logo](/vitepress.png)
![vitepress logo](/vitepress.png)
57 changes: 55 additions & 2 deletions __tests__/e2e/markdown-extensions/markdown-extensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,61 @@ describe('Emoji', () => {
describe('Table of Contents', () => {
test('render toc', async () => {
const items = page.locator('#table-of-contents + nav ul li')
const count = await items.count()
expect(count).toBe(44)
expect(
await items.evaluateAll((elements) =>
elements.map((el) => el.childNodes[0].textContent)
)
).toMatchInlineSnapshot(`
[
"Links",
"Internal Links",
"External Links",
"GitHub-Style Tables",
"Emoji",
"Table of Contents",
"Custom Containers",
"Default Title",
"Custom Title",
"Line Highlighting in Code Blocks",
"Single Line",
"Multiple single lines, ranges",
"Comment Highlight",
"Line Numbers",
"Import Code Snippets",
"Basic Code Snippet",
"Specify Region",
"With Other Features",
"Code Groups",
"Basic Code Group",
"With Other Features",
"Markdown File Inclusion",
"Region",
"Markdown At File Inclusion",
"Markdown Nested File Inclusion",
"Region",
"After Foo",
"Sub sub",
"Sub sub sub",
"Markdown File Inclusion with Range",
"Region",
"Markdown File Inclusion with Range without Start",
"Region",
"Markdown File Inclusion with Range without End",
"Region",
"Markdown At File Region Snippet",
"Region Snippet",
"Markdown At File Range Region Snippet",
"Range Region Line 2",
"Markdown At File Range Region Snippet without start",
"Range Region Line 1",
"Markdown At File Range Region Snippet without end",
"Range Region Line 3",
"Markdown File Inclusion with Header",
"header 1.1.1",
"header 1.1.2",
"Image Lazy Loading",
]
`)
})
})

Expand Down
35 changes: 35 additions & 0 deletions docs/en/guide/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,41 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
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.
:::

Instead of VS Code regions, you can also use header anchors to include a specific section of the file. For example, if you have a header in your markdown file like this:

```md
## My Base Section

Some content here.

### My Sub Section

Some more content here.

## Another Section

Content outside `My Base Section`.
```

You can include the `My Base Section` section like this:

```md
## My Extended Section
<!--@include: ./parts/basics.md#my-base-section-->
```

**Equivalent code**

```md
## My Extended Section

Some content here.

### My Sub Section

Some more content here.
```

## Math Equations

This is currently opt-in. To enable it, you need to install `markdown-it-mathjax3` and set `markdown.math` to `true` in your config file:
Expand Down
2 changes: 1 addition & 1 deletion src/node/markdownToVue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export async function createMarkdownToVueRenderFn(

// resolve includes
let includes: string[] = []
src = processIncludes(srcDir, src, fileOrig, includes)
src = processIncludes(md, srcDir, src, fileOrig, includes, cleanUrls)

const localeIndex = getLocaleForPath(siteConfig?.site, relativePath)

Expand Down
2 changes: 1 addition & 1 deletion src/node/plugins/localSearchPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function localSearchPlugin(
const relativePath = slash(path.relative(srcDir, file))
const env: MarkdownEnv = { path: file, relativePath, cleanUrls }
const md_raw = await fs.promises.readFile(file, 'utf-8')
const md_src = processIncludes(srcDir, md_raw, file, [])
const md_src = processIncludes(md, srcDir, md_raw, file, [], cleanUrls)
if (options._render) {
return await options._render(md_src, env, md)
} else {
Expand Down
52 changes: 44 additions & 8 deletions src/node/utils/processIncludes.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import fs from 'fs-extra'
import matter from 'gray-matter'
import type { MarkdownItAsync } from 'markdown-it-async'
import path from 'node:path'
import c from 'picocolors'
import { findRegion } from '../markdown/plugins/snippet'
import { slash } from '../shared'
import { slash, type MarkdownEnv } from '../shared'

export function processIncludes(
md: MarkdownItAsync,
srcDir: string,
src: string,
file: string,
includes: string[]
includes: string[],
cleanUrls: boolean
): string {
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
const regionRE = /(#[\w-]+)/
const regionRE = /(#[^\s\{]+)/
const rangeRE = /\{(\d*),(\d*)\}$/

return src.replace(includesRE, (m: string, m1: string) => {
Expand All @@ -39,17 +42,43 @@ export function processIncludes(
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')
let { start, end } = findRegion(lines, regionName.slice(1)) ?? {}

if (start === undefined) {
// region not found, it might be a header
const tokens = md
.parse(content, {
path: includePath,
relativePath: slash(path.relative(srcDir, includePath)),
cleanUrls
} satisfies MarkdownEnv)
.filter((t) => t.type === 'heading_open' && t.map)
const idx = tokens.findIndex(
(t) => t.attrGet('id') === regionName.slice(1)
)
const token = tokens[idx]
if (token) {
start = token.map![1]
const level = parseInt(token.tag.slice(1))
for (let i = idx + 1; i < tokens.length; i++) {
if (parseInt(tokens[i].tag.slice(1)) <= level) {
end = tokens[i].map![0]
break
}
}
}
}

content = lines.slice(start, end).join('\n')
}

if (range) {
const [, startLine, endLine] = range
const lines = content.split(/\r?\n/)
content = lines
.slice(
startLine ? parseInt(startLine, 10) - 1 : undefined,
endLine ? parseInt(endLine, 10) : undefined
startLine ? parseInt(startLine) - 1 : undefined,
endLine ? parseInt(endLine) : undefined
)
.join('\n')
}
Expand All @@ -60,7 +89,14 @@ export function processIncludes(

includes.push(slash(includePath))
// recursively process includes in the content
return processIncludes(srcDir, content, includePath, includes)
return processIncludes(
md,
srcDir,
content,
includePath,
includes,
cleanUrls
)

//
} catch (error) {
Expand Down