UNPKG

fumadocs-mdx

Version:

The built-in source for Fumadocs

247 lines (243 loc) 7.68 kB
import { t as fumaMatter } from "./fuma-matter-CHgJa_-B.js"; import * as fs$1 from "node:fs/promises"; import * as path$1 from "node:path"; import { unified } from "unified"; import { visit } from "unist-util-visit"; import { remarkHeading } from "fumadocs-core/mdx-plugins"; import { VFile } from "vfile"; //#region src/loaders/mdx/remark-unravel.ts function remarkMarkAndUnravel() { return (tree) => { visit(tree, function(node, index, parent) { let offset = -1; let all = true; let oneOrMore = false; if (parent && typeof index === "number" && node.type === "paragraph") { const children = node.children; while (++offset < children.length) { const child = children[offset]; if (child.type === "mdxJsxTextElement" || child.type === "mdxTextExpression") oneOrMore = true; else if (child.type === "text" && child.value.trim().length === 0) {} else { all = false; break; } } if (all && oneOrMore) { offset = -1; const newChildren = []; while (++offset < children.length) { const child = children[offset]; if (child.type === "mdxJsxTextElement") child.type = "mdxJsxFlowElement"; if (child.type === "mdxTextExpression") child.type = "mdxFlowExpression"; if (child.type === "text" && /^[\t\r\n ]+$/.test(String(child.value))) {} else newChildren.push(child); } parent.children.splice(index, 1, ...newChildren); return index; } } }); }; } //#endregion //#region src/loaders/mdx/mdast-utils.ts function flattenNode(node) { if ("children" in node) return node.children.map((child) => flattenNode(child)).join(""); if ("value" in node) return node.value; return ""; } //#endregion //#region src/loaders/mdx/remark-include.ts /** * VS Code–style region extraction * Adapted from VitePress: * https://github.com/vuejs/vitepress/blob/main/src/node/markdown/plugins/snippet.ts */ const REGION_MARKERS = [ { start: /^\s*\/\/\s*#?region\b\s*(.*?)\s*$/, end: /^\s*\/\/\s*#?endregion\b\s*(.*?)\s*$/ }, { start: /^\s*<!--\s*#?region\b\s*(.*?)\s*-->/, end: /^\s*<!--\s*#?endregion\b\s*(.*?)\s*-->/ }, { start: /^\s*\/\*\s*#region\b\s*(.*?)\s*\*\//, end: /^\s*\/\*\s*#endregion\b\s*(.*?)\s*\*\// }, { start: /^\s*#[rR]egion\b\s*(.*?)\s*$/, end: /^\s*#[eE]nd ?[rR]egion\b\s*(.*?)\s*$/ }, { start: /^\s*#\s*#?region\b\s*(.*?)\s*$/, end: /^\s*#\s*#?endregion\b\s*(.*?)\s*$/ }, { start: /^\s*(?:--|::|@?REM)\s*#region\b\s*(.*?)\s*$/, end: /^\s*(?:--|::|@?REM)\s*#endregion\b\s*(.*?)\s*$/ }, { start: /^\s*#pragma\s+region\b\s*(.*?)\s*$/, end: /^\s*#pragma\s+endregion\b\s*(.*?)\s*$/ }, { start: /^\s*\(\*\s*#region\b\s*(.*?)\s*\*\)/, end: /^\s*\(\*\s*#endregion\b\s*(.*?)\s*\*\)/ } ]; function dedent(lines) { const minIndent = lines.reduce((min, line) => { const match = line.match(/^(\s*)\S/); return match ? Math.min(min, match[1].length) : min; }, Infinity); return minIndent === Infinity ? lines.join("\n") : lines.map((l) => l.slice(minIndent)).join("\n"); } function extractCodeRegion(content, regionName) { const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) for (const re of REGION_MARKERS) { let match = re.start.exec(lines[i]); if (match?.[1] !== regionName) continue; let depth = 1; const extractedLines = []; for (let j = i + 1; j < lines.length; j++) { match = re.start.exec(lines[j]); if (match) { depth++; continue; } match = re.end.exec(lines[j]); if (match) { if (match[1] === regionName) depth = 0; else if (match[1] === "") depth--; else continue; if (depth > 0) continue; return dedent(extractedLines); } else extractedLines.push(lines[j]); } } throw new Error(`Region "${regionName}" not found`); } const ElementLikeTypes = [ "mdxJsxFlowElement", "mdxJsxTextElement", "containerDirective", "textDirective", "leafDirective" ]; function isElementLike(node) { return ElementLikeTypes.includes(node.type); } function parseElementAttributes(element) { if (Array.isArray(element.attributes)) { const attributes = {}; for (const attr of element.attributes) if (attr.type === "mdxJsxAttribute" && (typeof attr.value === "string" || attr.value === null)) attributes[attr.name] = attr.value; return attributes; } return element.attributes ?? {}; } function parseSpecifier(specifier) { const idx = specifier.lastIndexOf("#"); if (idx === -1) return { file: specifier }; return { file: specifier.slice(0, idx), section: specifier.slice(idx + 1) }; } function extractSection(root, section) { let nodes; let capturingHeadingContent = false; visit(root, (node) => { if (node.type === "heading") { if (capturingHeadingContent) return false; if (node.data?.hProperties?.id === section) { capturingHeadingContent = true; nodes = [node]; return "skip"; } return; } if (capturingHeadingContent) { nodes?.push(node); return "skip"; } if (isElementLike(node) && node.name === "section") { if (parseElementAttributes(node).id === section) { nodes = node.children; return false; } } }); if (nodes) return { type: "root", children: nodes }; } function remarkInclude() { const TagName = "include"; const embedContent = async (targetPath, heading, params, parent) => { const { _getProcessor = () => this, _compiler } = parent.data; let content; try { content = (await fs$1.readFile(targetPath)).toString(); } catch (e) { throw new Error(`failed to read file ${targetPath}\n${e instanceof Error ? e.message : String(e)}`, { cause: e }); } const ext = path$1.extname(targetPath); _compiler?.addDependency(targetPath); if (params.lang || ext !== ".md" && ext !== ".mdx") { const lang = params.lang ?? ext.slice(1); let value = content; if (heading) value = extractCodeRegion(content, heading.trim()); return { type: "code", lang, meta: params.meta, value, data: {} }; } const parser = _getProcessor(ext === ".mdx" ? "mdx" : "md"); const parsed = fumaMatter(content); const targetFile = new VFile({ path: targetPath, value: parsed.content, data: { ...parent.data, frontmatter: parsed.data } }); let mdast = parser.parse(targetFile); const baseProcessor = unified().use(remarkMarkAndUnravel); if (heading) { const extracted = extractSection(await baseProcessor.use(remarkHeading).run(mdast), heading); if (!extracted) throw new Error(`Cannot find section ${heading} in ${targetPath}, make sure you have encapsulated the section in a <section id="${heading}"> tag, or a :::section directive with remark-directive configured.`); mdast = extracted; } else mdast = await baseProcessor.run(mdast); await update(mdast, targetFile); return mdast; }; async function update(tree, file) { const queue = []; visit(tree, ElementLikeTypes, (_node, _, parent) => { const node = _node; if (node.name !== TagName) return; const specifier = flattenNode(node); if (specifier.length === 0) return "skip"; const attributes = parseElementAttributes(node); const { file: relativePath, section } = parseSpecifier(specifier); const targetPath = path$1.resolve("cwd" in attributes ? file.cwd : file.dirname, relativePath); queue.push(embedContent(targetPath, section, attributes, file).then((replace) => { Object.assign(parent && parent.type === "paragraph" ? parent : node, replace); })); return "skip"; }); await Promise.all(queue); } return async (tree, file) => { await update(tree, file); }; } //#endregion export { flattenNode as n, remarkInclude as t }; //# sourceMappingURL=remark-include-D3G3mAnv.js.map