diff --git a/__tests__/unit/node/markdown/plugins/snippet.test.ts b/__tests__/unit/node/markdown/plugins/snippet.test.ts
index aa940784a0f3..c285c833dd79 100644
--- a/__tests__/unit/node/markdown/plugins/snippet.test.ts
+++ b/__tests__/unit/node/markdown/plugins/snippet.test.ts
@@ -1,6 +1,6 @@
import {
dedent,
- findRegion,
+ findRegions,
rawPathToToken
} from 'node/markdown/plugins/snippet'
import { expect } from 'vitest'
@@ -106,9 +106,14 @@ describe('node/markdown/plugins/snippet', () => {
})
describe('findRegion', () => {
- it('returns null when no region markers are present', () => {
- const lines = ['function foo() {', ' console.log("hello");', '}']
- expect(findRegion(lines, 'foo')).toBeNull()
+ it('returns empty array when no region markers are present', () => {
+ const lines = [
+ 'function foo() {',
+ ' console.log("hello");',
+ ' return "foo";',
+ '}'
+ ]
+ expect(findRegions(lines, 'foo')).toHaveLength(0)
})
it('ignores non-matching region names', () => {
@@ -117,24 +122,24 @@ describe('node/markdown/plugins/snippet', () => {
'some code here',
'// #endregion regionA'
]
- expect(findRegion(lines, 'regionC')).toBeNull()
+ expect(findRegions(lines, 'regionC')).toHaveLength(0)
})
- it('returns null if a region start marker exists without a matching end marker', () => {
+ it('returns empty array if a region start marker exists without a matching end marker', () => {
const lines = [
'// #region missingEnd',
'console.log("inside region");',
'console.log("still inside");'
]
- expect(findRegion(lines, 'missingEnd')).toBeNull()
+ expect(findRegions(lines, 'missingEnd')).toHaveLength(0)
})
- it('returns null if an end marker exists without a preceding start marker', () => {
+ it('returns empty array if an end marker exists without a preceding start marker', () => {
const lines = [
'// #endregion ghostRegion',
'console.log("stray end marker");'
]
- expect(findRegion(lines, 'ghostRegion')).toBeNull()
+ expect(findRegions(lines, 'ghostRegion')).toHaveLength(0)
})
it('detects C#/JavaScript style region markers with matching tags', () => {
@@ -145,12 +150,18 @@ describe('node/markdown/plugins/snippet', () => {
'#endregion hello',
'Console.WriteLine("After region");'
]
- const result = findRegion(lines, 'hello')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- 'Console.WriteLine("Hello, World!");'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe('Console.WriteLine("Hello, World!");')
}
})
@@ -162,12 +173,18 @@ describe('node/markdown/plugins/snippet', () => {
'#endregion',
'Console.WriteLine("After region");'
]
- const result = findRegion(lines, 'hello')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- 'Console.WriteLine("Hello, World!");'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe('Console.WriteLine("Hello, World!");')
}
})
@@ -179,124 +196,182 @@ describe('node/markdown/plugins/snippet', () => {
' #endregion hello',
' Console.WriteLine("After region");'
]
- const result = findRegion(lines, 'hello')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- ' Console.WriteLine("Hello, World!");'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe(' Console.WriteLine("Hello, World!");')
}
})
it('detects TypeScript style region markers', () => {
const lines = [
'let regexp: RegExp[] = [];',
- '// #region foo',
+ '// #region hello',
'let start = -1;',
- '// #endregion foo'
+ '// #endregion hello'
]
- const result = findRegion(lines, 'foo')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- 'let start = -1;'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe('let start = -1;')
}
})
it('detects CSS style region markers', () => {
const lines = [
'.body-content {',
- '/* #region foo */',
+ '/* #region hello */',
' padding-left: 15px;',
- '/* #endregion foo */',
+ '/* #endregion hello */',
' padding-right: 15px;',
'}'
]
- const result = findRegion(lines, 'foo')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- ' padding-left: 15px;'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe(' padding-left: 15px;')
}
})
it('detects HTML style region markers', () => {
const lines = [
'
Some content
',
- '',
+ '',
' Hello world
',
- '',
+ '',
'Other content
'
]
- const result = findRegion(lines, 'foo')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- ' Hello world
'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe(' Hello world
')
}
})
it('detects Visual Basic style region markers (with case-insensitive "End")', () => {
const lines = [
'Console.WriteLine("VB")',
- '#Region VBRegion',
+ '#Region hello',
' Console.WriteLine("Inside region")',
- '#End Region VBRegion',
+ '#End Region hello',
'Console.WriteLine("Done")'
]
- const result = findRegion(lines, 'VBRegion')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- ' Console.WriteLine("Inside region")'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe(' Console.WriteLine("Inside region")')
}
})
it('detects Bat style region markers', () => {
- const lines = ['::#region foo', 'echo off', '::#endregion foo']
- const result = findRegion(lines, 'foo')
- expect(result).not.toBeNull()
+ const lines = ['::#region hello', '@ECHO OFF', 'REM #endregion hello']
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- 'echo off'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe('@ECHO OFF')
}
})
it('detects C/C++ style region markers using #pragma', () => {
const lines = [
- '#pragma region foo',
+ '#pragma region hello',
'int a = 1;',
- '#pragma endregion foo'
+ '#pragma endregion hello'
]
- const result = findRegion(lines, 'foo')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(1)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- 'int a = 1;'
- )
+ expect(
+ result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ ).toBe('int a = 1;')
}
})
- it('returns the first complete region when multiple regions exist', () => {
+ it('returns all regions with the same name when multiple exist', () => {
const lines = [
- '// #region foo',
+ '// #region hello',
'first region content',
- '// #endregion foo',
- '// #region foo',
+ '// #endregion hello',
+ 'between regions content',
+ '// #region hello',
'second region content',
- '// #endregion foo'
+ '// #endregion',
+ 'between regions content',
+ '// #region hello',
+ 'third region content',
+ '// #endregion hello',
+ 'below regions content'
]
- const result = findRegion(lines, 'foo')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'hello')
+ expect(result).toHaveLength(3)
if (result) {
- expect(lines.slice(result.start, result.end).join('\n')).toBe(
- 'first region content'
- )
+ const extracted = result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
+ const expected = [
+ 'first region content',
+ 'second region content',
+ 'third region content'
+ ].join('\n')
+ expect(extracted).toBe(expected)
}
})
@@ -309,15 +384,19 @@ describe('node/markdown/plugins/snippet', () => {
'// #endregion bar',
'// #endregion foo'
]
- const result = findRegion(lines, 'foo')
- expect(result).not.toBeNull()
+ const result = findRegions(lines, 'foo')
+ expect(result).toHaveLength(1)
if (result) {
- const extracted = lines.slice(result.start, result.end).join('\n')
+ const extracted = result
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
+ .join('\n')
const expected = [
"console.log('line before nested');",
- '// #region bar',
- "console.log('nested content');",
- '// #endregion bar'
+ "console.log('nested content');"
].join('\n')
expect(extracted).toBe(expected)
}
diff --git a/docs/en/guide/markdown.md b/docs/en/guide/markdown.md
index e1be3e6b366f..7ccf94d4fae3 100644
--- a/docs/en/guide/markdown.md
+++ b/docs/en/guide/markdown.md
@@ -656,12 +656,12 @@ The value of `@` corresponds to the source root. By default it's the VitePress p
:::
-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:
+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:
**Input**
```md
-<<< @/snippets/snippet-with-region.js#snippet{1}
+<<< @/snippets/snippet-with-region.js#snippet{2,5}
```
**Code file**
@@ -670,7 +670,7 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
**Output**
-<<< @/snippets/snippet-with-region.js#snippet{1}
+<<< @/snippets/snippet-with-region.js#snippet{2,5}
You can also specify the language inside the braces (`{}`) like this:
@@ -856,7 +856,7 @@ Can be created using `.foorc.json`.
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:
+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:
**Input**
diff --git a/docs/snippets/snippet-with-region.js b/docs/snippets/snippet-with-region.js
index 9c7faaebca19..fd0878627433 100644
--- a/docs/snippets/snippet-with-region.js
+++ b/docs/snippets/snippet-with-region.js
@@ -1,7 +1,15 @@
// #region snippet
function foo() {
- // ..
+ console.log('foo')
}
// #endregion snippet
-export default foo
+console.log('this line is not in #region snippet!')
+
+// #region snippet
+function bar() {
+ console.log('bar')
+}
+// #endregion snippet
+
+export { bar, foo }
diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts
index 71e5e76c9c16..756da390d76e 100644
--- a/src/node/markdown/plugins/snippet.ts
+++ b/src/node/markdown/plugins/snippet.ts
@@ -86,37 +86,44 @@ const markers = [
}
]
-export function findRegion(lines: Array, regionName: string) {
- let chosen: { re: (typeof markers)[number]; start: number } | null = null
- // find the regex pair for a start marker that matches the given region name
- for (let i = 0; i < lines.length; i++) {
- for (const re of markers) {
- if (re.start.exec(lines[i])?.[1] === regionName) {
- chosen = { re, start: i + 1 }
- break
+export function findRegions(lines: string[], regionName: string) {
+ const returned: {
+ re: (typeof markers)[number]
+ start: number
+ end: number
+ }[] = []
+
+ for (const re of markers) {
+ let nestedCounter = 0
+ let start: number | null = null
+
+ for (let i = 0; i < lines.length; i++) {
+ // find region start
+ const startMatch = re.start.exec(lines[i])
+ if (startMatch?.[1] === regionName) {
+ if (nestedCounter === 0) start = i + 1
+ nestedCounter++
+ continue
+ }
+
+ if (nestedCounter === 0) continue
+
+ // find region end
+ const endMatch = re.end.exec(lines[i])
+ if (endMatch?.[1] === regionName || endMatch?.[1] === '') {
+ nestedCounter--
+ // if all nested regions ended
+ if (nestedCounter === 0 && start != null) {
+ returned.push({ re, start, end: i })
+ start = null
+ }
}
}
- if (chosen) break
- }
- if (!chosen) return null
-
- let counter = 1
- // scan the rest of the lines to find the matching end marker, handling nested markers
- for (let i = chosen.start; i < lines.length; i++) {
- // check for an inner start marker for the same region
- if (chosen.re.start.exec(lines[i])?.[1] === regionName) {
- counter++
- continue
- }
- // check for an end marker for the same region
- const endRegion = chosen.re.end.exec(lines[i])?.[1]
- // allow empty region name on the end marker as a fallback
- if (endRegion === regionName || endRegion === '') {
- if (--counter === 0) return { ...chosen, end: i }
- }
+
+ if (returned.length > 0) break
}
- return null
+ return returned
}
export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => {
@@ -183,11 +190,14 @@ export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => {
includes.push(src)
}
- const isAFile = fs.statSync(src).isFile()
- if (!fs.existsSync(src) || !isAFile) {
- token.content = isAFile
- ? `Code snippet path not found: ${src}`
- : `Invalid code snippet option`
+ if (!fs.existsSync(src)) {
+ token.content = `Code snippet path not found: ${src}`
+ token.info = ''
+ return fence(...args)
+ }
+
+ if (!fs.statSync(src).isFile()) {
+ token.content = `Invalid code snippet option`
token.info = ''
return fence(...args)
}
@@ -196,13 +206,16 @@ export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => {
if (regionName) {
const lines = content.split('\n')
- const region = findRegion(lines, regionName)
+ const regions = findRegions(lines, regionName)
- if (region) {
+ if (regions.length > 0) {
content = dedent(
- lines
- .slice(region.start, region.end)
- .filter((l) => !(region.re.start.test(l) || region.re.end.test(l)))
+ regions
+ .flatMap((r) =>
+ lines
+ .slice(r.start, r.end)
+ .filter((l) => !(r.re.start.test(l) || r.re.end.test(l)))
+ )
.join('\n')
)
}
diff --git a/src/node/utils/processIncludes.ts b/src/node/utils/processIncludes.ts
index aa98ccbd5c9e..c13c2809573f 100644
--- a/src/node/utils/processIncludes.ts
+++ b/src/node/utils/processIncludes.ts
@@ -3,7 +3,7 @@ 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 { findRegions } from '../markdown/plugins/snippet'
import { slash, type MarkdownEnv } from '../shared'
export function processIncludes(
@@ -42,9 +42,9 @@ export function processIncludes(
if (region) {
const [regionName] = region
const lines = content.split(/\r?\n/)
- let { start, end } = findRegion(lines, regionName.slice(1)) ?? {}
+ let regions = findRegions(lines, regionName.slice(1))
- if (start === undefined) {
+ if (regions.length === 0) {
// region not found, it might be a header
const tokens = md
.parse(content, {
@@ -58,18 +58,22 @@ export function processIncludes(
)
const token = tokens[idx]
if (token) {
- start = token.map![1]
+ const start = token.map![1]
const level = parseInt(token.tag.slice(1))
+ let end = undefined
for (let i = idx + 1; i < tokens.length; i++) {
if (parseInt(tokens[i].tag.slice(1)) <= level) {
end = tokens[i].map![0]
break
}
}
+ regions.push({ start, end } as any)
}
}
- content = lines.slice(start, end).join('\n')
+ content = regions
+ .flatMap((region) => lines.slice(region.start, region.end))
+ .join('\n')
}
if (range) {
@@ -97,11 +101,9 @@ export function processIncludes(
includes,
cleanUrls
)
-
- //
} catch (error) {
if (process.env.DEBUG) {
- process.stderr.write(c.yellow(`\nInclude file not found: ${m1}`))
+ process.stderr.write(c.yellow(`Include file not found: ${m1}\n`))
}
return m // silently ignore error if file is not present