Skip to content

Commit

Permalink
feat: support using header anchors in markdown file inclusion (#4608)
Browse files Browse the repository at this point in the history
closes #4375
closes #4382

Co-authored-by: btea <2356281422@qq.com>
  • Loading branch information
brc-dd and btea authored Mar 9, 2025
1 parent 8aad617 commit b99d512
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 13 deletions.
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
47 changes: 47 additions & 0 deletions docs/en/guide/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,53 @@ 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.
```

Here, `my-base-section` is the generated id of the heading element. In case it's not easily guessable, you can open the part file in your browser and click on the heading anchor (`#` symbol left to the heading when hovered) to see the id in the URL bar. Or use browser dev tools to inspect the element. Alternatively, you can also specify the id to the part file like this:

```md
## My Base Section {#custom-id}
```

and include it like this:

```md
<!--@include: ./parts/basics.md#custom-id-->
```

## 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

0 comments on commit b99d512

Please sign in to comment.