From 36ef0a608863795015e200732120b5886b1bb6b5 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sat, 8 Mar 2025 19:51:02 +0530 Subject: [PATCH] feat: support using titles instead of region names in markdown file inclusion closes #4375 closes #4382 Co-authored-by: btea <2356281422@qq.com> --- .../e2e/markdown-extensions/header-include.md | 27 ++++++++++++++ __tests__/e2e/markdown-extensions/index.md | 6 +++- docs/en/guide/markdown.md | 35 ++++++++++++++++++ src/node/markdownToVue.ts | 2 +- src/node/plugins/localSearchPlugin.ts | 2 +- src/node/utils/processIncludes.ts | 36 +++++++++++++++---- 6 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 __tests__/e2e/markdown-extensions/header-include.md diff --git a/__tests__/e2e/markdown-extensions/header-include.md b/__tests__/e2e/markdown-extensions/header-include.md new file mode 100644 index 00000000..70fac23b --- /dev/null +++ b/__tests__/e2e/markdown-extensions/header-include.md @@ -0,0 +1,27 @@ +# header 1 + +header 1 content + +## header 1.1 + +header 1.1 content + +### header 1.1.1 + +header 1.1.1 content + +### header 1.1.2 + +header 1.1.2 content + +## header 1.2 + +header 1.2 content + +### header 1.2.1 + +header 1.2.1 content + +### header 1.2.2 + +header 1.2.2 content diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md index 855803f2..3446b4ef 100644 --- a/__tests__/e2e/markdown-extensions/index.md +++ b/__tests__/e2e/markdown-extensions/index.md @@ -213,6 +213,10 @@ export default config +## Markdown File Inclusion with Header + + + ## Image Lazy Loading -![vitepress logo](/vitepress.png) \ No newline at end of file +![vitepress logo](/vitepress.png) diff --git a/docs/en/guide/markdown.md b/docs/en/guide/markdown.md index cf052919..426e7161 100644 --- a/docs/en/guide/markdown.md +++ b/docs/en/guide/markdown.md @@ -897,6 +897,41 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co 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. ::: +Instead of VS Code regions, you can also use header anchors to include a specific section of the file. For example, if you have a header in your markdown file like this: + +```md +## My Base Section + +Some content here. + +### My Sub Section + +Some more content here. + +## Another Section + +Content outside `My Base Section`. +``` + +You can include the `My Base Section` section like this: + +```md +## My Extended Section + +``` + +**Equivalent code** + +```md +## My Extended Section + +Some content here. + +### My Sub Section + +Some more content here. +``` + ## Math Equations This is currently opt-in. To enable it, you need to install `markdown-it-mathjax3` and set `markdown.math` to `true` in your config file: diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index d4576cda..151177ab 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -142,7 +142,7 @@ export async function createMarkdownToVueRenderFn( // resolve includes let includes: string[] = [] - src = processIncludes(srcDir, src, fileOrig, includes) + src = processIncludes(md, srcDir, src, fileOrig, includes) const localeIndex = getLocaleForPath(siteConfig?.site, relativePath) diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts index debc024c..12786c8e 100644 --- a/src/node/plugins/localSearchPlugin.ts +++ b/src/node/plugins/localSearchPlugin.ts @@ -56,7 +56,7 @@ export async function localSearchPlugin( const relativePath = slash(path.relative(srcDir, file)) const env: MarkdownEnv = { path: file, relativePath, cleanUrls } const md_raw = await fs.promises.readFile(file, 'utf-8') - const md_src = processIncludes(srcDir, md_raw, file, []) + const md_src = processIncludes(md, srcDir, md_raw, file, []) if (options._render) { return await options._render(md_src, env, md) } else { diff --git a/src/node/utils/processIncludes.ts b/src/node/utils/processIncludes.ts index acdccab7..e8b584ca 100644 --- a/src/node/utils/processIncludes.ts +++ b/src/node/utils/processIncludes.ts @@ -1,18 +1,20 @@ import fs from 'fs-extra' 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 { slash } from '../shared' export function processIncludes( + md: MarkdownItAsync, srcDir: string, src: string, file: string, includes: string[] ): string { const includesRE = //g - const regionRE = /(#[\w-]+)/ + const regionRE = /(#\S+)/ const rangeRE = /\{(\d*),(\d*)\}$/ return src.replace(includesRE, (m: string, m1: string) => { @@ -39,8 +41,30 @@ export function processIncludes( if (region) { const [regionName] = region const lines = content.split(/\r?\n/) - const regionLines = findRegion(lines, regionName.slice(1)) - content = lines.slice(regionLines?.start, regionLines?.end).join('\n') + let { start, end } = findRegion(lines, regionName.slice(1)) ?? {} + + if (start === undefined) { + // region not found, it might be a header + const tokens = md + .parse(content, {}) + .filter((t) => t.type === 'heading_open' && t.map) + const idx = tokens.findIndex( + (t) => t.attrGet('id') === regionName.slice(1) + ) + const token = tokens[idx] + if (token) { + start = token.map![1] + const level = parseInt(token.tag.slice(1)) + for (let i = idx + 1; i < tokens.length; i++) { + if (parseInt(tokens[i].tag.slice(1)) <= level) { + end = tokens[i].map![0] - 1 + break + } + } + } + } + + content = lines.slice(start, end).join('\n') } if (range) { @@ -48,8 +72,8 @@ export function processIncludes( const lines = content.split(/\r?\n/) content = lines .slice( - startLine ? parseInt(startLine, 10) - 1 : undefined, - endLine ? parseInt(endLine, 10) : undefined + startLine ? parseInt(startLine) - 1 : undefined, + endLine ? parseInt(endLine) : undefined ) .join('\n') } @@ -60,7 +84,7 @@ export function processIncludes( includes.push(slash(includePath)) // recursively process includes in the content - return processIncludes(srcDir, content, includePath, includes) + return processIncludes(md, srcDir, content, includePath, includes) // } catch (error) {