Skip to content

Commit 78cc1be

Browse files
committed
feat(markdown): include all regions with same name
VitePress supports including a part of a file through a VS Code region. However, when multiple regions have the same name, only one is included. The workaround plugin suggested in #3690 is no longer maintained. [^1] Furthermore, it suffers from a memory leak causing huge RAM usage. [^2] While we at Fabric Docs have forked the plugin [^3] to fix some issues, it still feels fragile. Another problem is having to use two different syntaxes (`<<<` vs `@[]()`) for including files, which caused confusion. Since this feature has been requested by VitePress directly, I don't think there's any point in trying to fix the plugin any further. fix #3690 [^1]: <fabioaanthony/markdown-it-vuepress-code-snippet-enhanced#7> [^2]: <https://discord.com/channels/507304429255393322/1208846408552030238/1407245292482330695> via <https://discord.gg/v6v4pMv> [^3]: <https://github.com/IMB11/md-it-enhanced-snippets>
1 parent 6dfcdd3 commit 78cc1be

File tree

4 files changed

+74
-51
lines changed

4 files changed

+74
-51
lines changed

docs/en/guide/markdown.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -656,12 +656,12 @@ The value of `@` corresponds to the source root. By default it's the VitePress p
656656

657657
:::
658658

659-
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:
659+
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, and all regions with that name will be imported:
660660

661661
**Input**
662662

663663
```md
664-
<<< @/snippets/snippet-with-region.js#snippet{1}
664+
<<< @/snippets/snippet-with-region.js#snippet{2,5}
665665
```
666666

667667
**Code file**
@@ -670,7 +670,7 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
670670

671671
**Output**
672672

673-
<<< @/snippets/snippet-with-region.js#snippet{1}
673+
<<< @/snippets/snippet-with-region.js#snippet{2,5}
674674

675675
You can also specify the language inside the braces (`{}`) like this:
676676

@@ -856,7 +856,7 @@ Can be created using `.foorc.json`.
856856

857857
The format of the selected line range can be: `{3,}`, `{,10}`, `{1,10}`
858858

859-
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:
859+
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, and all regions with that name will be included:
860860

861861
**Input**
862862

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
// #region snippet
22
function foo() {
3-
// ..
3+
console.log('foo')
44
}
55
// #endregion snippet
66

7-
export default foo
7+
console.log('this line is not in #region snippet!')
8+
9+
// #region snippet
10+
function bar() {
11+
console.log('bar')
12+
}
13+
// #endregion snippet
14+
15+
export { bar, foo }

src/node/markdown/plugins/snippet.ts

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -86,37 +86,44 @@ const markers = [
8686
}
8787
]
8888

