Files
planet/frontend/src/pages/Docs/docs-content.ts
linkong e1984c7a35 release: bump version to 0.49.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:42:27 +08:00

251 lines
8.9 KiB
TypeScript

export type DocsGroup = 'Overview' | 'Manual' | 'Earth' | 'Frontend' | 'Backend' | 'Agents' | 'Ops' | 'Other'
export type DocsLang = 'zh' | 'en'
export interface DocsEntry {
slug: string
filename: string
title: string
group: DocsGroup
order: number
access: DocsAccess
}
export type DocsAccess = 'public' | 'docs_user' | 'docs_developer' | 'docs_admin'
export interface DocsCatalogItem {
slug: string
filename: string
lang: DocsLang
title: string
group: DocsGroup
order: number
access: DocsAccess
}
export interface DocsHeading {
id: string
level: number
text: string
}
export interface DocsMetadataEntry {
zh: { title: string; group: DocsGroup; order: number }
en: { title: string; group: DocsGroup; order: number }
}
const DOCS_GROUP_LABELS: Record<DocsLang, Record<DocsGroup, string>> = {
zh: {
Overview: '概览',
Manual: '使用手册',
Earth: '地球可视化',
Frontend: '前端',
Backend: '后端',
Agents: '智能体',
Ops: '运维',
Other: '其他',
},
en: {
Overview: 'Overview',
Manual: 'Manual',
Earth: 'Earth',
Frontend: 'Frontend',
Backend: 'Backend',
Agents: 'Agents',
Ops: 'Ops',
Other: 'Other',
},
}
const DOCS_README_FILENAME = 'README.md'
const MAX_HEADING_ID_LENGTH = 80
export const defaultDocsSlug = 'overview'
export const DOCS_METADATA: Record<string, DocsMetadataEntry> = {
[DOCS_README_FILENAME]: {
zh: { title: '技术文档', group: 'Overview', order: 0 },
en: { title: 'Technical Docs', group: 'Overview', order: 0 },
},
'quickstart.md': {
zh: { title: '快速开始', group: 'Manual', order: 1 },
en: { title: 'Quickstart', group: 'Manual', order: 1 },
},
'manual.md': {
zh: { title: 'Planet 使用手册', group: 'Manual', order: 2 },
en: { title: 'Planet Manual', group: 'Manual', order: 2 },
},
'location-pipeline-user.md': {
zh: { title: 'Earth 位置候选采集使用手册', group: 'Manual', order: 3 },
en: { title: 'Earth Location Candidate Collection User Guide', group: 'Manual', order: 3 },
},
'earth-frontend-context.md': {
zh: { title: 'Earth 前端结构', group: 'Earth', order: 10 },
en: { title: 'Earth Frontend Context', group: 'Earth', order: 10 },
},
'earth-layer-style-reference.md': {
zh: { title: 'Earth 图层样式属性索引', group: 'Earth', order: 11 },
en: { title: 'Earth Layer Style Reference', group: 'Earth', order: 11 },
},
'earth-render-layer-order.md': {
zh: { title: 'Earth 渲染图层顺序', group: 'Earth', order: 12 },
en: { title: 'Earth Render Layer Order', group: 'Earth', order: 12 },
},
'earth-satellite-footprint-policy.md': {
zh: { title: 'Earth 卫星覆盖策略', group: 'Earth', order: 13 },
en: { title: 'Earth Satellite Footprint Policy', group: 'Earth', order: 13 },
},
'earth-bgp-context.md': {
zh: { title: 'BGP 态势上下文', group: 'Earth', order: 14 },
en: { title: 'BGP Context', group: 'Earth', order: 14 },
},
'earth-news-live-streams-collector-format.md': {
zh: { title: '新闻直播采集格式', group: 'Earth', order: 15 },
en: { title: 'News Live Streams Collector Format', group: 'Earth', order: 15 },
},
'earth-interactable-usage.md': {
zh: { title: 'Earth 可交互图标接入', group: 'Earth', order: 16 },
en: { title: 'Earth Interactable Usage', group: 'Earth', order: 16 },
},
'earth-toolbar-overlay-coordination.md': {
zh: { title: 'Earth 工具栏与浮层协同', group: 'Earth', order: 17 },
en: { title: 'Earth Toolbar and Overlay Coordination', group: 'Earth', order: 17 },
},
'frontend-admin-frontend-context.md': {
zh: { title: '控制台前端结构', group: 'Frontend', order: 20 },
en: { title: 'Admin Frontend Context', group: 'Frontend', order: 20 },
},
'frontend-layout-guidelines.md': {
zh: { title: '前端布局指南', group: 'Frontend', order: 21 },
en: { title: 'Frontend Layout Guidelines', group: 'Frontend', order: 21 },
},
'docs-gatekeeper-development.md': {
zh: { title: 'Docs Gatekeeper 开发说明', group: 'Frontend', order: 22 },
en: { title: 'Docs Gatekeeper Development Guide', group: 'Frontend', order: 22 },
},
'backend-collectors.md': {
zh: { title: '数据采集系统', group: 'Backend', order: 30 },
en: { title: 'Data Collectors', group: 'Backend', order: 30 },
},
'backend-system-service-control.md': {
zh: { title: '系统服务控制', group: 'Backend', order: 31 },
en: { title: 'System Service Control', group: 'Backend', order: 31 },
},
'datasource-collector-settings-connectivity.md': {
zh: { title: '数据源、采集器设置与连接验证', group: 'Backend', order: 32 },
en: { title: 'Datasource Collector Settings and Connectivity', group: 'Backend', order: 32 },
},
'backend-datasources-api-performance.md': {
zh: { title: '数据源 API 性能', group: 'Backend', order: 33 },
en: { title: 'Datasource API Performance', group: 'Backend', order: 33 },
},
'location-pipeline-development.md': {
zh: { title: '通用位置估算管线开发说明', group: 'Backend', order: 34 },
en: { title: 'Shared Location Resolution Pipeline Development Guide', group: 'Backend', order: 34 },
},
'agents-aiprovider.md': {
zh: { title: 'AI Provider 指南', group: 'Agents', order: 40 },
en: { title: 'AI Provider Guide', group: 'Agents', order: 40 },
},
'ops-docker-compose-buildx-upgrade.md': {
zh: { title: 'Docker + Compose + Buildx 升级', group: 'Ops', order: 50 },
en: { title: 'Docker + Compose + Buildx Upgrade', group: 'Ops', order: 50 },
},
'ops-planet-sh-startup.md': {
zh: { title: 'planet.sh 启动机制', group: 'Ops', order: 51 },
en: { title: 'planet.sh Startup', group: 'Ops', order: 51 },
},
}
const GROUP_ORDER: DocsGroup[] = ['Overview', 'Manual', 'Earth', 'Frontend', 'Backend', 'Agents', 'Ops', 'Other']
export function slugFromFilename(filename: string): string {
return filename === DOCS_README_FILENAME ? defaultDocsSlug : filename.replace(/\.md$/, '')
}
export function getDocsEntries(lang: DocsLang, catalogItems: DocsCatalogItem[]): DocsEntry[] {
return catalogItems
.filter((item) => item.lang === lang)
.map((item) => ({
slug: item.slug,
filename: item.filename,
title: item.title,
group: item.group,
order: item.order,
access: item.access,
}))
.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title))
}
export function getDocsEntry(slug: string | undefined, entries: DocsEntry[]): DocsEntry | undefined {
const normalizedSlug = slug || defaultDocsSlug
return entries.find((entry) => entry.slug === normalizedSlug)
}
export function groupDocsEntries(entries: DocsEntry[]): Array<{ group: DocsGroup; entries: DocsEntry[] }> {
return GROUP_ORDER.map((group) => ({
group,
entries: entries.filter((entry) => entry.group === group),
})).filter((group) => group.entries.length > 0)
}
export function getDocsGroupLabel(group: DocsGroup, lang: DocsLang): string {
return DOCS_GROUP_LABELS[lang][group]
}
export function createHeadingId(text: string, usedIds: Map<string, number>): string {
const base = text
.toLowerCase()
.replace(/`([^`]+)`/g, '$1')
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.trim()
.replace(/\s+/g, '-')
.slice(0, MAX_HEADING_ID_LENGTH) || 'section'
const count = usedIds.get(base) || 0
usedIds.set(base, count + 1)
return count === 0 ? base : `${base}-${count + 1}`
}
export function extractHeadings(markdown: string): DocsHeading[] {
const usedIds = new Map<string, number>()
return markdown
.split(/\r?\n/)
.map((line) => line.trim().match(/^(#{1,3})\s+(.+)$/))
.filter((match): match is RegExpMatchArray => Boolean(match))
.map((match) => {
const text = match[2].trim()
return {
id: createHeadingId(text, usedIds),
level: match[1].length,
text,
}
})
}
// Returns a factory — call factory() inside MarkdownRenderer to get a fresh resolver
// per render. This is necessary because StrictMode double-invokes renders, which
// would exhaust a shared stateful closure and cause heading IDs to become undefined.
export function createHeadingIdResolver(markdown: string): () => (text: string, level: number) => string | undefined {
const headings = extractHeadings(markdown)
return () => {
const indexByKey = new Map<string, number>()
return (text: string, level: number) => {
const key = `${level}:${text}`
const currentIndex = indexByKey.get(key) || 0
indexByKey.set(key, currentIndex + 1)
const matchingHeadings = headings.filter((heading) => heading.level === level && heading.text === text)
return matchingHeadings[currentIndex]?.id
}
}
}
export function slugFromDocsHref(href: string): string | null {
const normalized = decodeURIComponent(href).split('#')[0].replace(/\\/g, '/')
const filename = normalized.split('/').pop()
if (!filename?.endsWith('.md')) {
return null
}
return slugFromFilename(filename)
}