mirror of https://github.com/vuejs/vitepress
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
530 lines
15 KiB
530 lines
15 KiB
import {
|
|
dedent,
|
|
findRegions,
|
|
markers,
|
|
stripRegionMarkers
|
|
} from 'node/markdown/regions'
|
|
|
|
const extract = (lines: string[], name: string) =>
|
|
findRegions(lines, name)
|
|
.flatMap((r) => lines.slice(r.start, r.end))
|
|
.join('\n')
|
|
|
|
describe('node/markdown/regions', () => {
|
|
describe('dedent', () => {
|
|
test('keeps lines when 0-level is minimal', () => {
|
|
expect(dedent(['fn main() {', ' println!("Hello");', '}'])).toEqual([
|
|
'fn main() {',
|
|
' println!("Hello");',
|
|
'}'
|
|
])
|
|
})
|
|
|
|
test('removes the common minimal indent', () => {
|
|
expect(dedent([' let a = {', ' value: 42', ' };'])).toEqual([
|
|
'let a = {',
|
|
' value: 42',
|
|
'};'
|
|
])
|
|
})
|
|
|
|
test('dedents a single line', () => {
|
|
expect(dedent([' let a = 42;'])).toEqual(['let a = 42;'])
|
|
})
|
|
|
|
test('handles tabs', () => {
|
|
expect(dedent(['\tlet a = {', '\t\tvalue: 42', '\t};'])).toEqual([
|
|
'let a = {',
|
|
'\tvalue: 42',
|
|
'};'
|
|
])
|
|
})
|
|
|
|
test('ignores blank lines when computing the minimal indent', () => {
|
|
expect(dedent([' a', '', ' b'])).toEqual(['a', '', 'b'])
|
|
})
|
|
|
|
test('keeps whitespace-only input as is', () => {
|
|
expect(dedent(['', ' '])).toEqual(['', ' '])
|
|
})
|
|
})
|
|
|
|
describe('findRegions', () => {
|
|
it('returns no regions without markers', () => {
|
|
const lines = ['function foo() {', ' console.log("hello");', '}']
|
|
expect(findRegions(lines, 'foo')).toHaveLength(0)
|
|
})
|
|
|
|
it('ignores non-matching and prefix-matching region names', () => {
|
|
const lines = [
|
|
'// #region regionA',
|
|
'some code here',
|
|
'// #endregion regionA'
|
|
]
|
|
expect(findRegions(lines, 'regionC')).toHaveLength(0)
|
|
expect(findRegions(lines, 'region')).toHaveLength(0)
|
|
})
|
|
|
|
it('returns no regions for a start marker without a matching end', () => {
|
|
const lines = [
|
|
'// #region missingEnd',
|
|
'console.log("inside region");',
|
|
'console.log("still inside");'
|
|
]
|
|
expect(findRegions(lines, 'missingEnd')).toHaveLength(0)
|
|
})
|
|
|
|
it('returns no regions for an end marker without a preceding start', () => {
|
|
const lines = [
|
|
'// #endregion ghostRegion',
|
|
'console.log("stray end marker");'
|
|
]
|
|
expect(findRegions(lines, 'ghostRegion')).toHaveLength(0)
|
|
})
|
|
|
|
it('ignores non-marker lines containing the word region', () => {
|
|
const lines = [
|
|
'const region = "region"',
|
|
'// #region hello',
|
|
'const x = 1',
|
|
'// endregion hello is mentioned here without a comment prefix'
|
|
]
|
|
expect(findRegions(lines, 'hello')).toHaveLength(0)
|
|
})
|
|
|
|
it('detects C#-style markers', () => {
|
|
const lines = [
|
|
'Console.WriteLine("Before region");',
|
|
'#region hello',
|
|
'Console.WriteLine("Hello, World!");',
|
|
'#endregion hello',
|
|
'Console.WriteLine("After region");'
|
|
]
|
|
expect(extract(lines, 'hello')).toBe(
|
|
'Console.WriteLine("Hello, World!");'
|
|
)
|
|
})
|
|
|
|
it('closes a named region with an anonymous end marker', () => {
|
|
const lines = [
|
|
'#region hello',
|
|
'Console.WriteLine("Hello, World!");',
|
|
'#endregion',
|
|
'Console.WriteLine("After region");'
|
|
]
|
|
expect(extract(lines, 'hello')).toBe(
|
|
'Console.WriteLine("Hello, World!");'
|
|
)
|
|
})
|
|
|
|
it('does not close a region with a differently named end marker', () => {
|
|
const lines = [
|
|
'#region hello',
|
|
'Console.WriteLine("Hello, World!");',
|
|
'#endregion world'
|
|
]
|
|
expect(findRegions(lines, 'hello')).toHaveLength(0)
|
|
})
|
|
|
|
it('keeps indentation of indented markers and content', () => {
|
|
const lines = [
|
|
' #region hello',
|
|
' Console.WriteLine("Hello, World!");',
|
|
' #endregion hello'
|
|
]
|
|
expect(extract(lines, 'hello')).toBe(
|
|
' Console.WriteLine("Hello, World!");'
|
|
)
|
|
})
|
|
|
|
it('detects double-slash markers with and without spacing', () => {
|
|
const lines = [
|
|
'let regexp: RegExp[] = [];',
|
|
'// #region foo',
|
|
'let start = -1;',
|
|
'//#endregion foo'
|
|
]
|
|
expect(extract(lines, 'foo')).toBe('let start = -1;')
|
|
})
|
|
|
|
it('detects hash-less double-slash markers like VS Code', () => {
|
|
const lines = ['// region foo', 'let start = -1;', '// endregion foo']
|
|
expect(extract(lines, 'foo')).toBe('let start = -1;')
|
|
})
|
|
|
|
it('detects CSS-style markers', () => {
|
|
const lines = [
|
|
'/* #region foo */',
|
|
' padding-left: 15px;',
|
|
'/*#endregion foo*/'
|
|
]
|
|
expect(extract(lines, 'foo')).toBe(' padding-left: 15px;')
|
|
})
|
|
|
|
it('detects HTML-style markers, with the hash optional', () => {
|
|
const lines = [
|
|
'<!-- #region foo -->',
|
|
' <h1>Hello world</h1>',
|
|
'<!--#endregion foo-->',
|
|
'<!-- region bar -->',
|
|
' <h2>Other</h2>',
|
|
'<!-- endregion bar -->'
|
|
]
|
|
expect(extract(lines, 'foo')).toBe(' <h1>Hello world</h1>')
|
|
expect(extract(lines, 'bar')).toBe(' <h2>Other</h2>')
|
|
})
|
|
|
|
it('detects Visual Basic-style markers', () => {
|
|
const lines = [
|
|
'#Region VBRegion',
|
|
' Console.WriteLine("Inside region")',
|
|
'#End Region VBRegion'
|
|
]
|
|
expect(extract(lines, 'VBRegion')).toBe(
|
|
' Console.WriteLine("Inside region")'
|
|
)
|
|
})
|
|
|
|
it('ignores the quotes around a Visual Basic region name', () => {
|
|
const lines = [
|
|
'#Region "Quoted Name"',
|
|
' Console.WriteLine("Inside region")',
|
|
'#End Region',
|
|
'#Region "Other"',
|
|
' Console.WriteLine("Other region")',
|
|
'#End Region "Other"'
|
|
]
|
|
expect(extract(lines, 'Quoted Name')).toBe(
|
|
' Console.WriteLine("Inside region")'
|
|
)
|
|
expect(extract(lines, 'Other')).toBe(
|
|
' Console.WriteLine("Other region")'
|
|
)
|
|
})
|
|
|
|
it('detects bat-style markers with case-insensitive REM', () => {
|
|
const lines = [
|
|
'@REM #region hello',
|
|
'@ECHO OFF',
|
|
'::#endregion hello',
|
|
'echo out',
|
|
'rem #region hello',
|
|
'exit 0',
|
|
'Rem #endregion hello'
|
|
]
|
|
expect(extract(lines, 'hello')).toBe('@ECHO OFF\nexit 0')
|
|
})
|
|
|
|
it('detects dash-dash markers, which require the hash', () => {
|
|
const lines = [
|
|
'-- #region foo',
|
|
'select 1;',
|
|
'-- #endregion foo',
|
|
'--#region bar',
|
|
'select 2;',
|
|
'--#endregion bar'
|
|
]
|
|
expect(extract(lines, 'foo')).toBe('select 1;')
|
|
expect(extract(lines, 'bar')).toBe('select 2;')
|
|
|
|
// prose comments are not markers
|
|
expect(findRegions(['-- region of interest', 'select 1;'], '')).toEqual(
|
|
[]
|
|
)
|
|
})
|
|
|
|
it('detects pragma markers, allowing space after the hash', () => {
|
|
const lines = [
|
|
'#pragma region foo',
|
|
'int a = 1;',
|
|
'#pragma endregion foo',
|
|
'# pragma region bar',
|
|
'int b = 2;',
|
|
'# pragma endregion bar'
|
|
]
|
|
expect(extract(lines, 'foo')).toBe('int a = 1;')
|
|
expect(extract(lines, 'bar')).toBe('int b = 2;')
|
|
})
|
|
|
|
it('detects paren-star markers', () => {
|
|
const lines = ['(* #region foo *)', 'let a = 1', '(* #endregion foo *)']
|
|
expect(extract(lines, 'foo')).toBe('let a = 1')
|
|
})
|
|
|
|
it('detects shell and python style hash markers', () => {
|
|
const lines = [
|
|
'# region hello',
|
|
'echo "inside"',
|
|
'#\tendregion hello',
|
|
'# #region hello',
|
|
'exit 0',
|
|
'# #endregion'
|
|
]
|
|
expect(extract(lines, 'hello')).toBe('echo "inside"\nexit 0')
|
|
})
|
|
|
|
it('detects JSON key-style markers with two or more slashes', () => {
|
|
const lines = [
|
|
'{',
|
|
' "// #region hello": "",',
|
|
' "one": true,',
|
|
' "//#endregion hello": "",',
|
|
' "two": false,',
|
|
' "/// #region hello": "",',
|
|
' "three": true,',
|
|
' "//// #endregion hello": ""',
|
|
'}'
|
|
]
|
|
expect(extract(lines, 'hello')).toBe(' "one": true,\n "three": true,')
|
|
})
|
|
|
|
it('concatenates multiple same-named regions in document order', () => {
|
|
const lines = [
|
|
'// #region hello',
|
|
'first region content',
|
|
'// #endregion hello',
|
|
'other content',
|
|
'// #region hello',
|
|
'second region content',
|
|
'// #endregion',
|
|
'// #region hello',
|
|
'third region content',
|
|
'// #endregion hello'
|
|
]
|
|
expect(extract(lines, 'hello')).toBe(
|
|
'first region content\nsecond region content\nthird region content'
|
|
)
|
|
})
|
|
|
|
it('merges same-named regions across different comment styles', () => {
|
|
const lines = [
|
|
'<template>',
|
|
' <!-- #region shared -->',
|
|
' <div>template part</div>',
|
|
' <!-- #endregion shared -->',
|
|
'</template>',
|
|
'<script>',
|
|
'// #region shared',
|
|
'const scriptPart = true',
|
|
'// #endregion shared',
|
|
'/* #region shared */',
|
|
'console.log(scriptPart)',
|
|
'/* #endregion shared */',
|
|
'</script>',
|
|
'<style>',
|
|
'/* #region shared */',
|
|
'.style-part {}',
|
|
'/* #endregion shared */',
|
|
'</style>'
|
|
]
|
|
const regions = findRegions(lines, 'shared')
|
|
expect(regions).toHaveLength(4)
|
|
expect(extract(lines, 'shared')).toBe(
|
|
[
|
|
' <div>template part</div>',
|
|
'const scriptPart = true',
|
|
'console.log(scriptPart)',
|
|
'.style-part {}'
|
|
].join('\n')
|
|
)
|
|
})
|
|
|
|
it('tracks nesting of same-named regions across styles', () => {
|
|
const lines = [
|
|
'// #region foo',
|
|
"console.log('double-slash only');",
|
|
'/* #region foo */',
|
|
"console.log('nested in both');",
|
|
'// #endregion foo',
|
|
"console.log('still in outer');",
|
|
'/* #endregion foo */',
|
|
"console.log('outside');"
|
|
]
|
|
const regions = findRegions(lines, 'foo')
|
|
expect(regions).toHaveLength(1)
|
|
expect(regions[0]).toMatchObject({ start: 1, end: 6 })
|
|
})
|
|
|
|
it('closes the innermost open region with an anonymous end marker', () => {
|
|
const lines = [
|
|
'<!-- #region demo -->',
|
|
'<template><div /></template>',
|
|
'<script setup>',
|
|
'// #region state',
|
|
'const count = ref(0)',
|
|
'// #endregion',
|
|
'function inc() {}',
|
|
'</script>',
|
|
'<!-- #endregion demo -->'
|
|
]
|
|
expect(extract(lines, 'demo')).toBe(
|
|
[
|
|
'<template><div /></template>',
|
|
'<script setup>',
|
|
'// #region state',
|
|
'const count = ref(0)',
|
|
'// #endregion',
|
|
'function inc() {}',
|
|
'</script>'
|
|
].join('\n')
|
|
)
|
|
expect(extract(lines, 'state')).toBe('const count = ref(0)')
|
|
})
|
|
|
|
it('is not closed by an anonymous end marker of a nested region in a fence', () => {
|
|
const lines = [
|
|
'<!-- #region sample -->',
|
|
'Use markers like this:',
|
|
'',
|
|
'```js',
|
|
'// #region foo',
|
|
'const a = 1',
|
|
'// #endregion',
|
|
'```',
|
|
'<!-- #endregion sample -->'
|
|
]
|
|
expect(extract(lines, 'sample')).toBe(
|
|
[
|
|
'Use markers like this:',
|
|
'',
|
|
'```js',
|
|
'// #region foo',
|
|
'const a = 1',
|
|
'// #endregion',
|
|
'```'
|
|
].join('\n')
|
|
)
|
|
})
|
|
|
|
it('is closed by an anonymous end marker of its own style', () => {
|
|
// a region documenting region syntax: the marker inside the fence is
|
|
// never closed, and the anonymous end marker belongs to the region
|
|
const lines = [
|
|
'<!-- #region real -->',
|
|
'How regions work:',
|
|
'',
|
|
'```js',
|
|
'// #region example',
|
|
'const a = 1',
|
|
'```',
|
|
'<!-- #endregion -->'
|
|
]
|
|
expect(extract(lines, 'real')).toBe(
|
|
[
|
|
'How regions work:',
|
|
'',
|
|
'```js',
|
|
'// #region example',
|
|
'const a = 1',
|
|
'```'
|
|
].join('\n')
|
|
)
|
|
})
|
|
|
|
it('ignores an anonymous end marker with nothing open in its style', () => {
|
|
const lines = [
|
|
'<!-- #region real -->',
|
|
'// #endregion',
|
|
'body',
|
|
'<!-- #endregion -->'
|
|
]
|
|
expect(extract(lines, 'real')).toBe('// #endregion\nbody')
|
|
})
|
|
|
|
it('tolerates an unclosed region nested inside the requested one', () => {
|
|
const lines = [
|
|
'// #region outer',
|
|
'const a = 1',
|
|
'// #region inner',
|
|
'const b = 2',
|
|
'// #endregion outer'
|
|
]
|
|
expect(extract(lines, 'outer')).toBe(
|
|
['const a = 1', '// #region inner', 'const b = 2'].join('\n')
|
|
)
|
|
})
|
|
|
|
it('keeps differently named nested regions verbatim', () => {
|
|
const lines = [
|
|
'// #region foo',
|
|
"console.log('line before nested');",
|
|
'// #region bar',
|
|
"console.log('nested content');",
|
|
'// #endregion bar',
|
|
'// #endregion foo'
|
|
]
|
|
expect(extract(lines, 'foo')).toBe(
|
|
[
|
|
"console.log('line before nested');",
|
|
'// #region bar',
|
|
"console.log('nested content');",
|
|
'// #endregion bar'
|
|
].join('\n')
|
|
)
|
|
})
|
|
|
|
it('supports special characters in region names', () => {
|
|
const lines = [
|
|
'// #region complex-name_123',
|
|
'const x = 1;',
|
|
'// #endregion complex-name_123',
|
|
'// #region my.region',
|
|
'const y = 2;',
|
|
'// #endregion my.region'
|
|
]
|
|
expect(extract(lines, 'complex-name_123')).toBe('const x = 1;')
|
|
expect(extract(lines, 'my.region')).toBe('const y = 2;')
|
|
})
|
|
|
|
it('reports the marker style that opened each region', () => {
|
|
const lines = ['// #region foo', 'const x = 1;', '// #endregion foo']
|
|
const [region] = findRegions(lines, 'foo')
|
|
expect(markers).toContain(region.marker)
|
|
expect(region.marker.start.test('// #region other')).toBe(true)
|
|
expect(region.marker.start.test('# region other')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('stripRegionMarkers', () => {
|
|
it('strips marker lines of every style and name by default', () => {
|
|
const lines = [
|
|
'// #region name',
|
|
'const a = 0;',
|
|
'/* #region HELLO */',
|
|
'const b = 0;',
|
|
'//\t#endregion complex_name-123',
|
|
'const c = 0;',
|
|
'/*#endregion*/'
|
|
]
|
|
expect(stripRegionMarkers(lines)).toEqual([
|
|
'const a = 0;',
|
|
'const b = 0;',
|
|
'const c = 0;'
|
|
])
|
|
})
|
|
|
|
it('keeps non-marker lines mentioning regions', () => {
|
|
const lines = ['const region = "region"', 'let a = 1']
|
|
expect(stripRegionMarkers(lines)).toEqual(lines)
|
|
})
|
|
|
|
it('strips only the given marker styles when provided', () => {
|
|
const lines = [
|
|
'// #region a',
|
|
'const x = 1',
|
|
'// #endregion a',
|
|
'# region b',
|
|
'const y = 2',
|
|
'# endregion b'
|
|
]
|
|
const [region] = findRegions(lines, 'a')
|
|
expect(stripRegionMarkers(lines, [region.marker])).toEqual([
|
|
'const x = 1',
|
|
'# region b',
|
|
'const y = 2',
|
|
'# endregion b'
|
|
])
|
|
})
|
|
})
|
|
})
|