UNPKG

fumadocs-core

Version:

The React.js library for building a documentation website

636 lines (629 loc) 19.6 kB
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