Core: Support attaching custom properties during page tree builder process

This commit is contained in:
fuma-nama
2024-06-07 19:22:59 +08:00
parent 23678263e2
commit 57eb7629e3
4 changed files with 107 additions and 63 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': minor
---
Support attaching custom properties during page tree builder process
+5 -1
View File
@@ -21,8 +21,12 @@ export interface Separator {
}
export interface Folder {
/**
* Optional id to be attached to folders
*/
id?: string;
type: 'folder';
id: string;
name: string;
root?: boolean;
defaultOpen?: boolean;
+23 -10
View File
@@ -6,7 +6,7 @@ import {
type Transformer,
} from './load-files';
import type { FileData, MetaData, PageData, UrlFn } from './types';
import type { CreatePageTreeBuilderOptions } from './page-tree-builder';
import type { BuildPageTreeOptions } from './page-tree-builder';
import { createPageTreeBuilder } from './page-tree-builder';
import { splitPath, type FileInfo } from './path';
import type { File, Storage } from './file-system';
@@ -31,12 +31,17 @@ export interface LoaderOptions {
*/
baseUrl?: string;
languages?: string[];
icon?: NonNullable<CreatePageTreeBuilderOptions['resolveIcon']>;
icon?: NonNullable<BuildPageTreeOptions['resolveIcon']>;
slugs?: LoadOptions['getSlugs'];
url?: UrlFn;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Inevitable
source: Source<any>;
transformers?: Transformer[];
/**
* Additional options for page tree builder
*/
pageTree?: Partial<Omit<BuildPageTreeOptions, 'storage' | 'getUrl'>>;
}
export interface Source<Config extends SourceConfig> {
@@ -149,13 +154,14 @@ export function loader<Options extends LoaderOptions>(
function createOutput({
source,
icon,
icon: resolveIcon,
languages,
rootDir = '',
transformers,
baseUrl = '/',
slugs: slugsFn = getSlugs,
url: getUrl = createGetUrl(baseUrl),
pageTree: pageTreeOptions = {},
}: LoaderOptions): LoaderOutput<LoaderConfig> {
const storage = loadFiles(
typeof source.files === 'function' ? source.files(rootDir) : source.files,
@@ -166,15 +172,22 @@ function createOutput({
},
);
const i18nMap = buildPageMap(storage, languages ?? [], getUrl);
const builder = createPageTreeBuilder({
storage,
resolveIcon: icon,
getUrl,
});
const builder = createPageTreeBuilder();
const pageTree =
languages === undefined
? builder.build()
: builder.buildI18n({ languages });
? builder.build({
storage,
resolveIcon,
getUrl,
...pageTreeOptions,
})
: builder.buildI18n({
languages,
storage,
resolveIcon,
getUrl,
...pageTreeOptions,
});
return {
pageTree: pageTree as LoaderOutput<LoaderConfig>['pageTree'],
+74 -52
View File
@@ -4,34 +4,50 @@ import type { File, Folder, Storage } from './file-system';
import { resolvePath } from './path';
import { type FileData, type UrlFn } from './types';
interface PageTreeBuilderContext extends CreatePageTreeBuilderOptions {
interface PageTreeBuilderContext {
lang?: string;
storage: Storage;
builder: PageTreeBuilder;
options: BuildPageTreeOptions;
}
export interface BuildPageTreeOptionsWithI18n {
languages: string[];
export interface BuildPageTreeOptions {
/**
* Attach the `folder.id` property
*
* @default false
*/
attachFolderIds?: boolean;
attachFile?: (node: PageTree.Item, file?: File) => PageTree.Item;
attachFolder?: (
node: PageTree.Folder,
folder: Folder,
meta?: File,
) => PageTree.Folder;
attachSeparator?: (node: PageTree.Separator) => PageTree.Separator;
storage: Storage;
getUrl: UrlFn;
resolveIcon?: (icon: string | undefined) => ReactElement | undefined;
}
export interface BuildPageTreeOptionsWithI18n extends BuildPageTreeOptions {
languages?: string[];
}
export interface PageTreeBuilder {
build: () => PageTree.Root;
build: (options: BuildPageTreeOptions) => PageTree.Root;
/**
* Build page tree and fallback to the default language if the localized page doesn't exist
*/
buildI18n: (
options?: Partial<BuildPageTreeOptionsWithI18n>,
options: BuildPageTreeOptionsWithI18n,
) => Record<string, PageTree.Root>;
}
export interface CreatePageTreeBuilderOptions {
storage: Storage;
getUrl: UrlFn;
resolveIcon?: (icon: string | undefined) => ReactElement | undefined;
}
const link = /^\[(?<text>.+)]\((?<url>.+)\)$/;
const separator = /^---(?<name>.*?)---$/;
const rest = '...';
@@ -82,12 +98,12 @@ function resolveFolderItem(
const separateResult = separator.exec(item);
if (separateResult?.groups) {
return [
{
type: 'separator',
name: separateResult.groups.name,
},
];
const node: PageTree.Separator = {
type: 'separator',
name: separateResult.groups.name,
};
return [ctx.options.attachSeparator?.(node) ?? node];
}
const linkResult = link.exec(item);
@@ -95,14 +111,15 @@ function resolveFolderItem(
const { url, text } = linkResult.groups;
const isRelative =
url.startsWith('/') || url.startsWith('#') || url.startsWith('.');
return [
{
type: 'page',
name: text,
url,
external: !isRelative,
},
];
const node: PageTree.Item = {
type: 'page',
name: text,
url,
external: !isRelative,
};
return [ctx.options.attachFile?.(node) ?? node];
}
const extractResult = extractor.exec(item);
@@ -171,16 +188,23 @@ function buildFolderNode(
children = nodes ?? restNodes;
}
return removeUndefined({
const node: PageTree.Folder = {
type: 'folder',
id: folder.file.flattenedPath,
name: metadata?.title ?? index?.name ?? pathToName(folder.file.name),
icon: ctx.resolveIcon?.(metadata?.icon),
icon: ctx.options.resolveIcon?.(metadata?.icon),
root: metadata?.root,
defaultOpen: metadata?.defaultOpen,
index,
children,
});
};
if (ctx.options.attachFolderIds) {
node.id = folder.file.flattenedPath;
}
return removeUndefined(
ctx.options.attachFolder?.(node, folder, meta) ?? node,
);
}
function buildFileNode(file: File, ctx: PageTreeBuilderContext): PageTree.Item {
@@ -188,12 +212,14 @@ function buildFileNode(file: File, ctx: PageTreeBuilderContext): PageTree.Item {
findLocalizedFile(file.file.flattenedPath, 'page', ctx) ?? file;
const data = localized.data as FileData['file'];
return removeUndefined({
const item: PageTree.Item = {
type: 'page',
name: data.data.title,
icon: ctx.resolveIcon?.(data.data.icon),
url: ctx.getUrl(data.slugs, ctx.lang),
});
icon: ctx.options.resolveIcon?.(data.data.icon),
url: ctx.options.getUrl(data.slugs, ctx.lang),
};
return removeUndefined(ctx.options.attachFile?.(item, file) ?? item);
}
function build(ctx: PageTreeBuilderContext): PageTree.Root {
@@ -206,27 +232,23 @@ function build(ctx: PageTreeBuilderContext): PageTree.Root {
};
}
export function createPageTreeBuilder(
options: CreatePageTreeBuilderOptions,
): PageTreeBuilder {
function getContext(
builder: PageTreeBuilder,
locale?: string,
): PageTreeBuilderContext {
return {
...options,
lang: locale,
builder,
};
}
export function createPageTreeBuilder(): PageTreeBuilder {
return {
build() {
return build(getContext(this));
build(options) {
return build({
options,
builder: this,
storage: options.storage,
});
},
buildI18n({ languages = [] } = {}) {
buildI18n({ languages = [], ...options }) {
const entries = languages.map<[string, PageTree.Root]>((lang) => {
const tree = build(getContext(this, lang));
const tree = build({
lang,
options,
builder: this,
storage: options.storage,
});
return [lang, tree];
});