MDX: Support reusing codeblocks in <include>

This commit is contained in:
Fuma Nama
2025-03-18 21:23:44 +08:00
parent b9ce39b3f2
commit 5163e9202a
4 changed files with 80 additions and 42 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': patch
---
Support reusing codeblocks in `<include>`
+26 -2
View File
@@ -1,14 +1,38 @@
---
title: Include
description: Reuse content from other Markdown files
description: Reuse content from other files.
---
## Usage
Specify the target Markdown file path in `<include>` tag (relative).
### Markdown
Specify the target Markdown file path in `<include>` tag (relative to the Markdown file itself).
```mdx title="page.mdx"
<include>./another.mdx</include>
```
This will display the content from target file (e.g. `another.mdx`).
### CodeBlock
For other types of files, it will become a codeblock:
```mdx title="page.mdx"
<include>./script.ts</include>
<include lang="tsx" meta='title="lib.ts"'>
./script.ts
</include>
```
### `cwd`
Resolve relative paths from cwd instead of Markdown file:
```mdx
<include cwd lang="tsx" meta='title="lib.ts"'>
./script.ts
</include>
```
@@ -16,15 +16,9 @@ description: Support multiple languages in your documentation
Define the i18n configurations in a file, we will import it with `@/ilb/i18n` in this guide.
```json doc-gen:file
{
"file": "../../examples/i18n/lib/i18n.ts",
"codeblock": {
"lang": "ts",
"meta": "title=\"lib/i18n.ts\""
}
}
```
<include cwd meta='title="lib/i18n.ts"'>
../../examples/i18n/lib/i18n.ts
</include>
Pass it to the source loader.
+46 -31
View File
@@ -1,11 +1,20 @@
import type { Processor, Transformer } from 'unified';
import { visit } from 'unist-util-visit';
import type { Literal, Root } from 'mdast';
import type { Code, Root, RootContent } from 'mdast';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import matter from 'gray-matter';
import type { CompilerOptions } from '@/utils/build-mdx';
function flattenNode(node: RootContent): string {
if ('children' in node)
return node.children.map((child) => flattenNode(child)).join('');
if ('value' in node) return node.value;
return '';
}
export function remarkInclude(this: Processor): Transformer<Root, Root> {
const TagName = 'include';
@@ -17,50 +26,56 @@ export function remarkInclude(this: Processor): Transformer<Root, Root> {
) {
const queue: Promise<void>[] = [];
visit(tree, ['mdxJsxFlowElement', 'paragraph'] as const, (node) => {
visit(tree, 'mdxJsxFlowElement', (node) => {
let specifier: string | undefined;
const params: Record<string, string | null> = {};
// without MDX, parse from paragraph nodes
if (node.type === 'paragraph' && node.children.length === 3) {
const [open, content, closure] = node.children;
if (node.name === TagName) {
const value = flattenNode(node);
if (
open.type === 'html' &&
open.value === `<${TagName}>` &&
content.type === 'text' &&
closure.type === 'html' &&
closure.value === `</${TagName}>`
) {
specifier = content.value.trim();
}
} else if (node.type === 'paragraph' && node.children.length === 1) {
const child = node.children[0];
if (child.type === 'mdxJsxTextElement' && child.name === TagName) {
const text = child.children.at(0);
if (text && text.type === 'text') {
specifier = text.value;
if (value.length > 0) {
for (const attr of node.attributes) {
if (
attr.type === 'mdxJsxAttribute' &&
(typeof attr.value === 'string' || attr.value === null)
) {
params[attr.name] = attr.value;
}
}
}
}
if (node.type === 'mdxJsxFlowElement' && node.name === TagName) {
const child = node.children.at(0) as Literal | undefined;
if (child && child.type === 'text') {
specifier = child.value;
specifier = value;
}
}
if (!specifier) return;
const targetPath = path.resolve(path.dirname(file), specifier);
const targetPath = path.resolve(
'cwd' in params ? process.cwd() : path.dirname(file),
specifier,
);
const asCode =
params.lang ||
(!specifier.endsWith('.md') && !specifier.endsWith('.mdx'));
queue.push(
fs.readFile(targetPath).then(async (content) => {
compiler?.addDependency(targetPath);
if (asCode) {
const lang = params.lang ?? path.extname(specifier).slice(1);
Object.assign(node, {
type: 'code',
lang,
meta: params.meta,
value: content.toString(),
data: {},
} satisfies Code);
return;
}
const parsed = processor.parse(matter(content).content);
compiler?.addDependency(targetPath);
await update(parsed as Root, targetPath, processor, compiler);
Object.assign(node, parsed);
}),