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.
115 lines
3.3 KiB
115 lines
3.3 KiB
import matter from 'gray-matter'
|
|
import { replaceAsync, type MarkdownItAsync } from 'markdown-it-async'
|
|
import path from 'node:path'
|
|
import { findRegions } from '../markdown/regions'
|
|
import { slash, type MarkdownEnv } from '../shared'
|
|
import { readTextFile } from './fs'
|
|
|
|
export function processIncludes(
|
|
md: MarkdownItAsync,
|
|
srcDir: string,
|
|
src: string,
|
|
file: string,
|
|
includes: string[],
|
|
cleanUrls: boolean,
|
|
ancestors: string[] = []
|
|
): Promise<string> {
|
|
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
|
|
const regionRE = /(#[^\s\{]+)/
|
|
const rangeRE = /\{(\d*),(\d*)\}$/
|
|
|
|
return replaceAsync(src, includesRE, async (m: string, m1: string) => {
|
|
if (!m1.length) return m
|
|
|
|
const range = m1.match(rangeRE)
|
|
const region = m1.match(regionRE)
|
|
|
|
const hasMeta = !!(region || range)
|
|
|
|
if (hasMeta) {
|
|
const len = (region?.[0].length || 0) + (range?.[0].length || 0)
|
|
m1 = m1.slice(0, -len) // remove meta info from the include path
|
|
}
|
|
|
|
const atPresent = m1[0] === '@'
|
|
|
|
const includePath = atPresent
|
|
? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1))
|
|
: path.join(path.dirname(file), m1)
|
|
|
|
// leave circular includes unexpanded — only repeats along the ancestor
|
|
// chain are cycles, the same file may still be included by siblings
|
|
if (includePath === file || ancestors.includes(includePath)) return m
|
|
|
|
let content = await readTextFile(includePath)
|
|
|
|
if (region) {
|
|
const [regionName] = region
|
|
const lines = content.split(/\r?\n/)
|
|
let selectedLines = lines
|
|
let { start, end } = findRegions(lines, regionName.slice(1))[0] ?? {}
|
|
|
|
if (start === undefined) {
|
|
// region not found, it might be a header
|
|
const headerContent =
|
|
path.extname(includePath) === '.md'
|
|
? matter(content, {}).content
|
|
: content
|
|
const headerLines = headerContent.split(/\r?\n/)
|
|
const tokens = md
|
|
.parse(headerContent, {
|
|
path: includePath,
|
|
relativePath: slash(path.relative(srcDir, includePath)),
|
|
cleanUrls
|
|
} satisfies MarkdownEnv)
|
|
.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) {
|
|
selectedLines = headerLines
|
|
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]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
content = selectedLines.slice(start, end).join('\n')
|
|
}
|
|
|
|
if (range) {
|
|
const [, startLine, endLine] = range
|
|
const lines = content.split(/\r?\n/)
|
|
content = lines
|
|
.slice(
|
|
startLine ? parseInt(startLine) - 1 : undefined,
|
|
endLine ? parseInt(endLine) : undefined
|
|
)
|
|
.join('\n')
|
|
}
|
|
|
|
if (!hasMeta && path.extname(includePath) === '.md') {
|
|
content = matter(content, {}).content
|
|
}
|
|
|
|
includes.push(slash(includePath))
|
|
|
|
// recursively process includes in the content
|
|
return processIncludes(
|
|
md,
|
|
srcDir,
|
|
content,
|
|
includePath,
|
|
includes,
|
|
cleanUrls,
|
|
[...ancestors, file]
|
|
)
|
|
})
|
|
}
|