diff --git a/__tests__/e2e/markdown-extensions/foo.md b/__tests__/e2e/markdown-extensions/foo.md index c601f247..bd93e61f 100644 --- a/__tests__/e2e/markdown-extensions/foo.md +++ b/__tests__/e2e/markdown-extensions/foo.md @@ -1,7 +1,11 @@ # Foo +This is before region + ## Region -this is region - \ No newline at end of file +This is a region + + +This is after region diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md index 3dba6b4e..36d6c028 100644 --- a/__tests__/e2e/markdown-extensions/index.md +++ b/__tests__/e2e/markdown-extensions/index.md @@ -181,7 +181,18 @@ export default config - ## Markdown Nested File Inclusion + +## Markdown File Inclusion with Range + + + +## Markdown File Inclusion with Range without Start + + + +## Markdown File Inclusion with Range without End + + diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 69764571..d3c38c0f 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -5,6 +5,8 @@ const getClassList = async (locator: Locator) => { return className?.split(' ').filter(Boolean) ?? [] } +const trim = (str?: string | null) => str?.replace(/\u200B/g, '').trim() + beforeEach(async () => { await goto('/markdown-extensions/') }) @@ -63,7 +65,7 @@ describe('Table of Contents', () => { test('render toc', async () => { const items = page.locator('#table-of-contents + nav ul li') const count = await items.count() - expect(count).toBe(27) + expect(count).toBe(33) }) }) @@ -161,7 +163,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(7) + expect(await lines.count()).toBe(11) }) test('specify region', async () => { @@ -214,7 +216,7 @@ describe('Code Groups', () => { // blocks const blocks = div.locator('.blocks > div') - expect(await blocks.nth(0).locator('code > span').count()).toBe(7) + 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) @@ -229,12 +231,38 @@ describe('Markdown File Inclusion', () => { const h1 = page.locator('#markdown-file-inclusion + h1') expect(await h1.getAttribute('id')).toBe('foo') }) + test('render markdown using @', async () => { const h1 = page.locator('#markdown-at-file-inclusion + h1') expect(await h1.getAttribute('id')).toBe('bar') }) + test('render markdown using nested inclusion', async () => { const h1 = page.locator('#markdown-nested-file-inclusion + h1') expect(await h1.getAttribute('id')).toBe('foo-1') }) + + test('support selecting range', async () => { + const h2 = page.locator('#markdown-file-inclusion-with-range + h2') + expect(trim(await h2.textContent())).toBe('Region') + + const p = page.locator('#markdown-file-inclusion-with-range + h2 + p') + expect(trim(await p.textContent())).toBe('This is a region') + }) + + test('support selecting range without specifying start', async () => { + const p = page.locator( + '#markdown-file-inclusion-with-range-without-start ~ p' + ) + expect(trim(await p.nth(0).textContent())).toBe('This is before region') + expect(trim(await p.nth(1).textContent())).toBe('This is a region') + }) + + test('support selecting range without specifying end', async () => { + const p = page.locator( + '#markdown-file-inclusion-with-range-without-end ~ p' + ) + expect(trim(await p.nth(0).textContent())).toBe('This is a region') + expect(trim(await p.nth(1).textContent())).toBe('This is after region') + }) }) diff --git a/docs/guide/markdown.md b/docs/guide/markdown.md index bd213f30..e20e864f 100644 --- a/docs/guide/markdown.md +++ b/docs/guide/markdown.md @@ -738,6 +738,42 @@ Some getting started stuff. Can be created using `.foorc.json`. ``` +It also supports selecting a line range: + +**Input** + +```md +# Docs + +## Basics + + +``` + +**Part file** (`parts/basics.md`) + +```md +Some getting started stuff. + +### Configuration + +Can be created using `.foorc.json`. +``` + +**Equivalent code** + +```md +# Docs + +## Basics + +### Configuration + +Can be created using `.foorc.json`. +``` + +The format of the selected line range can be: `{3,}`, `{,10}`, `{1,10}` + ::: warning Note that this does not throw errors if your file is not present. Hence, when using this feature make sure that the contents are being rendered as expected. ::: diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index 02d1f90e..1629f2d8 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -21,6 +21,7 @@ import { getGitTimestamp } from './utils/getGitTimestamp' const debug = _debug('vitepress:md') const cache = new LRUCache({ max: 1024 }) const includesRE = //g +const rangeRE = /\{(\d*),(\d*)\}$/ export interface MarkdownCompileResult { vueSrc: string @@ -88,15 +89,27 @@ export async function createMarkdownToVueRenderFn( let includes: string[] = [] function processIncludes(src: string): string { - return src.replace(includesRE, (m, m1) => { + return src.replace(includesRE, (m: string, m1: string) => { if (!m1.length) return m + const range = m1.match(rangeRE) + range && (m1 = m1.slice(0, -range[0].length)) const atPresent = m1[0] === '@' try { const includePath = atPresent ? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1)) : path.join(path.dirname(fileOrig), m1) - const content = fs.readFileSync(includePath, 'utf-8') + let content = fs.readFileSync(includePath, 'utf-8') + if (range) { + const [, startLine, endLine] = range + const lines = content.split(/\r?\n/) + content = lines + .slice( + startLine ? parseInt(startLine, 10) - 1 : undefined, + endLine ? parseInt(endLine, 10) : undefined + ) + .join('\n') + } includes.push(slash(includePath)) // recursively process includes in the content return processIncludes(content)