Skip to content

Commit b99d512

Browse files
brc-ddbtea
andauthored
feat: support using header anchors in markdown file inclusion (#4608)
closes #4375 closes #4382 Co-authored-by: btea <[email protected]>
1 parent 8aad617 commit b99d512

File tree

7 files changed

+180
-13
lines changed

7 files changed

+180
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# header 1
2+
3+
header 1 content
4+
5+
## header 1.1
6+
7+
header 1.1 content
8+
9+
### header 1.1.1
10+
11+
header 1.1.1 content
12+
13+
### header 1.1.2
14+
15+
header 1.1.2 content
16+
17+
## header 1.2
18+
19+
header 1.2 content
20+
21+
### header 1.2.1
22+
23+
header 1.2.1 content
24+
25+
### header 1.2.2
26+
27+
header 1.2.2 content

__tests__/e2e/markdown-extensions/index.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ export default config
213213

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

216+
## Markdown File Inclusion with Header
217+
218+
<!--@include: ./header-include.md#header-1-1-->
219+
216220
## Image Lazy Loading
217221

218-
![vitepress logo](/vitepress.png)
222+
![vitepress logo](/vitepress.png)

__tests__/e2e/markdown-extensions/markdown-extensions.test.ts

+55-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,61 @@ describe('Emoji', () => {
6464
describe('Table of Contents', () => {
6565
test('render toc', async () => {
6666
const items = page.locator('#table-of-contents + nav ul li')
67-
const count = await items.count()
68-
expect(count).toBe(44)
67+
expect(
68+
await items.evaluateAll((elements) =>
69+
elements.map((el) => el.childNodes[0].textContent)
70+
)
71+
).toMatchInlineSnapshot(`
72+
[
73+
"Links",
74+
"Internal Links",
75+
"External Links",
76+
"GitHub-Style Tables",
77+
"Emoji",
78+
"Table of Contents",
79+
"Custom Containers",
80+
"Default Title",
81+
"Custom Title",
82+
"Line Highlighting in Code Blocks",
83+
"Single Line",
84+
"Multiple single lines, ranges",
85+
"Comment Highlight",
86+
"Line Numbers",
87+
"Import Code Snippets",
88+
"Basic Code Snippet",
89+
"Specify Region",
90+
"With Other Features",
91+
"Code Groups",
92+
"Basic Code Group",
93+
"With Other Features",
94+
"Markdown File Inclusion",
95+
"Region",
96+
"Markdown At File Inclusion",
97+
"Markdown Nested File Inclusion",
98+
"Region",
99+
"After Foo",
100+
"Sub sub",
101+
"Sub sub sub",
102+
"Markdown File Inclusion with Range",
103+
"Region",
104+
"Markdown File Inclusion with Range without Start",
105+
"Region",
106+
"Markdown File Inclusion with Range without End",
107+
"Region",
108+
"Markdown At File Region Snippet",
109+
"Region Snippet",
110+
"Markdown At File Range Region Snippet",
111+
"Range Region Line 2",
112+
"Markdown At File Range Region Snippet without start",
113+
"Range Region Line 1",
114+
"Markdown At File Range Region Snippet without end",
115+
"Range Region Line 3",
116+
"Markdown File Inclusion with Header",
117+
"header 1.1.1",
118+
"header 1.1.2",
119+
"Image Lazy Loading",
120+
]
121+
`)
69122
})
70123
})
71124

docs/en/guide/markdown.md

+47
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,53 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
897897
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.
898898
:::
899899

900+
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:
901+
902+
```md
903+
## My Base Section
904+
905+
Some content here.
906+
907+
### My Sub Section
908+
909+
Some more content here.
910+
911+
## Another Section
912+
913+
Content outside `My Base Section`.
914+
```
915+
916+
You can include the `My Base Section` section like this:
917+
918+
```md
919+
## My Extended Section
920+
<!--@include: ./parts/basics.md#my-base-section-->
921+
```
922+
923+
**Equivalent code**
924+
925+
```md
926+
## My Extended Section
927+
928+
Some content here.
929+
930+
### My Sub Section
931+
932+
Some more content here.
933+
```
934+
935+
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:
936+
937+
```md
938+
## My Base Section {#custom-id}
939+
```
940+
941+
and include it like this:
942+
943+
```md
944+
<!--@include: ./parts/basics.md#custom-id-->
945+
```
946+
900947
## Math Equations
901948

902949
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:

src/node/markdownToVue.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export async function createMarkdownToVueRenderFn(
142142

143143
// resolve includes
144144
let includes: string[] = []
145-
src = processIncludes(srcDir, src, fileOrig, includes)
145+
src = processIncludes(md, srcDir, src, fileOrig, includes, cleanUrls)
146146

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

src/node/plugins/localSearchPlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export async function localSearchPlugin(
5656
const relativePath = slash(path.relative(srcDir, file))
5757
const env: MarkdownEnv = { path: file, relativePath, cleanUrls }
5858
const md_raw = await fs.promises.readFile(file, 'utf-8')
59-
const md_src = processIncludes(srcDir, md_raw, file, [])
59+
const md_src = processIncludes(md, srcDir, md_raw, file, [], cleanUrls)
6060
if (options._render) {
6161
return await options._render(md_src, env, md)
6262
} else {

src/node/utils/processIncludes.ts

+44-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import fs from 'fs-extra'
22
import matter from 'gray-matter'
3+
import type { MarkdownItAsync } from 'markdown-it-async'
34
import path from 'node:path'
45
import c from 'picocolors'
56
import { findRegion } from '../markdown/plugins/snippet'
6-
import { slash } from '../shared'
7+
import { slash, type MarkdownEnv } from '../shared'
78

89
export function processIncludes(
10+
md: MarkdownItAsync,
911
srcDir: string,
1012
src: string,
1113
file: string,
12-
includes: string[]
14+
includes: string[],
15+
cleanUrls: boolean
1316
): string {
1417
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
15-
const regionRE = /(#[\w-]+)/
18+
const regionRE = /(#[^\s\{]+)/
1619
const rangeRE = /\{(\d*),(\d*)\}$/
1720

1821
return src.replace(includesRE, (m: string, m1: string) => {
@@ -39,17 +42,43 @@ export function processIncludes(
3942
if (region) {
4043
const [regionName] = region
4144
const lines = content.split(/\r?\n/)
42-
const regionLines = findRegion(lines, regionName.slice(1))
43-
content = lines.slice(regionLines?.start, regionLines?.end).join('\n')
45+
let { start, end } = findRegion(lines, regionName.slice(1)) ?? {}
46+
47+
if (start === undefined) {
48+
// region not found, it might be a header
49+
const tokens = md
50+
.parse(content, {
51+
path: includePath,
52+
relativePath: slash(path.relative(srcDir, includePath)),
53+
cleanUrls
54+
} satisfies MarkdownEnv)
55+
.filter((t) => t.type === 'heading_open' && t.map)
56+
const idx = tokens.findIndex(
57+
(t) => t.attrGet('id') === regionName.slice(1)
58+
)
59+
const token = tokens[idx]
60+
if (token) {
61+
start = token.map![1]
62+
const level = parseInt(token.tag.slice(1))
63+
for (let i = idx + 1; i < tokens.length; i++) {
64+
if (parseInt(tokens[i].tag.slice(1)) <= level) {
65+
end = tokens[i].map![0]
66+
break
67+
}
68+
}
69+
}
70+
}
71+
72+
content = lines.slice(start, end).join('\n')
4473
}
4574

4675
if (range) {
4776
const [, startLine, endLine] = range
4877
const lines = content.split(/\r?\n/)
4978
content = lines
5079
.slice(
51-
startLine ? parseInt(startLine, 10) - 1 : undefined,
52-
endLine ? parseInt(endLine, 10) : undefined
80+
startLine ? parseInt(startLine) - 1 : undefined,
81+
endLine ? parseInt(endLine) : undefined
5382
)
5483
.join('\n')
5584
}
@@ -60,7 +89,14 @@ export function processIncludes(
6089

6190
includes.push(slash(includePath))
6291
// recursively process includes in the content
63-
return processIncludes(srcDir, content, includePath, includes)
92+
return processIncludes(
93+
md,
94+
srcDir,
95+
content,
96+
includePath,
97+
includes,
98+
cleanUrls
99+
)
64100

65101
//
66102
} catch (error) {

0 commit comments

Comments
 (0)