import { lazy } from "react" import { defaultDocLocale, docLocales, docSections, type DocLocale, type DocLocation, type DocPackage, type DocPage, type DocSection, type DocTocItem, type MdxComponents, } from "./types" import { trans, type Translation } from "../lib/locale" type DocContentModule = { default: React.ComponentType<{ components?: MdxComponents }> } type PackageDefinition = { key: string label: string description: Translation } type ParsedDocPage = DocPage & { packageKey: string } const packageDefinitions: readonly PackageDefinition[] = [ { key: "blocks", label: "Blocks", description: { zh: "构建应用外壳,并接入导航、通知、聊天和外观控制。", us: "Build an application shell and add navigation, notifications, chat, and appearance controls.", }, }, { key: "i18n", label: "I18n", description: { zh: "接入应用自有词典、本地化运行时、开发工具和完整翻译工作流。", us: "Add application-owned catalogs, localized runtime rendering, development tools, and translation workflows.", }, }, { key: "lexical", label: "Lexical", description: { zh: "从根节点和内容区域开始,逐步构建支持自定义操作与媒体的富文本编辑器。", us: "Build a rich-text editor progressively from its root and content surface to custom actions and media.", }, }, { key: "media", label: "Media", description: { zh: "通过存储 Adapter 构建媒体浏览、上传、目录管理和资源选择流程。", us: "Build media browsing, uploads, folder management, and asset selection with a storage adapter.", }, }, { key: "preferences", label: "Preferences", description: { zh: "使用 SSR 快照、持久化回调和可组合副作用管理类型安全且可扩展的应用偏好。", us: "Manage typed, extensible application preferences with SSR snapshots, persistence callbacks, and composable effects.", }, }, { key: "search", label: "Search", description: { zh: "通过可替换的 Adapter、分组结果、分页和搜索历史构建应用级搜索体验。", us: "Build application search with a replaceable adapter, grouped results, pagination, and search history.", }, }, { key: "ui", label: "UI", description: { zh: "配置共享 UI 层,并学习组合组件、Hooks、图标和本地化词典。", us: "Set up the shared UI layer and learn how to compose its components, hooks, icons, and locale catalogs.", }, }, ] const docModuleLoaders = import.meta.glob( "../../../packages/*/docs/*/**/*.mdx", { eager: false } ) const docFrontmatter = import.meta.glob>( "../../../packages/*/docs/*/**/*.mdx", { eager: true, import: "frontmatter" } ) const parsedPages = Object.entries(docFrontmatter).map( ([path, frontmatter]) => { const load = docModuleLoaders[path] if (!load) throw new Error(`Missing documentation module for "${path}".`) return parseDocPage(path, frontmatter, lazy(load)) } ) const packagesByLocale: Record = { "zh-Hans": packageDefinitions.map((definition) => createDocPackage(definition, "zh-Hans") ), "en-US": packageDefinitions.map((definition) => createDocPackage(definition, "en-US") ), } validateLocalizedPagePairs(packagesByLocale) export function getDocPackages(locale: DocLocale = defaultDocLocale) { return packagesByLocale[locale] } export function getDocHref(docPackage: DocPackage, page?: DocPage) { const prefix = getLocalePrefix(docPackage.locale) const target = page ?? getOverviewPage(docPackage) const pagePath = target.section === "overview" ? "" : `/${target.section}/${encodeURIComponent(target.slug)}` return `${prefix}/articles/${docPackage.key}${pagePath}` } export function getDocsHomeHref(locale: DocLocale) { return getLocalePrefix(locale) || "/" } export function getLocalizedHref(pathname: string, locale: DocLocale) { if (/^(?:\/(?:en-US|zh-Hans))?\/settings\/?$/.test(pathname)) { return `${getLocalePrefix(locale)}/settings` } const location = resolveDocLocation(pathname) if (!location) return getDocsHomeHref(locale) const docPackage = getDocPackages(locale).find( (item) => item.key === location.package.key ) const page = docPackage?.pages.find( (item) => item.section === location.page.section && item.slug === location.page.slug ) return docPackage && page ? getDocHref(docPackage, page) : getDocsHomeHref(locale) } export function resolveDocLocale(pathname: string): DocLocale { const firstSegment = pathname.split("/").find(Boolean) return isDocLocale(firstSegment) ? firstSegment : defaultDocLocale } export function resolveDocLocation(pathname: string): DocLocation | undefined { const segments = pathname.split("/").filter(Boolean) let locale: DocLocale = defaultDocLocale if (isDocLocale(segments[0])) { locale = segments.shift() as DocLocale } const [resource, packageKey, sectionValue, pageSlug, ...extra] = segments if (resource !== "articles" || extra.length > 0) return undefined const docPackage = getDocPackages(locale).find( (item) => item.key === packageKey ) if (!docPackage) return undefined if (sectionValue === undefined) { const page = getOverviewPage(docPackage) return { locale, package: docPackage, page } } if (sectionValue === "overview" || !isDocSection(sectionValue) || !pageSlug) { return undefined } const page = docPackage.pages.find( (item) => item.section === sectionValue && item.slug === pageSlug ) return page ? { locale, package: docPackage, page } : undefined } export function getDocSectionLabel(section: DocSection, locale: DocLocale) { const labels: Record = { overview: { zh: "概览", us: "Overview" }, guide: { zh: "指南", us: "Guide" }, advanced: { zh: "进阶", us: "Advanced" }, examples: { zh: "示例", us: "Examples" }, } return trans(locale, labels[section]) } export function getStaticDocRoutes() { return docLocales.flatMap((locale) => { const home = getDocsHomeHref(locale) return [ home, `${home === "/" ? "" : home}/settings`, ...getDocPackages(locale).flatMap((docPackage) => docPackage.pages.map((page) => getDocHref(docPackage, page)) ), ] }) } function createDocPackage( definition: PackageDefinition, locale: DocLocale ): DocPackage { const pages = parsedPages .filter( (page) => page.packageKey === definition.key && page.locale === locale ) .toSorted( (left, right) => docSections.indexOf(left.section) - docSections.indexOf(right.section) || left.order - right.order || left.slug.localeCompare(right.slug) ) const overviewCount = pages.filter( (page) => page.section === "overview" ).length if (overviewCount !== 1) { throw new Error( `${definition.key}/${locale} must contain exactly one docs/${locale}/overview.mdx file.` ) } for (const section of docSections.slice(1)) { if (!pages.some((page) => page.section === section)) { throw new Error( `${definition.key}/${locale} must contain at least one ${section} document.` ) } } return { key: definition.key, label: definition.label, description: trans(locale, definition.description), locale, pages, } } function parseDocPage( path: string, frontmatter: Record, Content: DocPage["Content"] ): ParsedDocPage { const match = path.match( /\/packages\/([^/]+)\/docs\/([^/]+)\/(overview|guide|advanced|examples)(?:\/([^/]+))?\.mdx$/ ) if (!match) { throw new Error( `Invalid documentation path "${path}". Expected docs//overview.mdx or docs///*.mdx.` ) } const [, packageKey, localeValue, sectionValue, nestedSlug] = match if (!isDocLocale(localeValue)) { throw new Error( `Unsupported documentation locale "${localeValue}" in "${path}".` ) } if (!isDocSection(sectionValue)) { throw new Error(`Unsupported documentation section in "${path}".`) } if ( (sectionValue === "overview" && nestedSlug !== undefined) || (sectionValue !== "overview" && nestedSlug === undefined) ) { throw new Error(`Documentation file is in the wrong directory: "${path}".`) } return { Content, description: readString(frontmatter, "description"), locale: localeValue, order: readNumber(frontmatter, "order"), packageKey, section: sectionValue, slug: nestedSlug ?? "overview", title: readString(frontmatter, "title"), toc: readToc(frontmatter.toc), } } function validateLocalizedPagePairs( packages: Record ) { for (const definition of packageDefinitions) { const keysByLocale = docLocales.map((locale) => { const docPackage = packages[locale].find( (item) => item.key === definition.key )! return docPackage.pages.map((page) => `${page.section}/${page.slug}`) }) if (keysByLocale[0]?.join("\n") !== keysByLocale[1]?.join("\n")) { throw new Error( `${definition.key} documentation must contain matching zh-Hans and en-US page paths.` ) } } } function getOverviewPage(docPackage: DocPackage) { return docPackage.pages.find((page) => page.section === "overview")! } function getLocalePrefix(locale: DocLocale) { return locale === defaultDocLocale ? "" : `/${locale}` } function isDocLocale(value: unknown): value is DocLocale { return docLocales.some((locale) => locale === value) } function isDocSection(value: unknown): value is DocSection { return docSections.some((section) => section === value) } function readString(frontmatter: Record, key: string) { const value = frontmatter[key] if (typeof value === "string" && value.trim()) return value throw new Error(`Documentation frontmatter requires a string "${key}".`) } function readNumber(frontmatter: Record, key: string) { const value = frontmatter[key] if (typeof value === "number" && Number.isFinite(value)) return value throw new Error(`Documentation frontmatter requires a number "${key}".`) } function readToc(value: unknown): readonly DocTocItem[] { if (value === undefined) return [] if (!Array.isArray(value)) { throw new Error('Documentation frontmatter "toc" must be an array.') } return value.map((item) => { if (!isRecord(item)) { throw new Error('Each documentation "toc" entry must be an object.') } return { id: readString(item, "id"), title: readString(item, "title"), items: item.items === undefined ? undefined : readToc(item.items), } }) } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null }