pull/5014/merge
Miroma 5 days ago committed by GitHub
commit e4a3b137f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 = [
'<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()
const result = findRegions(lines, 'hello')
expect(result).toHaveLength(1)
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' <h1>Hello world</h1>'
)
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()
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)
}

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

@ -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) => {
@ -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')
)
}

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

Loading…
Cancel
Save