fumadocs-core
Version:
The React.js library for building a documentation website
636 lines (629 loc) • 19.6 kB
JavaScript
import { t as normalizeUrl } from "../normalize-url-DP9-1I-S.js";
import { c as visit } from "../utils-DUvi2WkD.js";
import { a as path_exports, i as joinPath, n as dirname, o as slash, r as extname, s as splitPath, t as basename } from "../path-DHIjrDBP.js";
import { getSlugs, slugsPlugin } from "./plugins/slugs.js";
import { t as iconPlugin } from "../icon-Dt7IObrc.js";
import path from "node:path";
//#region src/source/source.ts
function multiple(sources) {
const out = { files: [] };
for (const [type, source$1] of Object.entries(sources)) for (const file of source$1.files) out.files.push({
...file,
data: {
...file.data,
type
}
});
return out;
}
function source(config) {
return { files: [...config.pages, ...config.metas] };
}
/**
* update a source object in-place.
*/
function update(source$1) {
return {
files(fn) {
source$1.files = fn(source$1.files);
return this;
},
page(fn) {
for (let i = 0; i < source$1.files.length; i++) {
const file = source$1.files[i];
if (file.type === "page") source$1.files[i] = fn(file);
}
return this;
},
meta(fn) {
for (let i = 0; i < source$1.files.length; i++) {
const file = source$1.files[i];
if (file.type === "meta") source$1.files[i] = fn(file);
}
return this;
},
build() {
return source$1;
}
};
}
//#endregion
//#region src/source/storage/file-system.ts
/**
* In memory file system.
*/
var FileSystem = class {
constructor(inherit) {
this.files = /* @__PURE__ */ new Map();
this.folders = /* @__PURE__ */ new Map();
if (inherit) {
for (const [k, v] of inherit.folders) this.folders.set(k, v);
for (const [k, v] of inherit.files) this.files.set(k, v);
} else this.folders.set("", []);
}
read(path$1) {
return this.files.get(path$1);
}
/**
* get the direct children of folder (in virtual file path)
*/
readDir(path$1) {
return this.folders.get(path$1);
}
write(path$1, file) {
if (!this.files.has(path$1)) {
const dir = dirname(path$1);
this.makeDir(dir);
this.readDir(dir)?.push(path$1);
}
this.files.set(path$1, file);
}
/**
* Delete files at specified path.
*
* @param path - the target path.
* @param [recursive=false] - if set to `true`, it will also delete directories.
*/
delete(path$1, recursive = false) {
if (this.files.delete(path$1)) return true;
if (recursive) {
const folder = this.folders.get(path$1);
if (!folder) return false;
this.folders.delete(path$1);
for (const child of folder) this.delete(child);
return true;
}
return false;
}
getFiles() {
return Array.from(this.files.keys());
}
makeDir(path$1) {
const segments = splitPath(path$1);
for (let i = 0; i < segments.length; i++) {
const segment = segments.slice(0, i + 1).join("/");
if (this.folders.has(segment)) continue;
this.folders.set(segment, []);
this.folders.get(dirname(segment)).push(segment);
}
}
};
//#endregion
//#region src/source/storage/content.ts
function isLocaleValid(locale) {
return locale.length > 0 && !/\d+/.test(locale);
}
const parsers = {
dir(path$1) {
const [locale, ...segs] = path$1.split("/");
if (locale && segs.length > 0 && isLocaleValid(locale)) return [segs.join("/"), locale];
return [path$1];
},
dot(path$1) {
const dir = dirname(path$1);
const parts = basename(path$1).split(".");
if (parts.length < 3) return [path$1];
const [locale] = parts.splice(parts.length - 2, 1);
if (!isLocaleValid(locale)) return [path$1];
return [joinPath(dir, parts.join(".")), locale];
},
none(path$1) {
return [path$1];
}
};
/**
* @param defaultLanguage - language to use when i18n is not configured.
* @returns a map of locale and its content storage.
*
* in the storage, locale codes are removed from file paths, hence the same file will have same file paths in every storage.
*/
function buildContentStorage(loaderConfig, defaultLanguage) {
const { source: source$1, plugins = [], i18n = {
defaultLanguage,
parser: "none",
languages: [defaultLanguage]
} } = loaderConfig;
const parser = parsers[i18n.parser ?? "dot"];
const storages = {};
const normalized = /* @__PURE__ */ new Map();
for (const inputFile of source$1.files) {
let file;
if (inputFile.type === "page") file = {
format: "page",
path: normalizePath(inputFile.path),
slugs: inputFile.slugs,
data: inputFile.data,
absolutePath: inputFile.absolutePath
};
else file = {
format: "meta",
path: normalizePath(inputFile.path),
absolutePath: inputFile.absolutePath,
data: inputFile.data
};
const [pathWithoutLocale, locale = i18n.defaultLanguage] = parser(file.path);
const list = normalized.get(locale) ?? [];
list.push({
pathWithoutLocale,
file
});
normalized.set(locale, list);
}
const fallbackLang = i18n.fallbackLanguage !== null ? i18n.fallbackLanguage ?? i18n.defaultLanguage : null;
function scan(lang) {
if (storages[lang]) return;
let storage;
if (fallbackLang && fallbackLang !== lang) {
scan(fallbackLang);
storage = new FileSystem(storages[fallbackLang]);
} else storage = new FileSystem();
for (const { pathWithoutLocale, file } of normalized.get(lang) ?? []) storage.write(pathWithoutLocale, file);
const context = { storage };
for (const plugin of plugins) plugin.transformStorage?.(context);
storages[lang] = storage;
}
for (const lang of i18n.languages) scan(lang);
return storages;
}
/**
* @param path - Relative path
* @returns Normalized path, with no trailing/leading slashes
* @throws Throws error if path starts with `./` or `../`
*/
function normalizePath(path$1) {
const segments = splitPath(slash(path$1));
if (segments[0] === "." || segments[0] === "..") throw new Error("It must not start with './' or '../'");
return segments.join("/");
}
//#endregion
//#region src/source/page-tree/transformer-fallback.ts
function transformerFallback() {
const addedFiles = /* @__PURE__ */ new Set();
return {
root(root) {
const isolatedStorage = new FileSystem();
for (const file of this.storage.getFiles()) {
if (addedFiles.has(file)) continue;
const content = this.storage.read(file);
if (content) isolatedStorage.write(file, content);
}
if (isolatedStorage.getFiles().length === 0) return root;
root.fallback = this.builder.build(isolatedStorage, {
...this.options,
id: `fallback-${root.$id ?? ""}`,
generateFallback: false
});
addedFiles.clear();
return root;
},
file(node, file) {
if (file) addedFiles.add(file);
return node;
},
folder(node, _dir, metaPath) {
if (metaPath) addedFiles.add(metaPath);
return node;
}
};
}
//#endregion
//#region src/source/page-tree/builder.ts
const group = /^\((?<name>.+)\)$/;
const link = /^(?<external>external:)?(?:\[(?<icon>[^\]]+)])?\[(?<name>[^\]]+)]\((?<url>[^)]+)\)$/;
const separator = /^---(?:\[(?<icon>[^\]]+)])?(?<name>.+)---|^---$/;
const rest = "...";
const restReversed = "z...a";
const extractPrefix = "...";
const excludePrefix = "!";
function createPageTreeBuilder(loaderConfig) {
const { plugins = [], url, pageTree: defaultOptions = {} } = loaderConfig;
return {
build(storage, options = defaultOptions) {
const key = "";
return this.buildI18n({ [key]: storage }, options)[key];
},
buildI18n(storages, options = defaultOptions) {
let nextId = 0;
const out = {};
const transformers = [];
if (options.transformers) transformers.push(...options.transformers);
for (const plugin of plugins) if (plugin.transformPageTree) transformers.push(plugin.transformPageTree);
if (options.generateFallback ?? true) transformers.push(transformerFallback());
for (const [locale, storage] of Object.entries(storages)) {
let rootId = locale.length === 0 ? "root" : locale;
if (options.id) rootId = `${options.id}-${rootId}`;
out[locale] = createPageTreeBuilderUtils({
rootId,
transformers,
builder: this,
options,
getUrl: url,
locale,
storage,
storages,
generateNodeId() {
return "_" + nextId++;
}
}).root();
}
return out;
}
};
}
function createFlattenPathResolver(storage) {
const map = /* @__PURE__ */ new Map();
const files = storage.getFiles();
for (const file of files) {
const content = storage.read(file);
const flattenPath = file.substring(0, file.length - extname(file).length);
map.set(flattenPath + "." + content.format, file);
}
return (name, format) => {
return map.get(name + "." + format) ?? name;
};
}
function createPageTreeBuilderUtils(ctx) {
const resolveFlattenPath = createFlattenPathResolver(ctx.storage);
const visitedPaths = /* @__PURE__ */ new Set();
function nextNodeId(localId = ctx.generateNodeId()) {
return `${ctx.rootId}:${localId}`;
}
return {
buildPaths(paths, reversed = false) {
const items = [];
const folders = [];
const sortedPaths = paths.sort((a, b) => a.localeCompare(b) * (reversed ? -1 : 1));
for (const path$1 of sortedPaths) {
const fileNode = this.file(path$1);
if (fileNode) {
if (basename(path$1, extname(path$1)) === "index") items.unshift(fileNode);
else items.push(fileNode);
continue;
}
const dirNode = this.folder(path$1, false);
if (dirNode) folders.push(dirNode);
}
items.push(...folders);
return items;
},
resolveFolderItem(folderPath, item) {
if (item === rest || item === restReversed) return item;
let match = separator.exec(item);
if (match?.groups) {
let node = {
$id: nextNodeId(),
type: "separator",
icon: match.groups.icon,
name: match.groups.name
};
for (const transformer of ctx.transformers) {
if (!transformer.separator) continue;
node = transformer.separator.call(ctx, node);
}
return [node];
}
match = link.exec(item);
if (match?.groups) {
const { icon, url, name, external } = match.groups;
let node = {
$id: nextNodeId(),
type: "page",
icon,
name,
url,
external: external ? true : void 0
};
for (const transformer of ctx.transformers) {
if (!transformer.file) continue;
node = transformer.file.call(ctx, node);
}
return [node];
}
const isExcept = item.startsWith(excludePrefix);
const isExtract = !isExcept && item.startsWith(extractPrefix);
let filename = item;
if (isExcept) filename = item.slice(1);
else if (isExtract) filename = item.slice(3);
const path$1 = resolveFlattenPath(joinPath(folderPath, filename), "page");
if (isExcept) {
visitedPaths.add(path$1);
return [];
}
const dirNode = this.folder(path$1, false);
if (dirNode) return isExtract ? dirNode.children : [dirNode];
const fileNode = this.file(path$1);
return fileNode ? [fileNode] : [];
},
folder(folderPath, isGlobalRoot) {
const { storage, options, transformers } = ctx;
const files = storage.readDir(folderPath);
if (!files) return;
const metaPath = resolveFlattenPath(joinPath(folderPath, "meta"), "meta");
const indexPath = resolveFlattenPath(joinPath(folderPath, "index"), "page");
let meta = storage.read(metaPath);
if (meta && meta.format !== "meta") meta = void 0;
const metadata = meta?.data ?? {};
const { root = isGlobalRoot, pages } = metadata;
let index;
let children;
if (pages) {
const resolved = pages.flatMap((item) => this.resolveFolderItem(folderPath, item));
if (!root && !visitedPaths.has(indexPath)) index = this.file(indexPath);
for (let i = 0; i < resolved.length; i++) {
const item = resolved[i];
if (item !== rest && item !== restReversed) continue;
const items = this.buildPaths(files.filter((file) => !visitedPaths.has(file)), item === restReversed);
resolved.splice(i, 1, ...items);
break;
}
children = resolved;
} else {
if (!root && !visitedPaths.has(indexPath)) index = this.file(indexPath);
children = this.buildPaths(files.filter((file) => !visitedPaths.has(file)));
}
let node = {
type: "folder",
name: metadata.title ?? index?.name ?? (() => {
const folderName = basename(folderPath);
return pathToName(group.exec(folderName)?.[1] ?? folderName);
})(),
icon: metadata.icon ?? index?.icon,
root: metadata.root,
defaultOpen: metadata.defaultOpen,
description: metadata.description,
collapsible: metadata.collapsible,
index,
children,
$id: nextNodeId(folderPath),
$ref: !options.noRef && meta ? { metaFile: metaPath } : void 0
};
visitedPaths.add(folderPath);
for (const transformer of transformers) {
if (!transformer.folder) continue;
node = transformer.folder.call(ctx, node, folderPath, metaPath);
}
return node;
},
file(path$1) {
const { options, getUrl, storage, locale, transformers } = ctx;
const page = storage.read(path$1);
if (page?.format !== "page") return;
const { title, description, icon } = page.data;
let item = {
$id: nextNodeId(path$1),
type: "page",
name: title ?? pathToName(basename(path$1, extname(path$1))),
description,
icon,
url: getUrl(page.slugs, locale),
$ref: !options.noRef ? { file: path$1 } : void 0
};
visitedPaths.add(path$1);
for (const transformer of transformers) {
if (!transformer.file) continue;
item = transformer.file.call(ctx, item, path$1);
}
return item;
},
root() {
const folder = this.folder("", true);
let root = {
$id: ctx.rootId,
name: folder.name || "Docs",
children: folder.children
};
for (const transformer of ctx.transformers) {
if (!transformer.root) continue;
root = transformer.root.call(ctx, root);
}
return root;
}
};
}
/**
* Get item name from file name
*
* @param name - file name
*/
function pathToName(name) {
const result = [];
for (const c of name) if (result.length === 0) result.push(c.toLocaleUpperCase());
else if (c === "-") result.push(" ");
else result.push(c);
return result.join("");
}
//#endregion
//#region src/source/loader.ts
function indexPages(storages, { url }) {
const result = {
pages: /* @__PURE__ */ new Map(),
pathToMeta: /* @__PURE__ */ new Map(),
pathToPage: /* @__PURE__ */ new Map()
};
for (const [lang, storage] of Object.entries(storages)) for (const filePath of storage.getFiles()) {
const item = storage.read(filePath);
const path$1 = `${lang}.${filePath}`;
if (item.format === "meta") {
result.pathToMeta.set(path$1, {
path: item.path,
absolutePath: item.absolutePath,
data: item.data
});
continue;
}
const page = {
absolutePath: item.absolutePath,
path: item.path,
url: url(item.slugs, lang),
slugs: item.slugs,
data: item.data,
locale: lang
};
result.pathToPage.set(path$1, page);
result.pages.set(`${lang}.${page.slugs.join("/")}`, page);
}
return result;
}
function createGetUrl(baseUrl, i18n) {
const baseSlugs = baseUrl.split("/");
return (slugs, locale) => {
const hideLocale = i18n?.hideLocale ?? "never";
let urlLocale;
if (hideLocale === "never") urlLocale = locale;
else if (hideLocale === "default-locale" && locale !== i18n?.defaultLanguage) urlLocale = locale;
const paths = [...baseSlugs, ...slugs];
if (urlLocale) paths.unshift(urlLocale);
return `/${paths.filter((v) => v.length > 0).join("/")}`;
};
}
function loader(...args) {
const loaderConfig = args.length === 2 ? resolveConfig(args[0], args[1]) : resolveConfig(args[0].source, args[0]);
const { i18n } = loaderConfig;
const defaultLanguage = i18n?.defaultLanguage ?? "";
const storages = buildContentStorage(loaderConfig, defaultLanguage);
const walker = indexPages(storages, loaderConfig);
const builder = createPageTreeBuilder(loaderConfig);
let pageTrees;
function getPageTrees() {
return pageTrees ??= builder.buildI18n(storages);
}
return {
_i18n: i18n,
get pageTree() {
const trees = getPageTrees();
return i18n ? trees : trees[defaultLanguage];
},
set pageTree(v) {
if (i18n) pageTrees = v;
else {
pageTrees ??= {};
pageTrees[defaultLanguage] = v;
}
},
getPageByHref(href, { dir = "", language = defaultLanguage } = {}) {
const [value, hash] = href.split("#", 2);
let target;
if (value.startsWith("./")) {
const path$1 = joinPath(dir, value);
target = walker.pathToPage.get(`${language}.${path$1}`);
} else target = this.getPages(language).find((item) => item.url === value);
if (target) return {
page: target,
hash
};
},
resolveHref(href, parent) {
if (href.startsWith("./")) {
const target = this.getPageByHref(href, {
dir: path.dirname(parent.path),
language: parent.locale
});
if (target) return target.hash ? `${target.page.url}#${target.hash}` : target.page.url;
}
return href;
},
getPages(language) {
const pages = [];
for (const [key, value] of walker.pages.entries()) if (language === void 0 || key.startsWith(`${language}.`)) pages.push(value);
return pages;
},
getLanguages() {
const list = [];
if (!i18n) return list;
for (const language of i18n.languages) list.push({
language,
pages: this.getPages(language)
});
return list;
},
getPage(slugs = [], language = defaultLanguage) {
let page = walker.pages.get(`${language}.${slugs.join("/")}`);
if (page) return page;
page = walker.pages.get(`${language}.${slugs.map(decodeURI).join("/")}`);
if (page) return page;
},
getNodeMeta(node, language = defaultLanguage) {
const ref = node.$ref?.metaFile;
if (!ref) return;
return walker.pathToMeta.get(`${language}.${ref}`);
},
getNodePage(node, language = defaultLanguage) {
const ref = node.$ref?.file;
if (!ref) return;
return walker.pathToPage.get(`${language}.${ref}`);
},
getPageTree(locale = defaultLanguage) {
const trees = getPageTrees();
return trees[locale] ?? trees[defaultLanguage];
},
generateParams(slug, lang) {
if (i18n) return this.getLanguages().flatMap((entry) => entry.pages.map((page) => ({
[slug ?? "slug"]: page.slugs,
[lang ?? "lang"]: entry.language
})));
return this.getPages().map((page) => ({ [slug ?? "slug"]: page.slugs }));
},
async serializePageTree(tree) {
const { renderToString } = await import("react-dom/server.edge");
return visit(tree, (node) => {
node = { ...node };
if ("icon" in node && node.icon) node.icon = renderToString(node.icon);
if (node.name) node.name = renderToString(node.name);
if ("children" in node) node.children = [...node.children];
return node;
});
}
};
}
function resolveConfig(source$1, { slugs, icon, plugins = [], baseUrl, url, ...base }) {
let config = {
...base,
url: url ? (...args) => normalizeUrl(url(...args)) : createGetUrl(baseUrl, base.i18n),
source: source$1,
plugins: buildPlugins([
icon && iconPlugin(icon),
...typeof plugins === "function" ? plugins({ typedPlugin: (plugin) => plugin }) : plugins,
slugsPlugin(slugs)
])
};
for (const plugin of config.plugins ?? []) {
const result = plugin.config?.(config);
if (result) config = result;
}
return config;
}
const priorityMap = {
pre: 1,
default: 0,
post: -1
};
function buildPlugins(plugins, sort = true) {
const flatten = [];
for (const plugin of plugins) if (Array.isArray(plugin)) flatten.push(...buildPlugins(plugin, false));
else if (plugin) flatten.push(plugin);
if (sort) return flatten.sort((a, b) => priorityMap[b.enforce ?? "default"] - priorityMap[a.enforce ?? "default"]);
return flatten;
}
//#endregion
export { FileSystem, path_exports as PathUtils, createGetUrl, getSlugs, loader, multiple, source, update };
//# sourceMappingURL=index.js.map