From d1436f975131d0c0c97bdf77c43bb9dc79204413 Mon Sep 17 00:00:00 2001 From: Miroma Date: Mon, 3 Nov 2025 09:49:48 +0100 Subject: [PATCH 1/3] 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]: [^2]: via [^3]: --- .../node/markdown/plugins/snippet.test.ts | 246 ++++++++++++------ docs/en/guide/markdown.md | 8 +- docs/snippets/snippet-with-region.js | 12 +- src/node/markdown/plugins/snippet.ts | 87 ++++--- src/node/utils/processIncludes.ts | 18 +- 5 files changed, 239 insertions(+), 132 deletions(-) diff --git a/__tests__/unit/node/markdown/plugins/snippet.test.ts b/__tests__/unit/node/markdown/plugins/snippet.test.ts index aa940784..39ea7fd4 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' @@ -39,6 +39,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', () => { @@ -97,6 +98,10 @@ describe('node/markdown/plugins/snippet', () => { };" `) }) + + test('empty string remains empty', () => { + expect(dedent('')).toBe('') + }) }) describe('rawPathToToken', () => { @@ -106,9 +111,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 +127,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 +155,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 +178,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 +201,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 +389,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 7249fac2..47ed2b76 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 9c7faaeb..fd087862 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 71e5e76c..756da390 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 aa98ccbd..c13c2809 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 From 49425d3d588c185f1c1e5a531e0cf40d1346023e Mon Sep 17 00:00:00 2001 From: Miroma Date: Sun, 21 Dec 2025 22:45:31 +0100 Subject: [PATCH 2/3] fix(md)!: warn if region not found, strip #regions when a region is not in the source file, instead of including the whole file, warn that the #region was not found in it. fix #4625 --- `<<<` snippets now also strip out all #region markers: ```file.ts // #region A // #region B console.log("Hello, World!"); // #endregion // #endregion ``` <<< file.ts#A ...does not include "#region B" anymore --- .../markdown-extensions.test.ts | 4 +- .../node/markdown/plugins/snippet.test.ts | 72 ++++++++++++++++++- src/node/markdown/plugins/snippet.ts | 29 ++++++-- src/node/utils/processIncludes.ts | 10 ++- 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 839f953c..ea41825f 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -216,7 +216,7 @@ describe('Line Numbers', () => { describe('Import Code Snippets', () => { test('basic', async () => { const lines = page.locator('#basic-code-snippet + div code > span') - expect(await lines.count()).toBe(11) + expect(await lines.count()).toBe(9) }) test('specify region', async () => { @@ -269,7 +269,7 @@ describe('Code Groups', () => { // blocks const blocks = div.locator('.blocks > div') - expect(await blocks.nth(0).locator('code > span').count()).toBe(11) + expect(await blocks.nth(0).locator('code > span').count()).toBe(9) expect(await getClassList(blocks.nth(1))).toContain('line-numbers-mode') expect(await getClassList(blocks.nth(1))).toContain('language-ts') expect(await blocks.nth(1).locator('code > span').count()).toBe(3) diff --git a/__tests__/unit/node/markdown/plugins/snippet.test.ts b/__tests__/unit/node/markdown/plugins/snippet.test.ts index 39ea7fd4..852367e0 100644 --- a/__tests__/unit/node/markdown/plugins/snippet.test.ts +++ b/__tests__/unit/node/markdown/plugins/snippet.test.ts @@ -1,7 +1,8 @@ import { dedent, findRegions, - rawPathToToken + rawPathToToken, + stripRegionMarkers } from 'node/markdown/plugins/snippet' import { expect } from 'vitest' @@ -406,5 +407,74 @@ describe('node/markdown/plugins/snippet', () => { 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) + if (result) { + 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;') + } + }) + }) + + describe('stripRegionMarkers', () => { + it('removes #region and #endregion lines', () => { + const src = [ + '// #region A', + '// #region B', + 'console.log("Hello, World!");', + '// #endregion B', + '// #endregion A' + ] + expect(stripRegionMarkers(src)).toBe('console.log("Hello, World!");') + }) + + it('removes region markers for various syntaxes', () => { + const src = [ + '', + '
hi
', + '', + '/* #region css */', + 'body {}', + '/* #endregion css */', + '#pragma region cpp', + 'int main(){}', + '#pragma endregion cpp', + '::#region bat', + 'ECHO ON', + 'REM #endregion bat' + ] + const out = stripRegionMarkers(src) + expect(out).not.toContain('#region') + expect(out).not.toContain('#endregion') + expect(out).toContain('
hi
') + expect(out).toContain('body {}') + expect(out).toContain('int main(){}') + expect(out).toContain('ECHO ON') + }) + + it('removes markers even if indented or with extra spaces', () => { + const src = [ + ' // #region spaced ', + '\t/* #region */', + 'code();', + ' // #endregion spaced', + '/* #endregion */' + ] + const out = stripRegionMarkers(src) + expect(out.trim()).toBe('code();') + }) }) }) diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts index 756da390..2d1bbcdf 100644 --- a/src/node/markdown/plugins/snippet.ts +++ b/src/node/markdown/plugins/snippet.ts @@ -126,6 +126,17 @@ export function findRegions(lines: string[], regionName: string) { return returned } +export function stripRegionMarkers(lines: string[]): string { + return lines + .filter((l) => { + for (const m of markers) { + if (m.start.test(l) || m.end.test(l)) return false + } + return true + }) + .join('\n') +} + export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => { const parser: RuleBlock = (state, startLine, endLine, silent) => { const CH = '<'.charCodeAt(0) @@ -182,7 +193,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) @@ -204,21 +215,27 @@ export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => { let content = fs.readFileSync(src, 'utf8').replace(/\r\n/g, '\n') - if (regionName) { + if (region) { const lines = content.split('\n') - const regions = findRegions(lines, regionName) + const regions = findRegions(lines, region) if (regions.length > 0) { content = dedent( - regions - .flatMap((r) => + stripRegionMarkers( + regions.flatMap((r) => lines .slice(r.start, r.end) .filter((l) => !(r.re.start.test(l) || r.re.end.test(l))) ) - .join('\n') + ) ) + } else { + token.content = `No region #${region} found in path: ${src}` + token.info = '' + return fence(...args) } + } else { + content = stripRegionMarkers(content.split('\n')) } token.content = content diff --git a/src/node/utils/processIncludes.ts b/src/node/utils/processIncludes.ts index c13c2809..bf4bcf22 100644 --- a/src/node/utils/processIncludes.ts +++ b/src/node/utils/processIncludes.ts @@ -71,9 +71,13 @@ export function processIncludes( } } - content = regions - .flatMap((region) => lines.slice(region.start, region.end)) - .join('\n') + if (regions.length > 0) { + content = regions + .flatMap((region) => lines.slice(region.start, region.end)) + .join('\n') + } else { + content = `No region or heading #${region} found in path: ${includePath}` + } } if (range) { From 2d0695b3b24078ace069345d7c4720129902dadb Mon Sep 17 00:00:00 2001 From: Miroma Date: Tue, 27 Jan 2026 14:56:57 +0100 Subject: [PATCH 3/3] feat(md): make stripping markers configurable --- .../markdown-extensions.test.ts | 4 ++-- .../node/markdown/plugins/snippet.test.ts | 19 +++++++++++++++---- src/node/markdown/markdown.ts | 7 ++++++- src/node/markdown/plugins/snippet.ts | 16 +++++++++++----- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index ea41825f..839f953c 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -216,7 +216,7 @@ describe('Line Numbers', () => { describe('Import Code Snippets', () => { test('basic', async () => { const lines = page.locator('#basic-code-snippet + div code > span') - expect(await lines.count()).toBe(9) + expect(await lines.count()).toBe(11) }) test('specify region', async () => { @@ -269,7 +269,7 @@ describe('Code Groups', () => { // blocks const blocks = div.locator('.blocks > div') - expect(await blocks.nth(0).locator('code > span').count()).toBe(9) + expect(await blocks.nth(0).locator('code > span').count()).toBe(11) expect(await getClassList(blocks.nth(1))).toContain('line-numbers-mode') expect(await getClassList(blocks.nth(1))).toContain('language-ts') expect(await blocks.nth(1).locator('code > span').count()).toBe(3) diff --git a/__tests__/unit/node/markdown/plugins/snippet.test.ts b/__tests__/unit/node/markdown/plugins/snippet.test.ts index 852367e0..15f72fc5 100644 --- a/__tests__/unit/node/markdown/plugins/snippet.test.ts +++ b/__tests__/unit/node/markdown/plugins/snippet.test.ts @@ -2,7 +2,7 @@ import { dedent, findRegions, rawPathToToken, - stripRegionMarkers + stripMarkers } from 'node/markdown/plugins/snippet' import { expect } from 'vitest' @@ -438,7 +438,18 @@ describe('node/markdown/plugins/snippet', () => { '// #endregion B', '// #endregion A' ] - expect(stripRegionMarkers(src)).toBe('console.log("Hello, World!");') + expect(stripMarkers(src, true)).toBe('console.log("Hello, World!");') + }) + + it('does not remove any marker if stripRegionMarkers is false', () => { + const src = [ + '// #region A', + '// #region B', + 'console.log("Hello, World!");', + '// #endregion B', + '// #endregion A' + ] + expect(stripMarkers(src, false)).toBe(src.join('\n')) }) it('removes region markers for various syntaxes', () => { @@ -456,7 +467,7 @@ describe('node/markdown/plugins/snippet', () => { 'ECHO ON', 'REM #endregion bat' ] - const out = stripRegionMarkers(src) + const out = stripMarkers(src, true) expect(out).not.toContain('#region') expect(out).not.toContain('#endregion') expect(out).toContain('
hi
') @@ -473,7 +484,7 @@ describe('node/markdown/plugins/snippet', () => { ' // #endregion spaced', '/* #endregion */' ] - const out = stripRegionMarkers(src) + const out = stripMarkers(src, true) expect(out.trim()).toBe('code();') }) }) diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index fbe92b30..de817b7f 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -140,6 +140,11 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { * @default 'Copy Code' */ codeCopyButtonTitle?: string + /** + * Remove all #region markers when including snippets + * @default false + */ + stripMarkersFromSnippets?: boolean /* ==================== Markdown It Plugins ==================== */ @@ -274,7 +279,7 @@ export async function createMarkdownRenderer( codeCopyButtonTitle, languageLabel: options.languageLabel }) - snippetPlugin(md, srcDir) + snippetPlugin(md, srcDir, options.stripMarkersFromSnippets) containerPlugin(md, options.container) imagePlugin(md, options.image) linkPlugin( diff --git a/src/node/markdown/plugins/snippet.ts b/src/node/markdown/plugins/snippet.ts index 2d1bbcdf..87ba8da1 100644 --- a/src/node/markdown/plugins/snippet.ts +++ b/src/node/markdown/plugins/snippet.ts @@ -126,7 +126,8 @@ export function findRegions(lines: string[], regionName: string) { return returned } -export function stripRegionMarkers(lines: string[]): string { +export function stripMarkers(lines: string[], stripMarkers: boolean): string { + if (!stripMarkers) return lines.join('\n') return lines .filter((l) => { for (const m of markers) { @@ -137,7 +138,11 @@ export function stripRegionMarkers(lines: string[]): string { .join('\n') } -export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => { +export const snippetPlugin = ( + md: MarkdownItAsync, + srcDir: string, + stripMarkersFromSnippets = false +) => { const parser: RuleBlock = (state, startLine, endLine, silent) => { const CH = '<'.charCodeAt(0) const pos = state.bMarks[startLine] + state.tShift[startLine] @@ -221,12 +226,13 @@ export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => { if (regions.length > 0) { content = dedent( - stripRegionMarkers( + stripMarkers( regions.flatMap((r) => lines .slice(r.start, r.end) .filter((l) => !(r.re.start.test(l) || r.re.end.test(l))) - ) + ), + stripMarkersFromSnippets ) ) } else { @@ -235,7 +241,7 @@ export const snippetPlugin = (md: MarkdownItAsync, srcDir: string) => { return fence(...args) } } else { - content = stripRegionMarkers(content.split('\n')) + content = stripMarkers(content.split('\n'), stripMarkersFromSnippets) } token.content = content