fix(md)!: include all regions with the 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 doing the same thing, causing confusion.

Since this feature has been requested by VitePress directly, I don't
think there's any point in working on the plugin any further.

fix #3690

[^1]: <https://github.com/fabioaanthony/markdown-it-vuepress-code-snippet-enhanced/issues/7>
[^2]: <https://discord.com/channels/507304429255393322/1208846408552030238/1407245292482330695> via <https://discord.fabricmc.net/>
[^3]: <https://github.com/IMB11/md-it-enhanced-snippets>
pull/5014/head
Miroma 3 months ago
parent 19357f9d33
commit cb83272074
No known key found for this signature in database

@ -1,6 +1,6 @@
import {
dedent,
findRegion,
findRegions,
rawPathToToken
} from 'node/markdown/plugins/snippet'
@ -38,6 +38,7 @@ const rawPathTokenMap: [string, Partial<{ filepath: string, extension: string, t
['./path/to/file#region {1,2,4-6 c#}', { filepath: './path/to/file', title: 'file', region: '#region', lines: '1,2,4-6', lang: 'c#' }],
['/path to/file {1,2,4-6 c#} [title]', { filepath: '/path to/file', title: 'title', lines: '1,2,4-6', lang: 'c#' }],
['./path to/file#region {1,2,4-6 c#} [title]', { filepath: './path to/file', title: 'title', region: '#region', lines: '1,2,4-6', lang: 'c#' }],
['./path/to/file {C++}', { filepath: './path/to/file', title: 'file', lang: 'C++' }],
]
describe('node/markdown/plugins/snippet', () => {
@ -105,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', () => {
@ -116,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', () => {
@ -144,13 +150,17 @@ describe('node/markdown/plugins/snippet', () => {
'#endregion hello',
'Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");'
)
}
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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('Console.WriteLine("Hello, World!");')
})
it('detects region markers even when the end marker omits the region name', () => {
@ -161,13 +171,17 @@ describe('node/markdown/plugins/snippet', () => {
'#endregion',
'Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");'
)
}
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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('Console.WriteLine("Hello, World!");')
})
it('handles indented region markers correctly', () => {
@ -178,125 +192,167 @@ describe('node/markdown/plugins/snippet', () => {
' #endregion hello',
' Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Hello, World!");'
)
}
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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(' 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()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'let start = -1;'
)
}
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' padding-left: 15px;'
)
}
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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(' padding-left: 15px;')
})
it('detects HTML style region markers', () => {
const lines = [
'<div>Some content</div>',
'<!-- #region foo -->',
'<!-- #region hello -->',
' <h1>Hello world</h1>',
'<!-- #endregion foo -->',
'<!-- #endregion hello -->',
'<div>Other content</div>'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' <h1>Hello world</h1>'
)
}
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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(' <h1>Hello world</h1>')
})
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()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Inside region")'
)
}
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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(' 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()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'echo off'
)
}
const lines = ['::#region hello', '@ECHO OFF', 'REM #endregion hello']
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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('@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()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'int a = 1;'
)
}
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(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()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'first region content'
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(3)
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)
})
it('handles nested regions with different names properly', () => {
@ -308,18 +364,38 @@ describe('node/markdown/plugins/snippet', () => {
'// #endregion bar',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
const extracted = lines.slice(result.start, result.end).join('\n')
const expected = [
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar'
].join('\n')
expect(extracted).toBe(expected)
}
const result = findRegions(lines, 'foo')
expect(result).toHaveLength(1)
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');",
"console.log('nested content');"
].join('\n')
expect(extracted).toBe(expected)
})
it('handles region names with hyphens and special characters', () => {
const lines = [
'// #region complex-name_123',
'const x = 1;',
'// #endregion complex-name_123'
]
const result = findRegions(lines, 'complex-name_123')
expect(result).toHaveLength(1)
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')
expect(extracted).toBe('const x = 1;')
})
})
})

@ -660,12 +660,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**
@ -674,7 +674,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:
@ -860,7 +860,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**

@ -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 }

@ -86,37 +86,44 @@ const markers = [
}
]
export function findRegion(lines: Array<string>, 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) => {
@ -175,7 +182,7 @@ export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => {
const [tokens, idx, , { includes }] = args
const token = tokens[idx]
// @ts-ignore
const [src, regionName] = token.src ?? []
const [src, region] = token.src ?? []
if (!src) return fence(...args)
@ -183,26 +190,32 @@ 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)
}
let content = fs.readFileSync(src, 'utf8').replace(/\r\n/g, '\n')
if (regionName) {
if (region) {
const lines = content.split('\n')
const region = findRegion(lines, regionName)
const regions = findRegions(lines, region)
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')
)
}

@ -2,7 +2,7 @@ import matter from 'gray-matter'
import type { MarkdownItAsync } from 'markdown-it-async'
import fs from 'node:fs'
import path from 'node:path'
import { findRegion } from '../markdown/plugins/snippet'
import { findRegions } from '../markdown/plugins/snippet'
import { slash, type MarkdownEnv } from '../shared'
export function processIncludes(
@ -41,9 +41,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, {
@ -57,18 +57,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) {

Loading…
Cancel
Save