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 { const includesRE = //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] ) }) }