From bd8b8e2772a39744c8ced370107642904153e9b2 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Thu, 27 Feb 2025 22:30:55 +0530 Subject: [PATCH] handle nested regions --- .../node/markdown/plugins/snippet.test.ts | 297 +++++++++++------- src/node/markdown/plugins/snippet.ts | 76 +++-- 2 files changed, 238 insertions(+), 135 deletions(-) diff --git a/__tests__/unit/node/markdown/plugins/snippet.test.ts b/__tests__/unit/node/markdown/plugins/snippet.test.ts index 0781b373..aa940784 100644 --- a/__tests__/unit/node/markdown/plugins/snippet.test.ts +++ b/__tests__/unit/node/markdown/plugins/snippet.test.ts @@ -99,145 +99,228 @@ describe('node/markdown/plugins/snippet', () => { }) }) - test('rawPathToToken', () => { - rawPathTokenMap.forEach(([rawPath, token]) => { + describe('rawPathToToken', () => { + test.each(rawPathTokenMap)('%s', (rawPath, token) => { expect(removeEmptyKeys(rawPathToToken(rawPath))).toEqual(token) }) }) describe('findRegion', () => { - test('when c# region with matching tag', () => { - const lines = `Console.WriteLine("Before region"); -#region hello -Console.WriteLine("Hello, World!"); -#endregion hello -Console.WriteLine("After region");`.split('\n') - const result = findRegion(lines, 'hello') + it('returns null when no region markers are present', () => { + const lines = ['function foo() {', ' console.log("hello");', '}'] + expect(findRegion(lines, 'foo')).toBeNull() + }) - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - 'Console.WriteLine("Hello, World!");' - ) + it('ignores non-matching region names', () => { + const lines = [ + '// #region regionA', + 'some code here', + '// #endregion regionA' + ] + expect(findRegion(lines, 'regionC')).toBeNull() }) - test('when c# region is not indented with spaces and no matching tag', () => { - const lines = `Console.WriteLine("Before region"); -#region hello -Console.WriteLine("Hello, World!"); -#endregion -Console.WriteLine("After region");`.split('\n') - const result = findRegion(lines, 'hello') - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - 'Console.WriteLine("Hello, World!");' - ) + it('returns null 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() }) - test('when c# region is indented with spaces and no matching tag', () => { - const lines = ` Console.WriteLine("Before region"); - #region hello - Console.WriteLine("Hello, World!"); - #endregion hello - Console.WriteLine("After region");`.split('\n') - const result = findRegion(lines, 'hello') - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - ' Console.WriteLine("Hello, World!");' - ) + it('returns null if an end marker exists without a preceding start marker', () => { + const lines = [ + '// #endregion ghostRegion', + 'console.log("stray end marker");' + ] + expect(findRegion(lines, 'ghostRegion')).toBeNull() }) - test('when c# region with matching tag', () => { - const lines = `Console.WriteLine("Before region"); -#region hello -Console.WriteLine("Hello, World!"); -#endregion hello -Console.WriteLine("After region");`.split('\n') - const result = findRegion(lines, 'hello') - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - 'Console.WriteLine("Hello, World!");' - ) + it('detects C#/JavaScript style region markers with matching tags', () => { + const lines = [ + 'Console.WriteLine("Before region");', + '#region hello', + 'Console.WriteLine("Hello, World!");', + '#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!");' + ) + } }) - test('when c# region is not indented with spaces and no matching tag', () => { - const lines = `Console.WriteLine("Before region"); -#region hello -Console.WriteLine("Hello, World!"); -#endregion -Console.WriteLine("After region");`.split('\n') + + it('detects region markers even when the end marker omits the region name', () => { + const lines = [ + 'Console.WriteLine("Before region");', + '#region hello', + 'Console.WriteLine("Hello, World!");', + '#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!");' + ) + } + }) - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - 'Console.WriteLine("Hello, World!");' - ) + it('handles indented region markers correctly', () => { + const lines = [ + ' Console.WriteLine("Before region");', + ' #region hello', + ' Console.WriteLine("Hello, World!");', + ' #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!");' + ) + } }) - test('when typescript region has matching tag', () => { - const lines = `let regexp: RegExp[] = [] -// #region foo -let start = -1 -// #endregion foo`.split('\n') + it('detects TypeScript style region markers', () => { + const lines = [ + 'let regexp: RegExp[] = [];', + '// #region foo', + 'let start = -1;', + '// #endregion foo' + ] const result = findRegion(lines, 'foo') + expect(result).not.toBeNull() + if (result) { + expect(lines.slice(result.start, result.end).join('\n')).toBe( + 'let start = -1;' + ) + } + }) - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - 'let start = -1' - ) + it('detects CSS style region markers', () => { + const lines = [ + '.body-content {', + '/* #region foo */', + ' padding-left: 15px;', + '/* #endregion foo */', + ' 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;' + ) + } }) - test('when typescript region is indented with spaces and no matching tag', () => { - const lines = ` let regexp: RegExp[] = [] - // #region foo - let start = -1 - // #endregion`.split('\n') + + it('detects HTML style region markers', () => { + const lines = [ + '
Some content
', + '', + '

Hello world

', + '', + '
Other content
' + ] const result = findRegion(lines, 'foo') + expect(result).not.toBeNull() + if (result) { + expect(lines.slice(result.start, result.end).join('\n')).toBe( + '

Hello world

' + ) + } + }) - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - ' let start = -1' - ) + it('detects Visual Basic style region markers (with case-insensitive "End")', () => { + const lines = [ + 'Console.WriteLine("VB")', + '#Region VBRegion', + ' Console.WriteLine("Inside region")', + '#End Region VBRegion', + '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")' + ) + } }) - test('when css region has matching tag', () => { - const lines = `.body-content { -/* #region foo */ - padding-left: 15px; -/* #endregion foo */ - padding-right: 15px; -}`.split('\n') + 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' + ) + } + }) - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - ' padding-left: 15px;' - ) - }) - test('when css region is indented with spaces and no matching tag', () => { - const lines = `.body-content { - /* #region foo */ - padding-left: 15px; - /* #endregion */ - padding-right: 15px; -}`.split('\n') + it('detects C/C++ style region markers using #pragma', () => { + const lines = [ + '#pragma region foo', + 'int a = 1;', + '#pragma endregion foo' + ] const result = findRegion(lines, 'foo') - - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - ' padding-left: 15px;' - ) + expect(result).not.toBeNull() + if (result) { + expect(lines.slice(result.start, result.end).join('\n')).toBe( + 'int a = 1;' + ) + } }) - test('when html region has matching tag', () => { - const lines = ` -

Hello world

- -

more text

`.split('\n') + it('returns the first complete region when multiple regions exist', () => { + const lines = [ + '// #region foo', + 'first region content', + '// #endregion foo', + '// #region foo', + 'second region content', + '// #endregion foo' + ] const result = findRegion(lines, 'foo') - - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - '

Hello world

' - ) + expect(result).not.toBeNull() + if (result) { + expect(lines.slice(result.start, result.end).join('\n')).toBe( + 'first region content' + ) + } }) - test('when html region is indented with spaces and no matching tag', () => { - const lines = ` -

Hello world

- -

more text

`.split('\n') - const result = findRegion(lines, 'foo') - expect(lines.slice(result?.start, result?.end).join('\n')).toBe( - '

Hello world

' - ) + it('handles nested regions with different names properly', () => { + const lines = [ + '// #region foo', + "console.log('line before nested');", + '// #region bar', + "console.log('nested content');", + '// #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) + } }) }) }) diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts index c03a8081..05356f72 100644 --- a/src/node/markdown/plugins/snippet.ts +++ b/src/node/markdown/plugins/snippet.ts @@ -51,22 +51,8 @@ export function dedent(text: string): string { return text } -function testLine( - line: string, - regexp: RegExp, - regionName: string, - end: boolean = false -) { - const [full, tag, name] = regexp.exec(line.trim()) || [] - - return full && tag && end - ? true - : name === regionName && - tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/) -} - export function findRegion(lines: Array, regionName: string) { - const regionRegexps = [ + const regionRegexps: [RegExp, RegExp][] = [ [ /^[ \t]*\/\/ ?#?(region) ([\w*-]+)$/, /^[ \t]*\/\/ ?#?(endregion) ?([\w*-]*)$/ @@ -82,20 +68,54 @@ export function findRegion(lines: Array, regionName: string) { [/^[ \t]*# ?(region) ([\w*-]+)$/, /^[ \t]*# ?(endregion) ?([\w*-]*)$/] // C#, PHP, Powershell, Python, perl & misc ] - let regexp: RegExp[] = [] - let start = -1 - - for (const [lineId, line] of lines.entries()) { - if (regexp.length === 0) { - for (const reg of regionRegexps) { - if (testLine(line, reg[0], regionName)) { - start = lineId + 1 - regexp = reg - break - } + let chosenRegex: [RegExp, RegExp] | null = null + let startLine = -1 + // find the regex pair for a start marker that matches the given region name + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + for (const [startRegex, endRegex] of regionRegexps) { + const startMatch = startRegex.exec(line) + if ( + startMatch && + startMatch[2] === regionName && + /^[rR]egion$/.test(startMatch[1]) + ) { + chosenRegex = [startRegex, endRegex] + startLine = i + 1 + break + } + } + if (chosenRegex) break + } + if (!chosenRegex) return null + + const [startRegex, endRegex] = chosenRegex + let counter = 1 + // scan the rest of the lines to find the matching end marker, handling nested markers + for (let i = startLine; i < lines.length; i++) { + const trimmed = lines[i].trim() + // check for an inner start marker for the same region + const startMatch = startRegex.exec(trimmed) + if ( + startMatch && + startMatch[2] === regionName && + /^[rR]egion$/.test(startMatch[1]) + ) { + counter++ + continue + } + // check for an end marker for the same region + const endMatch = endRegex.exec(trimmed) + if ( + endMatch && + // allow empty region name on the end marker as a fallback + (endMatch[2] === regionName || endMatch[2] === '') && + /^[Ee]nd ?[rR]egion$/.test(endMatch[1]) + ) { + counter-- + if (counter === 0) { + return { start: startLine, end: i, regexp: chosenRegex } } - } else if (testLine(line, regexp[1], regionName, true)) { - return { start, end: lineId, regexp } } }