89-
export function findRegion(lines: Array<string>, regionName: string) {
90-
let chosen: { re: (typeof markers)[number]; start: number } | null = null
91-
// find the regex pair for a start marker that matches the given region name
92-
for (let i = 0; i < lines.length; i++) {
93-
for (const re of markers) {
94-
if (re.start.exec(lines[i])?.[1] === regionName) {
95-
chosen = { re, start: i + 1 }
96-
break
89+
export function findRegions(lines: Array<string>, regionName: string) {
90+
const returned: {
91+
re: (typeof markers)[number]
92+
start: number
93+
end: number
94+
}[] = []
95+
96+
for (const re of markers) {
97+
let nestedCounter = 0
98+
let start: number | null = null
99+
100+
for (let i = 0; i < lines.length; i++) {
101+
// find region start
102+
const startMatch = re.start.exec(lines[i])
103+
if (startMatch?.[1] === regionName) {
104+
if (nestedCounter === 0) start = i + 1
105+
nestedCounter++
106+
continue
107+
}
108+
109+
if (nestedCounter === 0) continue
110+
111+
// find region end
112+
const endMatch = re.end.exec(lines[i])
113+
if (endMatch?.[1] === regionName || endMatch?.[1] === '') {
114+
nestedCounter--
115+
// if all nested regions ended
116+
if (nestedCounter === 0 && start != null) {
117+
returned.push({ re, start, end: i })
118+
start = null
119+
}
97120
}
98121
}
99-
if (chosen) break
100-
}
101-
if (!chosen) return null
102-
103-
let counter = 1
104-
// scan the rest of the lines to find the matching end marker, handling nested markers
105-
for (let i = chosen.start; i < lines.length; i++) {
106-
// check for an inner start marker for the same region
107-
if (chosen.re.start.exec(lines[i])?.[1] === regionName) {
108-
counter++
109-
continue
110-
}
111-
// check for an end marker for the same region
112-
const endRegion = chosen.re.end.exec(lines[i])?.[1]
113-
// allow empty region name on the end marker as a fallback
114-
if (endRegion === regionName || endRegion === '') {
115-
if (--counter === 0) return { ...chosen, end: i }
116-
}
122+
123+
if (returned.length > 0) break
117124
}
118125

119-
return null
126+
return returned
120127
}
121128

122129
export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => {
@@ -183,11 +190,14 @@ export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => {
183190
includes.push(src)
184191
}
185192

186-
const isAFile = fs.statSync(src).isFile()
187-
if (!fs.existsSync(src) || !isAFile) {
188-
token.content = isAFile
189-
? `Code snippet path not found: ${src}`
190-
: `Invalid code snippet option`
193+
if (!fs.existsSync(src)) {
194+
token.content = `Code snippet path not found: ${src}`
195+
token.info = ''
196+
return fence(...args)
197+
}
198+
199+
if (!fs.statSync(src).isFile()) {
200+
token.content = `Invalid code snippet option`
191201
token.info = ''
192202
return fence(...args)
193203
}
@@ -196,13 +206,16 @@ export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => {
196206

197207
if (regionName) {
198208
const lines = content.split('\n')
199-
const region = findRegion(lines, regionName)
209+
const regions = findRegions(lines, regionName)
200210

201-
if (region) {
211+
if (regions.length > 0) {
202212
content = dedent(
203-
lines
204-
.slice(region.start, region.end)
205-
.filter((l) => !(region.re.start.test(l) || region.re.end.test(l)))
213+
regions
214+
.flatMap((r) =>
215+
lines
216+
.slice(r.start, r.end)
217+
.filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
218+
)
206219
.join('\n')
207220
)
208221
}

src/node/utils/processIncludes.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import matter from 'gray-matter'
33
import type { MarkdownItAsync } from 'markdown-it-async'
44
import path from 'node:path'
55
import c from 'picocolors'
6-
import { findRegion } from '../markdown/plugins/snippet'
6+
import { findRegions } from '../markdown/plugins/snippet'
77
import { slash, type MarkdownEnv } from '../shared'
88

99
export function processIncludes(
@@ -42,9 +42,9 @@ export function processIncludes(
4242
if (region) {
4343
const [regionName] = region
4444
const lines = content.split(/\r?\n/)
45-
let { start, end } = findRegion(lines, regionName.slice(1)) ?? {}
45+
let regions = findRegions(lines, regionName.slice(1))
4646

47-
if (start === undefined) {
47+
if (regions.length === 0) {
4848
// region not found, it might be a header
4949
const tokens = md
5050
.parse(content, {
@@ -58,18 +58,22 @@ export function processIncludes(
5858
)
5959
const token = tokens[idx]
6060
if (token) {
61-
start = token.map![1]
61+
const start = token.map![1]
6262
const level = parseInt(token.tag.slice(1))
63+
let end = undefined
6364
for (let i = idx + 1; i < tokens.length; i++) {
6465
if (parseInt(tokens[i].tag.slice(1)) <= level) {
6566
end = tokens[i].map![0]
6667
break
6768
}
6869
}
70+
regions.push({ start, end } as any)
6971
}
7072
}
7173

72-
content = lines.slice(start, end).join('\n')
74+
content = regions
75+
.flatMap((region) => lines.slice(region.start, region.end))
76+
.join('\n')
7377
}
7478

7579
if (range) {
@@ -97,11 +101,9 @@ export function processIncludes(
97101
includes,
98102
cleanUrls
99103
)
100-
101-
//
102104
} catch (error) {
103105
if (process.env.DEBUG) {
104-
process.stderr.write(c.yellow(`\nInclude file not found: ${m1}`))
106+
process.stderr.write(c.yellow(`Include file not found: ${m1}\n`))
105107
}
106108

107109
return m // silently ignore error if file is not present

0 commit comments

Comments
 (0)