feat(docs): add bilingual package documentation site

This commit is contained in:
Maofeng
2026-09-20 15:53:28 +08:00
parent 60f6f1fb1a
commit dc80bd8ae0
111 changed files with 6531 additions and 0 deletions
@@ -0,0 +1,179 @@
import { I18nProvider } from "@workspace/i18n"
import {
SearchDialog,
SearchProvider,
type SearchAdapter,
type SearchResultItem,
} from "@workspace/search"
import { messages as searchMessagesEnUS } from "@workspace/search/locales/en-US"
import { messages as searchMessagesZhHans } from "@workspace/search/locales/zh-Hans"
import React from "react"
import {
getDocHref,
getDocPackages,
getDocSearchText,
getDocSectionLabel,
} from "../content/registry"
import type {
DocLocale,
DocPackage,
DocPage,
DocTocItem,
} from "../content/types"
import { useLocale } from "../lib/locale"
const searchCatalogs = {
"en-US": searchMessagesEnUS,
"zh-Hans": searchMessagesZhHans,
} as const
type DocsSearchPayload = {
href: string
}
export function DocsSearchProvider({
children,
}: {
children: React.ReactNode
}) {
const locale = useLocale()
const adapter = React.useMemo(() => createDocsSearchAdapter(locale), [locale])
return (
<I18nProvider catalogs={searchCatalogs} locale={locale}>
<SearchProvider
adapter={adapter}
historyStorageKey={`docs-search-history:${locale}`}
onSelect={({ item }) => {
if (isDocsSearchPayload(item.payload)) navigate(item.payload.href)
}}
>
{children}
<SearchDialog className="font-sans" />
</SearchProvider>
</I18nProvider>
)
}
function createDocsSearchAdapter(locale: DocLocale): SearchAdapter {
const docPackages = getDocPackages(locale)
return {
async search({ query, signal }) {
const normalizedQuery = normalize(query)
if (!normalizedQuery) return { groups: [] }
const terms = normalizedQuery.split(/\s+/).filter(Boolean)
const groups = docPackages.flatMap((docPackage) => {
const items = docPackage.pages
.map((page) => ({
item: createResultItem(docPackage, page, locale),
score: getMatchScore(
normalizedQuery,
terms,
docPackage.label,
page
),
}))
.filter((result) => result.score >= 0)
.sort(
(left, right) =>
right.score - left.score ||
left.item.title.localeCompare(right.item.title, locale)
)
.map((result) => result.item)
return items.length > 0
? [
{
id: docPackage.key,
label: docPackage.label,
items,
total: items.length,
},
]
: []
})
if (signal.aborted) return { groups: [] }
return { groups }
},
}
}
function createResultItem(
docPackage: DocPackage,
page: DocPage,
locale: DocLocale
): SearchResultItem {
return {
id: `${page.section}/${page.slug}`,
title: page.title,
description: page.description,
keywords: [
docPackage.label,
getDocSectionLabel(page.section, locale),
...page.toc.flatMap(flattenTocTitles),
getDocSearchText(page),
],
meta: [getDocSectionLabel(page.section, locale)],
payload: {
href: getDocHref(docPackage, page),
} satisfies DocsSearchPayload,
}
}
function getMatchScore(
query: string,
terms: readonly string[],
packageLabel: string,
page: DocPage
) {
const title = normalize(page.title)
const description = normalize(page.description)
const packageName = normalize(packageLabel)
const toc = normalize(page.toc.flatMap(flattenTocTitles).join(" "))
const body = normalize(getDocSearchText(page))
const searchableText = [packageName, title, description, toc, body].join(" ")
if (!terms.every((term) => searchableText.includes(term))) return -1
let score = 0
if (title === query) score += 100
else if (title.startsWith(query)) score += 70
else if (title.includes(query)) score += 50
if (packageName === query) score += 40
else if (packageName.includes(query)) score += 20
if (description.includes(query)) score += 15
if (toc.includes(query)) score += 10
if (page.section === "overview") score += 2
return score
}
function flattenTocTitles(item: DocTocItem): readonly string[] {
return [item.title, ...(item.items?.flatMap(flattenTocTitles) ?? [])]
}
function normalize(value: string) {
return value.normalize("NFKC").toLocaleLowerCase().trim()
}
function isDocsSearchPayload(value: unknown): value is DocsSearchPayload {
return (
typeof value === "object" &&
value !== null &&
"href" in value &&
typeof value.href === "string"
)
}
function navigate(href: string) {
const destination = new URL(href, window.location.href)
if (destination.href === window.location.href) return
window.history.pushState(null, "", destination)
window.scrollTo({ left: 0, top: 0 })
window.dispatchEvent(new PopStateEvent("popstate"))
}