mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
Core: Support duplicated names with meta and page files
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
Support duplicated names with meta and page files
|
||||
@@ -1,9 +1,9 @@
|
||||
import type * as PageTree from '@/server/page-tree';
|
||||
import { load, type LoadOptions, type VirtualFile } from './load';
|
||||
import type { MetaData, PageData, Transformer } from './types';
|
||||
import type { FileInfo, MetaData, PageData, Transformer } from './types';
|
||||
import type { CreatePageTreeBuilderOptions } from './page-tree-builder';
|
||||
import { createPageTreeBuilder } from './page-tree-builder';
|
||||
import { joinPaths } from './path';
|
||||
import { joinPaths, splitPath } from './path';
|
||||
import type { Node, Page } from './file-system';
|
||||
|
||||
interface LoaderConfig {
|
||||
@@ -106,6 +106,12 @@ function createGetUrl(
|
||||
};
|
||||
}
|
||||
|
||||
function getSlugs(info: FileInfo): string[] {
|
||||
const result = [...splitPath(info.dirname), info.name];
|
||||
|
||||
return result[result.length - 1] === 'index' ? result.slice(0, -1) : result;
|
||||
}
|
||||
|
||||
type InferSourceConfig<T> = T extends Source<infer Config> ? Config : never;
|
||||
|
||||
export function loader<Options extends LoaderOptions>(
|
||||
@@ -124,10 +130,7 @@ function createOutput({
|
||||
rootDir = '',
|
||||
transformers,
|
||||
baseUrl = '/',
|
||||
slugs = (info) => {
|
||||
const result = [...info.dirname.split('/'), info.name].filter(Boolean);
|
||||
return result[result.length - 1] === 'index' ? result.slice(0, -1) : result;
|
||||
},
|
||||
slugs = getSlugs,
|
||||
url = createGetUrl(baseUrl),
|
||||
}: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
const result = load({
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface Folder<
|
||||
export type Node<
|
||||
MD extends MetaData = MetaData,
|
||||
PD extends PageData = PageData,
|
||||
> = Meta<MD> | Page<PD> | Folder;
|
||||
> = Meta<MD> | Page<PD> | Folder<MD, PD>;
|
||||
|
||||
/**
|
||||
* A virtual file system that solves inconsistent behaviours
|
||||
@@ -48,11 +48,30 @@ export class Storage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file, it doesn't need an extension
|
||||
* @param path - flattened path
|
||||
*/
|
||||
read(path: string): Page | Meta | undefined {
|
||||
return this.files.get(path);
|
||||
private read(path: string, type: string): Page | Meta | undefined {
|
||||
return this.files.get(`${path}.${type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path - flattened path
|
||||
*/
|
||||
readPage(path: string): Page | undefined {
|
||||
const result = this.read(path, 'page');
|
||||
if (result?.type !== 'page') return;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path - flattened path
|
||||
*/
|
||||
readMeta(path: string): Meta | undefined {
|
||||
const result = this.read(path, 'meta');
|
||||
if (result?.type !== 'meta') return;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
readDir(path: string): Folder | undefined {
|
||||
@@ -71,7 +90,7 @@ export class Storage {
|
||||
|
||||
this.makeDir(node.file.dirname);
|
||||
this.readDir(node.file.dirname)?.children.push(node);
|
||||
this.files.set(node.file.flattenedPath, node);
|
||||
this.files.set(`${node.file.flattenedPath}.${node.type}`, node);
|
||||
}
|
||||
|
||||
list(): (Page | Meta)[] {
|
||||
|
||||
@@ -7,6 +7,7 @@ interface PageTreeBuilderContext {
|
||||
storage: Storage;
|
||||
lang?: string;
|
||||
|
||||
builder: PageTreeBuilder;
|
||||
resolveIcon: (icon: string | undefined) => ReactElement | undefined;
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ function buildAll(
|
||||
}
|
||||
|
||||
if (node.type === 'folder') {
|
||||
output.push(buildFolderNode(node, ctx));
|
||||
output.push(buildFolderNode(node, false, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,29 +73,70 @@ function getFolderMeta(
|
||||
folder: Folder,
|
||||
ctx: PageTreeBuilderContext,
|
||||
): Meta | undefined {
|
||||
let meta = ctx.storage.read(joinPaths([folder.file.path, 'meta']));
|
||||
let meta = ctx.storage.readMeta(joinPaths([folder.file.path, 'meta']));
|
||||
|
||||
if (ctx.lang) {
|
||||
meta =
|
||||
ctx.storage.read(joinPaths([folder.file.path, `meta.${ctx.lang}`])) ??
|
||||
ctx.storage.readMeta(joinPaths([folder.file.path, `meta.${ctx.lang}`])) ??
|
||||
meta;
|
||||
}
|
||||
|
||||
if (meta?.type === 'meta') return meta;
|
||||
return meta;
|
||||
}
|
||||
|
||||
function resolveFolderItem(
|
||||
folder: Folder,
|
||||
item: string,
|
||||
ctx: PageTreeBuilderContext,
|
||||
addedNodePaths: Set<string>,
|
||||
): PageTree.Node[] | '...' {
|
||||
if (item === rest) return '...';
|
||||
|
||||
const separateResult = separator.exec(item);
|
||||
|
||||
if (separateResult?.groups) {
|
||||
return [
|
||||
{
|
||||
type: 'separator',
|
||||
name: separateResult.groups.name,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const extractResult = extractor.exec(item);
|
||||
|
||||
const path = joinPaths([
|
||||
folder.file.path,
|
||||
extractResult?.groups?.name ?? item,
|
||||
]);
|
||||
const itemNode = ctx.storage.readDir(path) ?? ctx.storage.readPage(path);
|
||||
|
||||
if (!itemNode) return [];
|
||||
|
||||
addedNodePaths.add(itemNode.file.path);
|
||||
|
||||
if (itemNode.type === 'folder') {
|
||||
const node = buildFolderNode(itemNode, false, ctx);
|
||||
|
||||
return extractResult?.groups ? node.children : [node];
|
||||
}
|
||||
|
||||
return [buildFileNode(itemNode, ctx)];
|
||||
}
|
||||
|
||||
function buildFolderNode(
|
||||
folder: Folder,
|
||||
defaultIsRoot: boolean,
|
||||
ctx: PageTreeBuilderContext,
|
||||
defaultIsRoot = false,
|
||||
): PageTree.Folder {
|
||||
const indexNode = folder.children.find(
|
||||
(node) => node.type === 'page' && node.file.name === 'index',
|
||||
) as Page | undefined;
|
||||
const index = indexNode ? buildFileNode(indexNode, ctx) : undefined;
|
||||
const indexFile = ctx.storage.readPage(
|
||||
joinPaths([folder.file.flattenedPath, 'index']),
|
||||
);
|
||||
|
||||
const index = indexFile ? buildFileNode(indexFile, ctx) : undefined;
|
||||
const meta = getFolderMeta(folder, ctx)?.data;
|
||||
|
||||
let children: PageTree.Folder['children'];
|
||||
let children: PageTree.Node[];
|
||||
|
||||
if (!meta) {
|
||||
children = buildAll(folder.children, ctx, !defaultIsRoot);
|
||||
@@ -103,39 +145,7 @@ function buildFolderNode(
|
||||
const addedNodePaths = new Set<string>();
|
||||
|
||||
const resolved = meta.pages?.flatMap<PageTree.Node | '...'>((item) => {
|
||||
if (item === rest) return '...';
|
||||
|
||||
const result = separator.exec(item);
|
||||
|
||||
if (result?.groups) {
|
||||
return {
|
||||
type: 'separator',
|
||||
name: result.groups.name,
|
||||
};
|
||||
}
|
||||
|
||||
const extractResult = extractor.exec(item);
|
||||
const extractName = extractResult?.groups?.name ?? item;
|
||||
|
||||
const itemNode =
|
||||
ctx.storage.readDir(joinPaths([folder.file.path, extractName])) ??
|
||||
ctx.storage.read(joinPaths([folder.file.path, extractName]));
|
||||
|
||||
if (!itemNode) return [];
|
||||
|
||||
addedNodePaths.add(itemNode.file.path);
|
||||
|
||||
if (itemNode.type === 'folder') {
|
||||
const node = buildFolderNode(itemNode, ctx);
|
||||
|
||||
return extractResult?.groups ? node.children : node;
|
||||
}
|
||||
|
||||
if (itemNode.type === 'page') {
|
||||
return buildFileNode(itemNode, ctx);
|
||||
}
|
||||
|
||||
return [];
|
||||
return resolveFolderItem(folder, item, ctx, addedNodePaths);
|
||||
});
|
||||
|
||||
const restNodes = buildAll(
|
||||
@@ -169,9 +179,11 @@ function buildFolderNode(
|
||||
function buildFileNode(page: Page, ctx: PageTreeBuilderContext): PageTree.Item {
|
||||
let localePage = page;
|
||||
if (ctx.lang) {
|
||||
const result = ctx.storage.read(`${page.file.flattenedPath}.${ctx.lang}`);
|
||||
const result = ctx.storage.readPage(
|
||||
`${page.file.flattenedPath}.${ctx.lang}`,
|
||||
);
|
||||
|
||||
if (result?.type === 'page') localePage = result;
|
||||
if (result) localePage = result;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -184,7 +196,7 @@ function buildFileNode(page: Page, ctx: PageTreeBuilderContext): PageTree.Item {
|
||||
|
||||
function build(ctx: PageTreeBuilderContext): PageTree.Root {
|
||||
const root = ctx.storage.root();
|
||||
const folder = buildFolderNode(root, ctx, true);
|
||||
const folder = buildFolderNode(root, true, ctx);
|
||||
|
||||
return {
|
||||
name: folder.name,
|
||||
@@ -196,24 +208,28 @@ export function createPageTreeBuilder({
|
||||
storage,
|
||||
resolveIcon = () => undefined,
|
||||
}: CreatePageTreeBuilderOptions): PageTreeBuilder {
|
||||
const context: PageTreeBuilderContext = {
|
||||
storage,
|
||||
resolveIcon(icon) {
|
||||
if (!icon) return;
|
||||
return resolveIcon(icon);
|
||||
},
|
||||
};
|
||||
function getContext(
|
||||
builder: PageTreeBuilder,
|
||||
locale?: string,
|
||||
): PageTreeBuilderContext {
|
||||
return {
|
||||
storage,
|
||||
lang: locale,
|
||||
resolveIcon(icon) {
|
||||
if (!icon) return;
|
||||
return resolveIcon(icon);
|
||||
},
|
||||
builder,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
build() {
|
||||
return build(context);
|
||||
return build(getContext(this));
|
||||
},
|
||||
buildI18n({ languages = [] } = {}) {
|
||||
const entries = languages.map<[string, PageTree.Root]>((lang) => {
|
||||
const tree = build({
|
||||
...context,
|
||||
lang,
|
||||
});
|
||||
const tree = build(getContext(this, lang));
|
||||
|
||||
return [lang, tree];
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { loader } from '../src/source';
|
||||
import type * as PageTree from '../src/server/page-tree';
|
||||
|
||||
describe('Page Tree Builder', () => {
|
||||
test('Simple', () => {
|
||||
@@ -25,10 +24,19 @@ describe('Page Tree Builder', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.pageTree).toEqual({
|
||||
name: '',
|
||||
children: [{ type: 'page', name: 'Hello', url: '/test' }],
|
||||
} satisfies PageTree.Root);
|
||||
expect(result.pageTree).toMatchInlineSnapshot(`
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"icon": undefined,
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/test",
|
||||
},
|
||||
],
|
||||
"name": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('Nested Directories', async () => {
|
||||
@@ -60,23 +68,35 @@ describe('Page Tree Builder', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.pageTree).toEqual({
|
||||
name: '',
|
||||
children: [
|
||||
{ type: 'page', name: 'Hello', url: '/test' },
|
||||
{
|
||||
type: 'folder',
|
||||
name: 'Nested',
|
||||
children: [
|
||||
{
|
||||
type: 'page',
|
||||
name: 'Nested Page',
|
||||
url: '/nested/test',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} satisfies PageTree.Root);
|
||||
expect(result.pageTree).toMatchInlineSnapshot(`
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"icon": undefined,
|
||||
"name": "Hello",
|
||||
"type": "page",
|
||||
"url": "/test",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"icon": undefined,
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/nested/test",
|
||||
},
|
||||
],
|
||||
"defaultOpen": undefined,
|
||||
"icon": undefined,
|
||||
"index": undefined,
|
||||
"name": "Nested",
|
||||
"root": undefined,
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('Internationalized Routing', () => {
|
||||
@@ -138,22 +158,34 @@ describe('Page Tree Builder', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.pageTree['cn']).toEqual({
|
||||
name: 'Docs Chinese',
|
||||
children: [
|
||||
{ type: 'page', name: 'Hello Chinese', url: '/cn/test' },
|
||||
{
|
||||
type: 'folder',
|
||||
name: 'Nested Chinese',
|
||||
children: [
|
||||
{
|
||||
type: 'page',
|
||||
name: 'Nested Page Chinese',
|
||||
url: '/cn/nested/test',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} satisfies PageTree.Root);
|
||||
expect(result.pageTree['cn']).toMatchInlineSnapshot(`
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"icon": undefined,
|
||||
"name": "Hello Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/test",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"icon": undefined,
|
||||
"name": "Nested Page Chinese",
|
||||
"type": "page",
|
||||
"url": "/cn/nested/test",
|
||||
},
|
||||
],
|
||||
"defaultOpen": undefined,
|
||||
"icon": undefined,
|
||||
"index": undefined,
|
||||
"name": "Nested Chinese",
|
||||
"root": undefined,
|
||||
"type": "folder",
|
||||
},
|
||||
],
|
||||
"name": "Docs Chinese",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,8 +19,8 @@ describe('Virtual Storage', () => {
|
||||
});
|
||||
|
||||
test('Reading', () => {
|
||||
expect(storage.read('test')).toBeDefined();
|
||||
expect(storage.read('meta')).toBeDefined();
|
||||
expect(storage.readPage('test')).toBeDefined();
|
||||
expect(storage.readMeta('meta')).toBeDefined();
|
||||
});
|
||||
|
||||
test('Nested Directories', () => {
|
||||
@@ -31,7 +31,7 @@ describe('Virtual Storage', () => {
|
||||
|
||||
expect(storage.readDir('dir1')).toBeDefined();
|
||||
expect(storage.readDir('dir1/dir2')).toBeDefined();
|
||||
expect(storage.read('dir1/dir2/meta')).toBeDefined();
|
||||
expect(storage.readMeta('dir1/dir2/meta')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user