mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-07-27 20:25:10 +00:00
Core: Support <auto-files /> syntax in remark-mdx-files plugin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Support `<auto-files />` syntax in `remark-mdx-files` plugin
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
"fumadocs-openapi": patch
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Fix Parameter Serialization
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
"fumadocs-obsidian": patch
|
||||
'fumadocs-obsidian': patch
|
||||
---
|
||||
|
||||
fix slugification
|
||||
|
||||
@@ -28,12 +28,20 @@ import { File, Folder, Files } from 'fumadocs-ui/components/files';
|
||||
</Files>
|
||||
```
|
||||
|
||||
### As CodeBlock
|
||||
### File
|
||||
|
||||
You can enable [`remark-mdx-files`](/docs/headless/mdx/remark-mdx-files) to define file structure with codeblocks.
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="FileProps" />
|
||||
|
||||
### Folder
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="FolderProps" />
|
||||
|
||||
## Remark Plugin
|
||||
|
||||
You can enable [`remark-mdx-files`](/docs/headless/mdx/remark-mdx-files) for additional feature & syntax.
|
||||
|
||||
```tsx title="source.config.ts (Fumadocs MDX)"
|
||||
import { remarkMdxFiles } from 'fumadocs-core/mdx-plugins';
|
||||
import { remarkMdxFiles } from 'fumadocs-core/mdx-plugins/remark-mdx-files';
|
||||
import { defineConfig } from 'fumadocs-mdx/config';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -44,7 +52,9 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
it will convert `files` codeblocks into MDX usage, like:
|
||||
### CodeBlock Syntax
|
||||
|
||||
It will convert `files` codeblocks into `<Files />` component, like:
|
||||
|
||||
````md title="content.md"
|
||||
```files
|
||||
@@ -57,10 +67,12 @@ project
|
||||
```
|
||||
````
|
||||
|
||||
### File
|
||||
### `<auto-files>`
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="FileProps" />
|
||||
Generate `<Files />` component from glob.
|
||||
|
||||
### Folder
|
||||
```mdx title="content.mdx"
|
||||
<auto-files dir="./my-dir" pattern="**/*.{ts,tsx}" />
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="FolderProps" />
|
||||
<auto-files dir="./my-dir" pattern="**/*.{ts,tsx}" defaultOpenAll />
|
||||
```
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"remark-rehype": "^11.1.2",
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"shiki": "^3.20.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -10,5 +10,5 @@ export * from './remark-code-tab';
|
||||
export * from './remark-steps';
|
||||
export * from './remark-npm';
|
||||
export * from './codeblock-utils';
|
||||
export * from './remark-mdx-files';
|
||||
export { remarkMdxFiles, type RemarkMdxFilesOptions } from './remark-mdx-files';
|
||||
export * from './remark-mdx-mermaid';
|
||||
|
||||
@@ -2,20 +2,26 @@ import type { Root } from 'mdast';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { Transformer } from 'unified';
|
||||
import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
||||
import type { VFile } from 'vfile';
|
||||
import path from 'node:path';
|
||||
|
||||
interface FileNode {
|
||||
export interface FileNode {
|
||||
depth: number;
|
||||
type: 'file';
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface FolderNode {
|
||||
export interface FolderNode {
|
||||
depth: number;
|
||||
type: 'folder';
|
||||
name: string;
|
||||
children: Node[];
|
||||
}
|
||||
|
||||
export interface ToMdxOptions {
|
||||
defaultOpenAll: boolean;
|
||||
}
|
||||
|
||||
type Node = FileNode | FolderNode;
|
||||
|
||||
export interface RemarkMdxFilesOptions {
|
||||
@@ -23,7 +29,12 @@ export interface RemarkMdxFilesOptions {
|
||||
* @defaultValue files
|
||||
*/
|
||||
lang?: string;
|
||||
toMdx?: (node: Node) => MdxJsxFlowElement;
|
||||
toMdx?: (node: Node, options: ToMdxOptions) => MdxJsxFlowElement;
|
||||
}
|
||||
|
||||
interface AutoFilesProps extends Partial<ToMdxOptions> {
|
||||
dir?: string;
|
||||
patterns?: string[];
|
||||
}
|
||||
|
||||
function parseFileTree(code: string) {
|
||||
@@ -65,13 +76,13 @@ function parseFileTree(code: string) {
|
||||
return stack.get(0);
|
||||
}
|
||||
|
||||
function defaultToMDX(node: Node, depth = 0): MdxJsxFlowElement {
|
||||
function defaultToMDX(node: Node, options: ToMdxOptions, depth = 0): MdxJsxFlowElement {
|
||||
if (depth === 0) {
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'Files',
|
||||
attributes: [],
|
||||
children: [defaultToMDX(node, depth + 1)],
|
||||
children: [defaultToMDX(node, options, depth + 1)],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,21 +99,26 @@ function defaultToMDX(node: Node, depth = 0): MdxJsxFlowElement {
|
||||
};
|
||||
}
|
||||
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'defaultOpen',
|
||||
value: null,
|
||||
});
|
||||
if (options.defaultOpenAll) {
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'defaultOpen',
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
attributes,
|
||||
name: 'Folder',
|
||||
children: node.children.map((item) => defaultToMDX(item, depth + 1)),
|
||||
children: node.children.map((item) => defaultToMDX(item, options, depth + 1)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* **Files CodeBlock:**
|
||||
*
|
||||
* Convert codeblocks with `files` as lang, like:
|
||||
*
|
||||
* ```files
|
||||
@@ -114,19 +130,129 @@ function defaultToMDX(node: Node, depth = 0): MdxJsxFlowElement {
|
||||
* ├── package.json
|
||||
* ```
|
||||
*
|
||||
* into MDX `<Files />` component
|
||||
* into MDX `<Files />` component.
|
||||
*
|
||||
* **Auto Files:**
|
||||
*
|
||||
* Generates MDX `<Files />` component from file system.
|
||||
*
|
||||
* ```mdx
|
||||
* <auto-files dir="scripts" pattern="my-dir/*" defaultOpenAll />
|
||||
* ```
|
||||
*/
|
||||
export function remarkMdxFiles(options: RemarkMdxFilesOptions = {}): Transformer<Root, Root> {
|
||||
const { lang = 'files', toMdx = defaultToMDX } = options;
|
||||
|
||||
return (tree) => {
|
||||
visit(tree, 'code', (node) => {
|
||||
if (node.lang !== lang || !node.value) return;
|
||||
async function autoFiles(
|
||||
file: VFile,
|
||||
node: MdxJsxFlowElement,
|
||||
{ patterns, dir, defaultOpenAll = false }: AutoFilesProps,
|
||||
) {
|
||||
const { glob } = await import('tinyglobby');
|
||||
if (!patterns) {
|
||||
file.fail('Missing `pattern` prop in <auto-files>', {
|
||||
place: node.position,
|
||||
});
|
||||
}
|
||||
|
||||
const fileTree = parseFileTree(node.value);
|
||||
if (!fileTree) return;
|
||||
const baseDir = file.dirname ?? file.cwd;
|
||||
const cwd = dir ? path.join(baseDir, dir) : baseDir;
|
||||
const files = await glob(patterns, { cwd });
|
||||
Object.assign(node, toMdx(buildFileTreeFromGlob(cwd, files), { defaultOpenAll }));
|
||||
}
|
||||
|
||||
Object.assign(node, toMdx(fileTree));
|
||||
return async (tree, file) => {
|
||||
const queue: Promise<void>[] = [];
|
||||
|
||||
visit(tree, ['code', 'mdxJsxFlowElement'] as const, (node) => {
|
||||
if (node.type === 'code') {
|
||||
if (node.lang !== lang || !node.value) return;
|
||||
|
||||
const fileTree = parseFileTree(node.value);
|
||||
if (!fileTree) return;
|
||||
|
||||
Object.assign(
|
||||
node,
|
||||
toMdx(fileTree, {
|
||||
defaultOpenAll: true,
|
||||
}),
|
||||
);
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
if (node.type === 'mdxJsxFlowElement') {
|
||||
if (node.name !== 'auto-files') return;
|
||||
const parsed: AutoFilesProps = {};
|
||||
for (const attr of node.attributes) {
|
||||
if (attr.type !== 'mdxJsxAttribute') continue;
|
||||
const { name, value } = attr;
|
||||
|
||||
switch (name) {
|
||||
case 'dir':
|
||||
if (typeof value === 'string') parsed.dir = value;
|
||||
break;
|
||||
case 'pattern':
|
||||
if (typeof value === 'string') {
|
||||
parsed.patterns ??= [];
|
||||
parsed.patterns.push(value);
|
||||
}
|
||||
break;
|
||||
case 'defaultOpenAll':
|
||||
parsed.defaultOpenAll = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
queue.push(autoFiles(file, node, parsed));
|
||||
return 'skip';
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(queue);
|
||||
};
|
||||
}
|
||||
|
||||
function buildFileTreeFromGlob(dir: string, files: string[]): Node {
|
||||
const nodeMap = new Map<string, FolderNode>();
|
||||
const root: FolderNode = {
|
||||
depth: 0,
|
||||
type: 'folder',
|
||||
name: path.basename(dir),
|
||||
children: [],
|
||||
};
|
||||
nodeMap.set('', root);
|
||||
|
||||
for (const file of files) {
|
||||
const parts = path
|
||||
.normalize(file)
|
||||
.split(path.sep)
|
||||
.filter((part) => part.length > 0);
|
||||
let currentPath = '';
|
||||
let current = root;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const name = parts[i];
|
||||
const nextPath = path.join(currentPath, name);
|
||||
|
||||
if (i === parts.length - 1) {
|
||||
// Add file node.
|
||||
const fileNode: FileNode = { depth: i + 1, type: 'file', name };
|
||||
current.children.push(fileNode);
|
||||
} else {
|
||||
// Add or retrieve folder node using the map.
|
||||
let folder = nodeMap.get(nextPath);
|
||||
|
||||
if (!folder) {
|
||||
folder = { depth: i + 1, type: 'folder', name, children: [] };
|
||||
nodeMap.set(nextPath, folder);
|
||||
current.children.push(folder);
|
||||
}
|
||||
|
||||
current = folder;
|
||||
currentPath = nextPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
```files
|
||||
project
|
||||
├── src
|
||||
│ ├── index.js
|
||||
│ └── utils
|
||||
│ └── helper.js
|
||||
├── package.json
|
||||
```
|
||||
|
||||
<auto-files dir="page-trees" pattern="**/*.json" />
|
||||
@@ -11,3 +11,19 @@
|
||||
<File name="package.json" />
|
||||
</Folder>
|
||||
</Files>
|
||||
|
||||
<Files>
|
||||
<Folder name="page-trees">
|
||||
<File name="basic.tree.json" />
|
||||
|
||||
<File name="i18n-dir.tree.json" />
|
||||
|
||||
<File name="i18n-no-prefix.tree.json" />
|
||||
|
||||
<File name="i18n.entries.json" />
|
||||
|
||||
<File name="i18n.tree.json" />
|
||||
|
||||
<File name="nested.tree.json" />
|
||||
</Folder>
|
||||
</Files>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { remark } from 'remark';
|
||||
import {
|
||||
@@ -16,7 +16,6 @@ import { fileURLToPath } from 'node:url';
|
||||
import remarkMdx from 'remark-mdx';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { createProcessor } from '@mdx-js/mdx';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { remarkSteps } from '@/mdx-plugins/remark-steps';
|
||||
import remarkDirective from 'remark-directive';
|
||||
|
||||
@@ -24,7 +23,7 @@ const cwd = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
test('Remark Heading', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-heading.md');
|
||||
const content = readFileSync(file);
|
||||
const content = await fs.readFile(file);
|
||||
|
||||
const result = await remark().use(remarkHeading).process(content);
|
||||
|
||||
@@ -33,8 +32,21 @@ test('Remark Heading', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Mdx Files', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-mdx-files.mdx');
|
||||
const content = await fs.readFile(file);
|
||||
|
||||
const result = await remark().use(remarkMdx).use(remarkMdxFiles).process({
|
||||
path: file,
|
||||
value: content,
|
||||
});
|
||||
await expect(String(result.value)).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-mdx-files.output.mdx'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Structure', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-structure.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-structure.md'));
|
||||
const result = await remark().use(remarkGfm).use(remarkStructure).process(content);
|
||||
|
||||
await expect(result.data.structuredData).toMatchFileSnapshot(
|
||||
@@ -43,7 +55,7 @@ test('Remark Structure', async () => {
|
||||
});
|
||||
|
||||
test('Remark Admonition', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-admonition.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-admonition.md'));
|
||||
const processor = remark().use(remarkMdx).use(remarkDirective).use(remarkDirectiveAdmonition);
|
||||
let tree = processor.parse(content);
|
||||
tree = await processor.run(tree);
|
||||
@@ -65,7 +77,7 @@ test('Remark Steps', async () => {
|
||||
|
||||
test('Remark Image: With Path', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-image.md');
|
||||
const content = readFileSync(file);
|
||||
const content = await fs.readFile(file);
|
||||
const processor = remark()
|
||||
.use(remarkImage, { publicDir: path.resolve(cwd, './fixtures') })
|
||||
.use(remarkMdx);
|
||||
@@ -80,7 +92,7 @@ test('Remark Image: With Path', async () => {
|
||||
});
|
||||
|
||||
test('Remark Image: Without Import', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-image.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-image.md'));
|
||||
const result = await remark()
|
||||
.use(remarkImage, {
|
||||
publicDir: path.resolve(cwd, './fixtures'),
|
||||
@@ -95,7 +107,7 @@ test('Remark Image: Without Import', async () => {
|
||||
});
|
||||
|
||||
test('Remark Image: `publicDir` with URL', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-image-public-dir.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-image-public-dir.md'));
|
||||
const result = await remark()
|
||||
.use(remarkImage, {
|
||||
publicDir: 'https://fumadocs.dev',
|
||||
@@ -109,17 +121,8 @@ test('Remark Image: `publicDir` with URL', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark MDX Files', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-mdx-files.md'));
|
||||
const result = await remark().use(remarkMdxFiles).use(remarkMdx).process(content);
|
||||
|
||||
await expect(result.value).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-mdx-files.output.mdx'),
|
||||
);
|
||||
});
|
||||
|
||||
test('converts mermaid codeblock to MDX Mermaid component', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-mdx-mermaid.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-mdx-mermaid.md'));
|
||||
const result = await remark().use(remarkMdxMermaid).use(remarkMdx).process(content);
|
||||
|
||||
await expect(result.value).toMatchFileSnapshot(
|
||||
@@ -128,7 +131,7 @@ test('converts mermaid codeblock to MDX Mermaid component', async () => {
|
||||
});
|
||||
|
||||
test('Rehype Toc', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/rehype-toc.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/rehype-toc.md'));
|
||||
|
||||
const processor = createProcessor({
|
||||
remarkPlugins: [remarkHeading],
|
||||
|
||||
Generated
+3
@@ -1571,6 +1571,9 @@ importers:
|
||||
shiki:
|
||||
specifier: ^3.20.0
|
||||
version: 3.20.0
|
||||
tinyglobby:
|
||||
specifier: ^0.2.15
|
||||
version: 0.2.15
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
|
||||
Reference in New Issue
Block a user